From 8bb24dab3c83605a2448dc38c98321f328bd7cdf Mon Sep 17 00:00:00 2001 From: subochev Date: Sun, 20 Sep 2026 15:41:30 +0300 Subject: [PATCH] refactor(event-store): earliestEventDate returns non-nullable Instant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше возвращал Instant? — null для пустого буфера. Клиенту приходилось проверять if (earliest != null && ...) — легко ошибиться. Теперь всегда Instant. Если буфер пуст, возвращает Clock.System.now() на момент вызова. Это убирает nullable + сохраняет семантически корректное поведение: - Клиент может безопасно сделать store.events(earliest) → получит только live event'ы, без ложного catchup. - Если бы возвращали null/DISTANT_PAST/null-check, клиент мог бы ошибочно подписаться на несуществующий catchup-диапазон. Edge case (в KDoc): клиент, подключившийся ДО первого event'а, получает earliest ≈ now. Его lastSeen < earliest → адаптируется в первом poll'е. Изменение breaking — но :event-store ещё не имеет implementations и никакие consumer'ы не используют этот метод. --- .../pw/binom/agentik/eventStore/EventStore.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/event-store/src/commonMain/kotlin/pw/binom/agentik/eventStore/EventStore.kt b/event-store/src/commonMain/kotlin/pw/binom/agentik/eventStore/EventStore.kt index 08948fc..cf55128 100644 --- a/event-store/src/commonMain/kotlin/pw/binom/agentik/eventStore/EventStore.kt +++ b/event-store/src/commonMain/kotlin/pw/binom/agentik/eventStore/EventStore.kt @@ -77,22 +77,33 @@ interface EventStore : AutoCloseable { fun events(after: Instant?): Flow /** - * Date самого старого event'а, всё ещё хранящегося в буфере. + * Date **стартовой точки** буфера. * - * `null` если буфер пуст (или store только что стартовал — ни одного - * события ещё не было). + * - Если буфер не пуст → `date` самого старого буферизованного event'а. + * - Если буфер пуст → текущее время (`Clock.System.now()` на момент вызова). + * + * **Семантика "now если пусто"** важна: позволяет клиенту безопасно + * подписаться на [events](after = earliest) сразу — он получит только + * новые live event'ы, без ложного catchup. Если бы возвращалось + * `Instant.DISTANT_PAST` или `null` (с проверкой), клиент мог бы + * ошибочно подписаться на несуществующий catchup и зависнуть в ожидании. * * **Используется клиентом для gap detection**: * - `lastSeen < earliest` → есть дыра в покрытии, нужен fallback * в message store за диапазоном `[lastSeen, earliest)`. * - `lastSeen >= earliest` → всё доступно через [events](after), * fallback не нужен. - * - `earliest == null` → store пуст, первый live event сам станет - * `earliest` для следующего клиента. + * - `lastSeen == earliest` → OK, первый live event будет > earliest. * - * Suspend потому что в persistent impl'ах требует SQL query (`MIN(date)`). + * **Edge case**: клиент, подключившийся до того как store увидел хоть + * один event, получает `earliest ≈ now`. Его `lastSeen` будет < earliest + * — адаптируется в первом же poll'е и пойдёт через fallback если + * сообщения audit log существуют (для consistency с прошлым). + * + * Suspend потому что в persistent impl'ах требует SQL query (`MIN(date)` + * или `Clock.now()` для пустого буфера). */ - suspend fun earliestEventDate(): Instant? + suspend fun earliestEventDate(): Instant override fun close() }