Damian Dudycz
Damian Dudycz

Reputation: 2810

Swift Combine - perform tasks in given sequence

Im trying to create a networking layer, using Combine framework. I have a few functions already implemented, which are working fine. Some of these api calls are requiring an access token, and there are calls to obtain and refresh this token. Now the problem Im facing is that I would like the functions that require this token, to automatically try to refresh it before executing - only if it needs refreshing, or if I get an error. Right now I have functions like this:

static func login(with parameters: CredentialsRequest) -> AnyPublisher<LoggedUser, RequestError> 
static func refreshToken(_ token: Token) -> AnyPublisher<Token, RequestError>
static func activeGames(token: Token) -> AnyPublisher<[Game], RequestError>

Token has a property: requiresRefresh: Bool

How could I make the function activeGames still return the type AnyPublisher<[Game], RequestError>, but work in such a way, that if first checks if token needs to be refreshed and if so, it waits before it's refreshed?

Upvotes: 1

Views: 1162

Answers (1)

Cristik
Cristik

Reputation: 32928

A little off-topic, but I'd recommend using Future instead of AnyPublisher, at least for better semantics, as futures deliver a single value, just like a networking call does.


One approach would be to lift any function that receives a token as argument:

func authenticated<P: Publisher, T>(_ source: @escaping (Token) -> P) -> (Token) -> AnyPublisher<T, RequestError> where P.Output == T, P.Failure == RequestError {
    return { token in
        let first: AnyPublisher<Token, RequestError>
        if token.requiresRefresh {
            // token needs refresh, let's create the refresh operation
            first = refreshToken(token)
        } else {
            // token doesn't need refresh, just forward it downstream
            first = Result<Token, RequestError>.Publisher(token).eraseToAnyPublisher()
        }
        // now, the Publisher chain
        return first
            .flatMap(source)
            .catch { (error) -> AnyPublisher<T, RequestError> in
                // assuming this error corresponds to the invalid/expired token
                if case .invalidToken = error {
                    // and in this case we redo the authentication and the actual request
                    return refreshToken(token).flatMap(source).eraseToAnyPublisher()
                } else {
                    // report the error otherwise
                    return Fail<T, RequestError>(error: error).eraseToAnyPublisher()
                }
            }.eraseToAnyPublisher()
    }
}

You would then use it like this authenticated(activeGames)(token), so basically calling the result of the lifted function instead of directly calling the function.

Functions that have more arguments than the token, can be also lifted via some overloads of the above function:

// for token + another argument
func authenticated<P: Publisher, T, A1>(_ source: @escaping (Token, A1) -> P) -> (Token, A1) -> AnyPublisher<T, RequestError> where P.Output == T, P.Failure == RequestError {
    return { token, arg1 in
        let src: (A1) -> (Token) -> P = { arg1 in { token in source(token, arg1) } }
        return authenticated(src(arg1))(token)
    }
}

// for token + two other arguments
func authenticated<P: Publisher, T, A1, A2>(_ source: @escaping (Token, A1, A2) -> P) -> (Token, A1, A2) -> AnyPublisher<T, RequestError> where P.Output == T, P.Failure == RequestError {
    return { token, arg1, arg2 in
        let src: (A1, A2) -> (Token) -> P = { arg1, arg2 in { token in source(token, arg1, arg2) } }
        return authenticated(src(arg1, arg2))(token)
    }
}

A somehow simpler solution would involve operating on the lowest level function, for example the one just above the URLSession stack, but formulating this solution requires knowledge about your networking stack implementation.

Upvotes: 2

Related Questions