How to prevent from calling async function many times but call completion for each of them?

Viewed 438

This is code I am using currently:

typealias ResponseHandler = (SomeResponse?, Error?) -> Void

class LoginService {
    private var authorizeTokenCompletions = [ResponseHandler]()
    func authorizeToken(withRefreshToken refreshToken: String, completion: @escaping ResponseHandler) {
        if authorizeTokenCompletions.isEmpty {
            authorizeTokenCompletions.append(completion)
            post { [weak self] response, error in
                self?.authorizeTokenCompletions.forEach { $0(response, error) }
                self?.authorizeTokenCompletions.removeAll()
            }
        } else {
            authorizeTokenCompletions.append(completion)
        }
    }

    private func post(completion: @escaping ResponseHandler) {
        // async
        completion(nil, nil)
    }
}

What is idea of above code?

  1. authorizeToken function may be called as many times as it needs (for example 20 times)
  2. Only one asynchronous request (post) may be pushed at a time.
  3. All completions from called authorizeToken functions should be called with the same parameters as the first one completed.

Usage:

let service = LoginService()

service.authorizeToken(withRefreshToken: "") { a, b in print(a)}
service.authorizeToken(withRefreshToken: "") { a, b in print(a)}
service.authorizeToken(withRefreshToken: "") { a, b in print(a)}
service.authorizeToken(withRefreshToken: "") { a, b in print(a)}
service.authorizeToken(withRefreshToken: "") { a, b in print(a)}

All completions above should be printed with result from the first one which was called.

Is it possible to do this with RxSwift?

PS I will award a bounty of 100 once it is possible for the one who help me with this;)

2 Answers

Is it possible to do this with RxSwift?

Yes it is possible. RxSwift and Handling Invalid Tokens.

The simplest solution:

func authorizeToken(withRefreshToken refreshToken: String) -> Observable<SomeResponse> {
    Observable.create { observer in
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            print("async operation")
            observer.onNext(SomeResponse())
        }
        return Disposables.create()
    }
}

let response = authorizeToken(withRefreshToken: "")
    .share(replay: 1)

response.subscribe(onNext: { print($0) })
response.subscribe(onNext: { print($0) })
response.subscribe(onNext: { print($0) })
response.subscribe(onNext: { print($0) })
response.subscribe(onNext: { print($0) })

The above will only work if all requests (subscribes) are made before the first one completes. Just like your code.

If you want to store the response for use even after completion, then you can use replay instead of share.

let response = authorizeToken(withRefreshToken: "")
    .replayAll()

let disposable = response.connect() // this calls the async function. The result will be stored until `disposable.dispose()` is called.

response.subscribe(onNext: { print($0) })

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    response.subscribe(onNext: { print($0) }) // this won't perform the async operation again even if the operation completed some time ago.
}

Answering

is it possible to do this with RxSwift

that's not possible, as every time we trigger the function it gets dispatched and we can't access the callbacks from other threads.

You're creating a race condition, a workaround is to populate the data once in a singleton, and rather than calling the function multiple times use that singleton.

some other approach might also work singleton is just an example.

Race condition: A race condition is what happens when the expected completion order of a sequence of operations becomes unpredictable, causing our program logic to end up in an undefined state

Related