Pankaj Gaikar
Pankaj Gaikar

Reputation: 2483

Swift - Queueing Combine Requests

I am working on a Combine request which I want to either execute or queue to execute after a certain event. Below is the scenario -

  1. New request is generated.
  2. Check if the app has an access token
  3. If yes, execute the request
  4. If no, fetch token, then execute the request

Below is my API where every request will be triggered -

public func fetchData<T: Codable>(to request: URLRequest) -> AnyPublisher<Result<T>, Error> {
    
    if hasToken {
        return self.urlSession.dataTaskPublisher(for: request)
            .tryMap(self.parseJson)
            .receive(on: RunLoop.main)
            .subscribe(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    else {
        // Store request somewhere
        // Get token
        // Execute stored request
    }
}

I would really appreciate it if anyone can suggest how I can proceed with the else part of my code.

Upvotes: 2

Views: 997

Answers (1)

New Dev
New Dev

Reputation: 49590

The approach you're starting with wouldn't work if there were multiple requests in quick succession happening. All these requests would see hasToken as being false, and they will all initiate a token request.

You could possibly create a var tokenRequested: Bool property and synchronize access to it.

The way I approached this - not sure if this is the best approach - was by creating a pipeline through which all request would be queued:

class Service {
    
    private let requestToken = PassthroughSubject<Void, Error>()
    private let tokenSubject = CurrentValueSubject<Token?, Error>(nil)

    var c: Set<AnyCancellable> = []
    
    init() {
        requestToken.zip(tokenSubject)
            .flatMap { (_, token) -> AnyPublisher<Token, Error> in
                if let token = token {
                    return Just(token).setFailureType(to: Error.self)
                                      .eraseToAnyPublisher()
                } else {
                    return self.fetchToken()
                               .eraseToAnyPublisher()
                }
            }
            .map { $0 as Token? }
            .subscribe(tokenSubject)
            .store(in: &c)
    }
   
    private func fetchToken() -> AnyPublisher<Token, Error> {
        // async code to fetch the token
    }
}

Any request via requestToken syncs with a value from tokenSubject, so the first one goes together with the initial nil, but subsequent ones wait until publishes the next value, which happens when the previous one completes.

Then, to make a request, you'd first get the token getToken(), then make the request.

extension Service {
    private func getToken() -> AnyPublisher<Token, Error> {
        // request token, which starts queues it in the pipeline
        requestToken.send(())

        // wait until next token is available
        return tokenSubject
            .compactMap { $0 } // non-nil token
            .first() // only one
            .eraseToAnyPublisher()
    }

    func fetchData<T: Codable>(request: URLRequest) -> AnyPublisher<T, Error> {
        getToken()
            .flatMap { token in
                // make request
            }
            .eraseToAnyPublisher()
    }
}

Upvotes: 3

Related Questions