API Queue Using Await/Async?

Hi,

Wondering if anyone can help me with this question. I have a lot of async APIs called from various parts of my application. They all manage login tokens and follow this kind of pattern:

    func syncData() async -> Void {
        do {
            let timezone = TimeZone.current.secondsFromGMT()
            let timezoneInHours = timezone / 60 / 60
            if let data = try await APIHelper.authCall(data: [ "timezone" : timezoneInHours ], method: "POST", url: "v1/retrieve") {
      
                let decoder = JSONDecoder()
                let jsonData = try decoder.decode(ResponseObject.self, from: data)
                if let tokenData = jsonData.token {
                   
                    // Processing

            }
        }
        catch(let error) {
            print(error)
        }
    }

the APIHelper.authCall() is just a wrapper for firing APIs with some additional data sent automatically in the header etc.

The problem I am now facing is that there appears to be overlap between different APIs as they can fire all at once if the users state changes a lot...


.onChange(of: data2) { oldValue, newValue in
    Task {
        await callAPI()
    }
}
.onChange(of: data1) { oldValue, newValue in
    Task {
        await callAPI()
    }
}
.onAppear { 
    Task {
        await callAPI()
    }
}

It seems the await APIs work a bit like other languages where multiple APIs can be fired off at the same time, but the response may appear later and occasionally in a different order. Since some of these handle login state, it can sometimes lead to users being assigned an old token and being logged out.

To solve this, I would like each API to be added to a global queue which is accessible from anywhere in the application, and waits for a ResponseObject type before firing the next API in the queue. hat way I can ensure that the login token is always correct and up to date. I was trying to accomplish this through a dispatch queue but didn't have much luck with await/async functions. I also tried with actors but faced a couple of problems.

Is there a best practice or standard way to do this? It seems like it would be quite a common problem - but I haven't found a standard process for doing it in Swift - is the answer using actors?

You'll probably encounter a similar pitfall with using "just" an actor without any additional setup, since those are reentrant, and not working around this fact is a common source of ordering bugs.

I would suggest instead to either:

  • Create an actual queue using, for instance, AsyncStream from the standard library. The items you submit to it could either be closures (and the stream's receiver simply calls them in a loop) or input values to whichever function you want to serialize.
  • "Link up" the tasks that you spawn in a way that every newly created task first awaits the completion of the previous one:
let newTask = Task {
    let _ = await currentTask.value
    await callAPI()
}

currentTask = newTask

— just make sure that you make this swap in one atomic operation, however. The latter approach requires a bit more setup, but has the advantage that you can make it support task cancellation if you care about it (while you can't "cancel" closures in an AsyncStream).

It's still possible to achieve full exclusion of work on an actor if you want to go this route however — one possible approach is to delimit every function with a Semaphore.

2 Likes