Swift Concurrency: Hold off Tasks until certain data is loaded

166 views Asked by At

I've been using Swift Concurrency for 2 years, but can't get my head around how to resolve this situation.

In a class named APIManager, there is an execute method that performs a network call. When the server responds with a 401, the bearer token needs to be refreshed & the call retried.

Looks roughly like this:

class APIManager {
   func execute<R: ResponseProtocol>(request: RequestProtocol, response: R) async throws -> R.Value {
       let session = getURLSession()
       let urlRequest: URLRequest = request.buildURLRequest()

       let (data, urlResponse) = try await session.data(for: urlRequest)

       if let httpURLResponse = urlResponse as? HTTPURLResponse,
          httpURLResponse.statusCode == 401 {
          try await renewBearerToken()
          return try await execute(request: request, response: response)
       }

       return decode(data: data)
   }
}

There is only one instance of this class (injected into every service). Tasks are fired randomly around the application (like on button clicks), and invoke this method.

What I would like to achieve is whenever the class is loading data, especially when refreshing a token, make sure any other parallel fired Task waits for its completion before also completing the call (or it might risk hitting that 401 too).

Any ideas if and how this can be enforced? TaskGroups, Actors?

1

There are 1 answers

0
Rob On

You can save a reference to the Task for the bearer token request, and await it:

actor APIManager {
    private var bearerTask: Task<Void, Error>?
    
    func execute<R: ResponseProtocol>(request: RequestProtocol, response: R) async throws -> R.Value {
        _ = try await bearerTask?.value
        
        let session = getURLSession()
        let urlRequest: URLRequest = request.buildURLRequest()
        
        let (data, urlResponse) = try await session.data(for: urlRequest)
        
        if let httpURLResponse = urlResponse as? HTTPURLResponse, httpURLResponse.statusCode == 401 {
            // if 401, only create new bearer task if one hasn’t already been created yet

            if bearerTask == nil {
                bearerTask = Task {
                    defer { bearerTask = nil }
                    try await renewBearerToken()
                }
            }

            // now try request again (which will wait for the bearer task created above

            return try await execute(request: request, response: response)
        }
        
        return decode(data: data)
    }
}

I would make the API manager an actor (or isolate it to a global actor) to avoid races on bearerTask. And I check whether the bearer task is nil to avoid multiple, redundant bearer token requests if you just happened to quickly fire off many requests with the invalid token before the first of them gets around to returning the 401.

But the key observation is that multiple API calls can await the same bearerTask.