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()
}
}