Изменения вводят live-канал OnlineOutbox и фоновую синхронизацию диалогов.
This commit is contained in:
+65
@@ -0,0 +1,65 @@
|
||||
package pw.binom.agentik.outbox.inmemory
|
||||
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import pw.binom.agentik.outbox.MutableOnlineOutbox
|
||||
import pw.binom.agentik.outbox.OnlineEvent
|
||||
|
||||
/**
|
||||
* In-memory реализация [MutableOnlineOutbox] — единственный live-канал
|
||||
* без хранения.
|
||||
*
|
||||
* **Никакого буфера событий**: [OnlineEvent] нигде не накапливаются
|
||||
* (в этом весь смысл — дельты токенов не должны течь в durable-стор).
|
||||
* Единственное, что живёт в памяти, — [MutableSharedFlow] с BOUNDED
|
||||
* internal buffer'ом для развязки продюсера/подписчиков; при переполнении
|
||||
* **старые дропаются** ([BufferOverflow.DROP_OLDEST]), [appendOnline] не
|
||||
* блокируется. Потеря дельты допустима (см. [OnlineOutbox]).
|
||||
*
|
||||
* **Маршрутизация**: один общий [MutableSharedFlow] c `conversationId`
|
||||
* в envelope; [onlineEvents] фильтрует по диалогу. Отдельный flow-на-диалог
|
||||
* не держим, чтобы не плодить per-conversation подписки, которые надо
|
||||
* чистить вручную.
|
||||
*/
|
||||
class InMemoryOnlineOutbox(
|
||||
liveBufferCapacity: Int = DEFAULT_LIVE_BUFFER_CAPACITY,
|
||||
) : MutableOnlineOutbox {
|
||||
|
||||
private data class Envelope(val conversationId: String, val event: OnlineEvent)
|
||||
|
||||
private val liveFlow = MutableSharedFlow<Envelope>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = liveBufferCapacity,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
init {
|
||||
require(liveBufferCapacity > 0) {
|
||||
"liveBufferCapacity must be > 0, got $liveBufferCapacity"
|
||||
}
|
||||
}
|
||||
|
||||
override fun onlineEvents(): Flow<OnlineEvent> =
|
||||
liveFlow.map { it.event }
|
||||
|
||||
override fun onlineEvents(conversationId: String): Flow<OnlineEvent> =
|
||||
liveFlow.filter { it.conversationId == conversationId }.map { it.event }
|
||||
|
||||
override suspend fun appendOnline(conversationId: String, event: OnlineEvent)=
|
||||
liveFlow.emit(Envelope(conversationId, event))
|
||||
|
||||
override fun tryAppendOnline(conversationId: String, event: OnlineEvent): Boolean =
|
||||
liveFlow.tryEmit(Envelope(conversationId, event))
|
||||
|
||||
override fun close() {
|
||||
// replay = 0 — чистить нечего; сам flow соберётся GC'ом при выходе ссылки.
|
||||
// Идемпотентно: повторный close() безопасен.
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val DEFAULT_LIVE_BUFFER_CAPACITY = 1
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -164,7 +164,7 @@ class InMemoryOutboxStoreTest {
|
||||
store.append(CommonEvent.Conversation(
|
||||
date = now,
|
||||
conversationId = "c-1",
|
||||
event = Event.AppendText(date = now, body = "hi"),
|
||||
event = Event.Working(date = now),
|
||||
))
|
||||
|
||||
// Snapshot-based test of the default impl (uses events() + filterIsInstance).
|
||||
@@ -180,9 +180,9 @@ class InMemoryOutboxStoreTest {
|
||||
fun `conversationEvents with conversationId filters to that conversation`() = runBlocking {
|
||||
val store = InMemoryOutboxStore(maxMessages = null, ttl = null)
|
||||
val now = Instant.fromEpochSeconds(0)
|
||||
store.append(CommonEvent.Conversation(now, "c-1", Event.AppendText(now, "a")))
|
||||
store.append(CommonEvent.Conversation(now, "c-2", Event.AppendText(now, "b")))
|
||||
store.append(CommonEvent.Conversation(now, "c-1", Event.AppendText(now, "c")))
|
||||
store.append(CommonEvent.Conversation(now, "c-1", Event.Working(now)))
|
||||
store.append(CommonEvent.Conversation(now, "c-2", Event.Working(now)))
|
||||
store.append(CommonEvent.Conversation(now, "c-1", Event.Working(now)))
|
||||
|
||||
// Test the filter logic by manually filtering snapshot.
|
||||
val c1 = store.snapshot()
|
||||
@@ -200,7 +200,7 @@ class InMemoryOutboxStoreTest {
|
||||
date = now,
|
||||
event = AgentEvent.Created(date = now, conversationId = "created"),
|
||||
))
|
||||
store.append(CommonEvent.Conversation(now, "c-1", Event.AppendText(now, "hi")))
|
||||
store.append(CommonEvent.Conversation(now, "c-1", Event.Working(now)))
|
||||
|
||||
val all = store.snapshot()
|
||||
val agents = all.filterIsInstance<CommonEvent.Agent>()
|
||||
|
||||
Reference in New Issue
Block a user