Search code examples
swiftasync-awaitconcurrency

Suspend multiple async/await function calls until 1 network call returns (Swift)


I use a token to complete a lot of tasks in my app. When my token expires, I want to send 1 token refresh network call to my backend even if multiple tasks fail and request the token at the same time. This was easy with completion handlers, as I could just send a network call on the first token refresh request and save subsequent token refresh request completion handlers in an array until the network call returned, then I'd call all the completion handlers with the new token. Is there a way to suspend multiple async/await function calls like that while I wait for the network call to complete? Is the answer to use continuations?


Solution

  • You should simply await a saved Task. E.g.:

    actor NetworkManager {
        var tokenTask: Task<String, Error>?
        
        func token() async throws -> String {
            if let tokenTask {
                print("awaiting prior request")
                return try await tokenTask.value
            }
            
            let task = Task {
                print("starting request")
        
                // perform network request here
            }
            tokenTask = task
            
            return try await withTaskCancellationHandler {
                try await task.value
            } onCancel: {
                task.cancel()
            }
        }
    }
    

    Note, because Task {…} is unstructured concurrency, we want to wrap the awaiting of its value in withTaskCancellationHandler so that the caller has the option to cancel the task, should it ever need to do so. As a general rule, when using unstructured concurrency, we bear the burden of handling cancelation ourselves.

    Apple introduced us to this pattern (of multiple await on the same Task) in the code accompanying WWDC 2021 video Protect mutable state with Swift actors.