Basic SSE infrastructure
This commit is contained in:
@@ -5,18 +5,29 @@ import io.ktor.client.*
|
||||
import io.ktor.client.plugins.auth.*
|
||||
import io.ktor.client.plugins.auth.providers.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.plugins.sse.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.atomicfu.locks.ReentrantLock
|
||||
import kotlinx.atomicfu.locks.withLock
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class Client private constructor(private val _auth: AuthProvider) {
|
||||
private val _authClient = platformClient {
|
||||
private val _authClient = platformClient {
|
||||
install(ContentNegotiation) { json() }
|
||||
}
|
||||
private val _client = platformClient {
|
||||
var tryingRefresh = false
|
||||
install(ContentNegotiation) { json() }
|
||||
|
||||
install(SSE) {
|
||||
maxReconnectionAttempts = 10
|
||||
reconnectionTime = 1.seconds
|
||||
bufferPolicy = SSEBufferPolicy.LastEvents(5)
|
||||
}
|
||||
|
||||
install(Auth) {
|
||||
bearer {
|
||||
cacheTokens = false
|
||||
@@ -28,17 +39,16 @@ class Client private constructor(private val _auth: AuthProvider) {
|
||||
}
|
||||
|
||||
refreshTokens {
|
||||
if(tryingRefresh) {
|
||||
if (tryingRefresh) {
|
||||
_auth.onLogout()
|
||||
null
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
tryingRefresh = true
|
||||
val ref = _auth.refresh.value ?: return@refreshTokens null
|
||||
println("Trying to re-authenticate using $ref")
|
||||
|
||||
val res = callRoute(_authClient, Routes.Auth.refresh, RefreshRequest(ref), true).foldSuspend({
|
||||
if(it.msg.startsWith(COROUTINE_CANCELLED)) println("Coro calling refresh was cancelled")
|
||||
if (it.msg.startsWith(COROUTINE_CANCELLED)) println("Coro calling refresh was cancelled")
|
||||
_auth.onLogout()
|
||||
null
|
||||
}) {
|
||||
@@ -57,27 +67,44 @@ class Client private constructor(private val _auth: AuthProvider) {
|
||||
|
||||
private var _counter = 0
|
||||
|
||||
private suspend fun <TReq: Any, TRes: Any> callRoute(using: HttpClient, route: ApiRoute<TReq, TRes>, body: TReq, wasInternal: Boolean = false): Either<ErrorResponse, TRes> {
|
||||
private suspend fun <TReq : Any, TRes : Any> callRoute(
|
||||
using: HttpClient,
|
||||
route: ApiRoute<TReq, TRes>,
|
||||
body: TReq,
|
||||
wasInternal: Boolean = false
|
||||
): Either<ErrorResponse, TRes> {
|
||||
val ctr = _counter++
|
||||
return try {
|
||||
println("Calling route ${route.pattern} with client $using, request ID=$ctr (internal request: $wasInternal)")
|
||||
val client = IClient.Default(using, _auth.server.value ?: throw IllegalStateException("No server URL set."))
|
||||
val res = route.call(client, body, ctr)
|
||||
res
|
||||
}
|
||||
catch(e: CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
println("Call to ${route.pattern} [$ctr] was cancelled")
|
||||
ErrorResponse(COROUTINE_CANCELLED).error()
|
||||
}
|
||||
catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
println("Call to ${route.pattern} [$ctr] ran into an exception")
|
||||
ErrorResponse(e.message ?: "Unknown error.").error()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <TReq: Any, TRes: Any> callRoute(route: ApiRoute<TReq, TRes>, body: TReq): Either<ErrorResponse, TRes> =
|
||||
suspend fun <TReq : Any, TRes : Any> callRoute(
|
||||
route: ApiRoute<TReq, TRes>,
|
||||
body: TReq
|
||||
): Either<ErrorResponse, TRes> =
|
||||
callRoute(_client, route, body)
|
||||
|
||||
suspend fun <T> connectSSE(route: String, serializer: KSerializer<T>, onEvent: suspend (T) -> Unit) {
|
||||
println("Client tries to set up SSE to $route")
|
||||
val server = _auth.server.value ?: throw IllegalStateException("No server URL set.")
|
||||
_client.sse(urlString = "$server$route", showCommentEvents = true, showRetryEvents = true) {
|
||||
incoming.collect {
|
||||
println("RECEIVE: $it")
|
||||
if(it.data != null) onEvent(Json.decodeFromString(serializer, it.data!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val _sync = ReentrantLock()
|
||||
private var _instance: Client? = null
|
||||
@@ -85,11 +112,10 @@ class Client private constructor(private val _auth: AuthProvider) {
|
||||
|
||||
fun construct(auth: AuthProvider): Client {
|
||||
_sync.withLock {
|
||||
if(_instance == null) {
|
||||
if (_instance == null) {
|
||||
_instance = Client(auth)
|
||||
return _instance!!
|
||||
}
|
||||
else throw IllegalStateException("Client already constructed")
|
||||
} else throw IllegalStateException("Client already constructed")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import com.jaytux.phoebench.clients.withScope
|
||||
import com.jaytux.phoebench.common.HomeResponse
|
||||
import com.jaytux.phoebench.common.InviteListResponse
|
||||
import com.jaytux.phoebench.common.UserListResponse
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
class HomeVM(
|
||||
@@ -42,6 +44,8 @@ class HomeVM(
|
||||
val invites = _invites.immutable()
|
||||
val users = _users.immutable()
|
||||
|
||||
private var _listenJob: Job? = null
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
@@ -52,6 +56,7 @@ class HomeVM(
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_listenJob?.cancel()
|
||||
resetAdmin()
|
||||
_username.value = null
|
||||
_isAdmin.value = false
|
||||
@@ -67,6 +72,17 @@ class HomeVM(
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
_listenJob = withScope {
|
||||
_client.connectSSE("/rt/project", serializer<HomeResponse.ProjectSummary>()) {
|
||||
if(it.owner.name == _username.value) {
|
||||
if(_ownProjects.value.none { p -> p.id == it.id }) _ownProjects.value += it
|
||||
}
|
||||
if(it.isPublic) {
|
||||
if(_publicProjects.value.none { p -> p.id == it.id }) _publicProjects.value += it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withScope {
|
||||
resetAdmin()
|
||||
_repo.getHome().snackOr {
|
||||
|
||||
Reference in New Issue
Block a user