refactor(event-store): split into EventStore (read-only) + MutableEventStore
ci / JVM build + tests (push) Failing after 1m49s

Разделяет интерфейс на read-only (EventStore) и write (MutableEventStore).

EventStore (read-only, для consumer'ов):
  - events(after: Instant?): Flow<CommonEvent>
  - earliestEventDate(): Instant
  - close()

MutableEventStore : EventStore (для producer'ов):
  - + append(event: CommonEvent)
  - suspend, не идемпотентный, может быть silently evicted

Зачем:
  - Consumer'ы (server SSE, admin dashboard, parent agents) принимают
    EventStore — compile-time гарантия что они не могут писать в store.
  - Producer'ы (ChatAgent, sub-agents, A2A-bridge) принимают MutableEventStore.
  - Тесты могут использовать EventStore без опасности случайной модификации.

Миграция:
  - :event-store пока без implementations, поэтому ничего не сломалось.
  - Когда добавим InMemoryEventStore — он будет реализовывать оба
    (MutableEventStore = EventStore + append). Подписки получают только
    read-only projection через приведение типа.

Также: импорт обновлён AllEvent → CommonEvent (по rename в :proto).
This commit is contained in:
2026-09-20 15:46:59 +03:00
parent 1f85cde1b8
commit e2f0e434d1
7 changed files with 70 additions and 39 deletions
@@ -2,7 +2,7 @@ package pw.binom.agentik.eventStore
import kotlinx.coroutines.flow.Flow
import kotlin.time.Instant
import pw.binom.agentik.proto.AllEvent
import pw.binom.agentik.proto.CommonEvent
/**
* Bounded-tail event log с автоматическим управлением TTL.
@@ -30,11 +30,8 @@ import pw.binom.agentik.proto.AllEvent
* - Позволяет impl выбирать retention strategy (TTL, size cap, sliding window).
* - Сохраняет контракт clean: интерфейс только о put/get.
*
* **Append is NOT idempotent**: [AllEvent] не имеет уникального id,
* поэтому retry с тем же logical event приведёт к дубликату в tail'е.
* Для at-least-once → exactly-once нужна дедупликация на стороне
* consumer'а (catchup через `:message-store-api` audit log имеет
* монотонный `id` и работает как dedup anchor).
* **Read-only**: этот интерфейс предоставляет только read-операции.
* Для записи см. [MutableEventStore].
*
* **Подписки нереентрантные**: каждый вызов [events] создаёт **новую
* подписку** (cold Flow). Один [events] НЕ видит события, добавленные до
@@ -46,18 +43,6 @@ import pw.binom.agentik.proto.AllEvent
*/
interface EventStore : AutoCloseable {
/**
* Положить event в log.
*
* - **Идемпотентно по [AllEvent.date]**: повторный append с тем же id — no-op.
* - **Silently evicted**: implementation может выкинуть этот event сразу
* после append (TTL/cap) без уведомления producer'а. Producer **не
* должен** полагаться на то, что event дойдёт до клиента, если он
* вне retention window.
* - **Suspend**: для KMP I/O impl'ов (SQLite через JNI).
*/
suspend fun append(event: AllEvent)
/**
* Subscribe на events.
*
@@ -78,7 +63,7 @@ interface EventStore : AutoCloseable {
* полноты клиент обязан cross-check с [earliestEventDate] и fallback
* в message store при gap'е (см. KDoc интерфейса).
*/
fun events(after: Instant?): Flow<AllEvent>
fun events(after: Instant?): Flow<CommonEvent>
/**
* Date **стартовой точки** буфера.
@@ -0,0 +1,52 @@
package pw.binom.agentik.eventStore
import pw.binom.agentik.proto.CommonEvent
/**
* Mutable вариант [EventStore] — добавляет producer-операцию [append].
*
* Этот интерфейс предназначен **только для producer'ов** (ChatAgent,
* sub-agents, A2A-bridge). Consumer'ы (server SSE endpoints, admin
* dashboards, parent agents) должны принимать **read-only** [EventStore]
* — тогда невозможно случайно писать в store из observer'а.
*
* Типичное использование:
* ```
* // Producer
* class ChatAgent(private val events: MutableEventStore) {
* suspend fun doSomething() {
* events.append(CommonEvent.Agent(date = now, event = AgentEvent.Created(...)))
* }
* }
*
* // Consumer
* class EventStreamEndpoint(private val events: EventStore) {
* fun stream() = events.events(after = null)
* // Ошибка компиляции если раскомментировать:
* // events.append(...) // ← нельзя, MutableEventStore нет в типе
* }
* ```
*
* **Append НЕ идемпотентен**: [CommonEvent] не имеет уникального id,
* поэтому retry с тем же logical event (например, после network failure
* между producer и store) приведёт к дубликату в tail'е. Это OK для
* use case'a bounded-tail — клиент, делающий catchup через [events](after),
* получит свой диапазон ровно один раз при подключении, а последующие
* retry producer'а просто насытят tail повторами, не задевая уже
* обработанные. Для гарантированной exactly-once — dedup через
* [message-store] (там есть монотонный `id`).
*
* **Silently evicted**: implementation может выкинуть этот event сразу
* после append (TTL/cap) без уведомления producer'а. Producer **не
* должен** полагаться на то, что event дойдёт до клиента, если он
* вне retention window.
*/
interface MutableEventStore : EventStore {
/**
* Положить event в log.
*
* - **Не идемпотентно** — см. KDoc интерфейса.
* - **Suspend** для KMP I/O impl'ов (SQLite через JNI).
*/
suspend fun append(event: CommonEvent)
}