Skip to content

Instantly share code, notes, and snippets.

@remcomokveld
Created October 14, 2024 07:42
Show Gist options
  • Save remcomokveld/9ab0ee6b5c452dfca1cf56744f9fb65a to your computer and use it in GitHub Desktop.
Save remcomokveld/9ab0ee6b5c452dfca1cf56744f9fb65a to your computer and use it in GitHub Desktop.
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