Coverage Summary for Class: LoggingSseSession (dev.skymansandy.wiretap.plugin.sse)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| LoggingSseSession |
75%
(3/4)
|
100%
(2/2)
|
95.7%
(22/23)
|
97.2%
(105/108)
|
| LoggingSseSession$incoming$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(27/27)
|
| LoggingSseSession$incoming$2 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(10/10)
|
| LoggingSseSession$onSessionClosed$1 |
100%
(1/1)
|
50%
(4/8)
|
100%
(17/17)
|
90.2%
(101/112)
|
| Total |
85.7%
(6/7)
|
60%
(6/10)
|
97.6%
(41/42)
|
94.6%
(243/257)
|
/*
* Copyright (c) 2026 skymansandy. All rights reserved.
*/
package dev.skymansandy.wiretap.plugin.sse
import dev.skymansandy.wiretap.domain.model.SseConnection
import dev.skymansandy.wiretap.domain.model.SseEvent
import dev.skymansandy.wiretap.domain.model.SseStatus
import dev.skymansandy.wiretap.domain.orchestrator.SseLogManager
import dev.skymansandy.wiretap.helper.util.currentTimeMillis
import io.ktor.client.plugins.sse.SSESession
import io.ktor.sse.ServerSentEvent
import io.ktor.utils.io.InternalAPI
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
/**
* [SSESession] wrapper that logs all incoming SSE events via Wiretap.
*
* Intercepts the delegate's [incoming] flow with [onEach] / [onCompletion]
* operators so every event and session lifecycle change is recorded.
*/
internal class LoggingSseSession(
private val delegate: SSESession,
private val connectionId: Long,
private val url: String,
private val sseLogManager: SseLogManager,
) : SSESession {
override val coroutineContext: CoroutineContext = delegate.coroutineContext
private val logScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override val incoming: Flow<ServerSentEvent> = delegate.incoming
.onEach { event -> logEvent(event) }
.onCompletion { cause -> onSessionClosed(cause) }
@InternalAPI
override fun bodyBuffer(): ByteArray = delegate.bodyBuffer()
private fun onSessionClosed(cause: Throwable?) {
logScope.launch {
when {
cause != null && cause !is CancellationException -> {
sseLogManager.updateConnection(
SseConnection(
id = connectionId,
url = url,
status = SseStatus.Failed,
failureMessage = cause.message ?: cause::class.simpleName ?: "Unknown error",
closedAt = currentTimeMillis(),
timestamp = currentTimeMillis(),
),
)
}
else -> {
sseLogManager.updateConnection(
SseConnection(
id = connectionId,
url = url,
status = SseStatus.Closed,
closedAt = currentTimeMillis(),
timestamp = currentTimeMillis(),
),
)
}
}
}
}
private suspend fun logEvent(event: ServerSentEvent) {
val data = event.data ?: return
val byteCount = data.encodeToByteArray().size.toLong()
sseLogManager.logEvent(
SseEvent(
connectionId = connectionId,
eventType = event.event,
data = data,
eventId = event.id,
retryMs = event.retry,
byteCount = byteCount,
timestamp = currentTimeMillis(),
),
)
}
}