Created
October 14, 2024 07:42
-
-
Save remcomokveld/9ab0ee6b5c452dfca1cf56744f9fb65a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.sxmp.clientsdk.network | |
import com.sxmp.clientsdk.common.ClientSdkInternalApi | |
import com.sxmp.coroutines.dispatchers | |
import com.sxmp.network.EndpointRequest | |
import com.sxmp.network.NetworkClient | |
import kotlinx.coroutines.withContext | |
import okhttp3.RequestBody | |
import okhttp3.Response | |
@ClientSdkInternalApi | |
internal class OperationExecutor( | |
private val networkClient: NetworkClient, | |
private val tokenDelegate: AccessTokenOwner, | |
) { | |
/** | |
* Execute the given [operation] potentially waiting for an access token if it is needed and refreshing the access | |
* token if the access-token-based operation returns a 401. | |
* | |
* All operations that are needed to create the initial access token and token refreshes return false for | |
* [Operation.isAccessTokenNeeded] and will therefore just be executed immediately. | |
*/ | |
suspend fun execute(operation: Operation): Response { | |
val result = if (!operation.isAccessTokenNeeded) { | |
operation.execute() | |
} else { | |
val firstAccessToken = tokenDelegate.awaitAccessToken() | |
val firstAttempt = operation.withAccessToken(firstAccessToken).execute() | |
if (firstAttempt.code == 401) { | |
val newAccessToken = tokenDelegate.refreshToken(firstAccessToken) | |
operation.withAccessToken(newAccessToken).execute() | |
} else { | |
firstAttempt | |
} | |
} | |
val convergenceToken = result.header("x-sxm-convergence-token") | |
if (convergenceToken != null) tokenDelegate.refreshWithConvergenceToken(convergenceToken) | |
return result | |
} | |
private suspend fun Operation.execute(): Response = | |
withContext(dispatchers().io) { networkClient.newCall(endpoint, body, variables).execute() } | |
} | |
internal data class Operation( | |
val endpoint: EndpointRequest, | |
val body: RequestBody? = null, | |
val variables: Map<String, String>, | |
) { | |
val isAccessTokenNeeded = endpoint.endpoint.headers.any { it.value.contains("{accessToken}") } | |
fun withAccessToken(token: String) = copy(variables = variables.plus("{accessToken}" to token)) | |
} | |
/** | |
* This interface is to be implemented by the user domain where the actual tokens are owned. | |
*/ | |
@ClientSdkInternalApi | |
public interface AccessTokenOwner { | |
/** | |
* Returns an access token when one is available. | |
* | |
* If an access token is readily available the implementation of this function should return it without suspending. | |
* | |
* If the session is still being created, the implementation should suspend and return once that session is created | |
* and an access token is available. | |
*/ | |
public suspend fun awaitAccessToken(): String | |
/** | |
* Do a token refresh with refreshes the given [oldToken] and return the a refreshed access token. | |
*/ | |
public suspend fun refreshToken(oldToken: String): String | |
/** | |
* This method gets invoked by [OperationExecutor] if a response contains a convergence token. | |
* | |
* The implementation is responsible for triggering a token refresh with that [convergenceToken]. | |
*/ | |
public suspend fun refreshWithConvergenceToken(convergenceToken: String) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment