refactor(event-store): earliestEventDate returns non-nullable Instant
ci / JVM build + tests (push) Failing after 1m50s

Раньше возвращал 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'ы не используют этот метод.
This commit is contained in:
2026-09-20 15:41:30 +03:00
parent 7358175499
commit 8bb24dab3c
@@ -77,22 +77,33 @@ interface EventStore : AutoCloseable {
fun events(after: Instant?): Flow<Event>
/**
* 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()
}