CLI client, SSE
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Serializable
|
||||
sealed class HomeEvent {
|
||||
@Serializable
|
||||
data class Created(val summary: HomeResponse.ProjectSummary) : HomeEvent()
|
||||
|
||||
@Serializable
|
||||
data class Changed(val summary: HomeResponse.ProjectSummary) : HomeEvent()
|
||||
|
||||
@Serializable
|
||||
data class Deleted(val id: Uuid) : HomeEvent()
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class AdminEvent {
|
||||
@Serializable
|
||||
data class NewUser(val user: UserListResponse.UserData) : AdminEvent()
|
||||
|
||||
@Serializable
|
||||
data class UserChanged(val user: UserListResponse.UserData) : AdminEvent()
|
||||
|
||||
@Serializable
|
||||
data class UserDeleted(val id: Uuid) : AdminEvent()
|
||||
|
||||
@Serializable
|
||||
data class NewInvite(val invite: InviteListResponse.Invite) : AdminEvent()
|
||||
|
||||
@Serializable
|
||||
data class InviteDeleted(val id: Uuid) : AdminEvent()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ProjectEvent {
|
||||
@Serializable
|
||||
object Deleted : ProjectEvent()
|
||||
|
||||
@Serializable
|
||||
data class Updated(val changes: HomeResponse.ProjectSummary) : ProjectEvent()
|
||||
|
||||
@Serializable
|
||||
data class NewLabel(val label: LabelResponse) : ProjectEvent()
|
||||
|
||||
@Serializable
|
||||
data class LabelChanged(val label: LabelResponse) : ProjectEvent()
|
||||
|
||||
@Serializable
|
||||
data class LabelDeleted(val id: Uuid) : ProjectEvent()
|
||||
|
||||
@Serializable
|
||||
data class NewEntry(val entry: EntryResponse) : ProjectEvent()
|
||||
|
||||
@Serializable
|
||||
data class EntryDeleted(val id: Uuid) : ProjectEvent()
|
||||
}
|
||||
@@ -38,7 +38,12 @@ object Routes {
|
||||
|
||||
object Entry {
|
||||
val new = ApiRoute.post<EntryRequest, EntryResponse>("/entry", Elevation.AUTH)
|
||||
val update = ApiRoute.patchUuidNoRes<PartialEntryRequest>("/entry", Elevation.AUTH)
|
||||
val delete = ApiRoute.deleteUuidNoRes("/entry", Elevation.AUTH)
|
||||
}
|
||||
|
||||
object SSE {
|
||||
val home = SSERoute.noArgs<HomeEvent>("/rt/home", Elevation.AUTH)
|
||||
val admin = SSERoute.noArgs<AdminEvent>("/rt/admin", Elevation.ADMIN)
|
||||
val projectSpecific = SSERoute.uuid<ProjectEvent>("/rt/project", Elevation.AUTH)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.ResponseException
|
||||
import io.ktor.client.plugins.sse.SSEClientException
|
||||
import io.ktor.client.plugins.sse.sse
|
||||
import io.ktor.client.plugins.sse.sseSession
|
||||
import io.ktor.http.Parameters
|
||||
import io.ktor.http.buildUrl
|
||||
import io.ktor.util.reflect.TypeInfo
|
||||
import io.ktor.util.reflect.typeInfo
|
||||
import io.ktor.utils.io.CancellationException
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
sealed class SSERoute<TParams, TEvent: Any>(val path: String, val elevation: Elevation, private val _resType: TypeInfo) {
|
||||
open val pattern = path
|
||||
private val deserializer = Json.serializersModule.serializer(_resType.kotlinType!!)
|
||||
|
||||
protected open fun buildUrl(params: TParams): String = path
|
||||
|
||||
abstract fun extractParams(reqParams: Parameters): TParams?
|
||||
|
||||
suspend fun call(client: IClient, params: TParams, handler: suspend (Either<ErrorResponse, TEvent>) -> Unit): Either<ErrorResponse, Unit> {
|
||||
val fullUrl = "${client.serverUrl}${buildUrl(params)}"
|
||||
try {
|
||||
client.client.sse(urlString = fullUrl, showCommentEvents = true, showRetryEvents = true) {
|
||||
incoming.collect {
|
||||
try {
|
||||
val data = it.data
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (data != null) {
|
||||
println("SSE [$path] with data $data")
|
||||
handler((Json.decodeFromString(deserializer, data) as TEvent).value())
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
withContext(NonCancellable) {
|
||||
handler(ErrorResponse("The stream to $fullUrl was disconnected.").error())
|
||||
}
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
handler(ErrorResponse(e.message ?: "Unknown error while streaming $fullUrl").error())
|
||||
}
|
||||
}
|
||||
}
|
||||
return Unit.value()
|
||||
}
|
||||
catch(e: ResponseException) {
|
||||
val error = e.response.body<ErrorResponse>()
|
||||
return error.error()
|
||||
}
|
||||
catch(e: SSEClientException) {
|
||||
return ErrorResponse(e.message ?: "Could not set up event stream for $fullUrl.").error()
|
||||
}
|
||||
catch(e: CancellationException) {
|
||||
throw e
|
||||
}
|
||||
catch(e: Exception) {
|
||||
return ErrorResponse(e.message ?: "SSE connection failed.").error()
|
||||
}
|
||||
}
|
||||
|
||||
class SSERoute0<TEvent: Any>(path: String, elevation: Elevation, resType: TypeInfo) : SSERoute<Unit, TEvent>(path, elevation, resType) {
|
||||
override fun extractParams(reqParams: Parameters) {}
|
||||
}
|
||||
|
||||
class SSERoute1<T1, TEvent: Any>(path: String, elevation: Elevation, resType: TypeInfo, val urlEncode: (T1) -> String, val urlDecode: (String?) -> T1?)
|
||||
: SSERoute<T1, TEvent>(path, elevation, resType)
|
||||
{
|
||||
override val pattern: String = "$path/{param}"
|
||||
|
||||
override fun buildUrl(params: T1): String = "$path/${urlEncode(params)}"
|
||||
|
||||
override fun extractParams(reqParams: Parameters): T1? = urlDecode(reqParams["param"])
|
||||
}
|
||||
|
||||
companion object {
|
||||
inline fun <reified TEvent: Any> noArgs(path: String, elevation: Elevation) =
|
||||
SSERoute0<TEvent>(path, elevation, typeInfo<TEvent>())
|
||||
|
||||
inline fun <T, reified TEvent: Any> single(path: String, elevation: Elevation,
|
||||
noinline urlEncode: (T) -> String = { it.toString() }, noinline urlDecode: (String?) -> T?
|
||||
) = SSERoute1<T, TEvent>(path, elevation, typeInfo<TEvent>(), urlEncode, urlDecode)
|
||||
|
||||
inline fun <reified TEvent: Any> uuid(path: String, elevation: Elevation) = single<Uuid, TEvent>(path, elevation) {
|
||||
it?.let { p -> Uuid.parseOrNull(p) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user