= infra-persistence

== purpose

Provides concrete `EventStore` implementations for persisting and replaying the event log. Supports in-memory storage (for testing and ephemeral sessions) and SQLite-backed durable storage. Also provides a live artifact repository that subscribes to events and maintains an up-to-date in-memory view of artifact state per session.

== responsibilities

* Implement the `EventStore` interface for both transient and durable storage
* Enforce duplicate event ID detection and sequence ordering
* Provide reactive subscriptions via `Flow<StoredEvent>` per session and globally
* Maintain an artifact state projection that stays current via event subscription
* Support transactional batch appends for atomic event persistence

== non-responsibilities

* Does not define event serialization — uses `JsonEventSerializer` from `core:events`
* Does not implement CAS content-addressable storage — that belongs to `infra:artifacts-cas`
* Does not handle event compaction or retention
* Does not implement cross-session querying beyond `allSessionIds()`

== key types

=== InMemoryEventStore
* **kind**: class
* **purpose**: Thread-safe, in-memory event store backed by `ConcurrentHashMap`. Supports duplicate detection via `seenEventIds` set and session-scoped sequence numbers.
* **fields**: `streams` (per-session event lists), `sequences` (per-session sequence counters), `globalSeq` (global counter), `seenEventIds` (dedup set)

=== SqliteEventStore
* **kind**: class
* **purpose**: Durable event store backed by a SQLite database with WAL mode. Uses JDBC directly. Emits events to reactive flows after append.
* **fields**: `connection` (JDBC SQLite connection), `jsonSerializer` (serialization bridge), `artifactStore` (used for `flushBefore` coordination)

=== LiveArtifactRepository
* **kind**: class
* **purpose**: Subscribes to an `EventStore` per session and maintains an up-to-date in-memory `ConcurrentHashMap` of `ArtifactState` values. Supports lookup by session, stage, and artifact ID.
* **fields**: `artifactCache` (session → artifactId → state), `stageIndex` (session → artifactId → stageId), `subscriptions` (session → coroutine Job), `projector` (reduces events to state)

=== JDBCHelper
* **kind**: object
* **purpose**: Utility providing `Connection.transaction()` inline helper with automatic commit/rollback.

== event flow

*Inbound:*
* `ArtifactCreatedEvent`, `ArtifactValidatingEvent`, `ArtifactValidatedEvent`, `ArtifactRejectedEvent`, `ArtifactSupersededEvent`, `ArtifactArchivedEvent`, `ArtifactRelationshipAddedEvent` — consumed by `LiveArtifactRepository` to maintain artifact projections
* `NewEvent` — appended to event stores via `append()` / `appendAll()`

*Outbound:*
* `StoredEvent` — emitted via `Flow<StoredEvent>` subscriptions after each append

== integration points

* `core:events` — `EventStore`, `StoredEvent`, `NewEvent`, all artifact event payloads, `EventId`, `SessionId`
* `core:artifacts` — `ArtifactProjector`, `ArtifactReducer`, `ArtifactState`, `ArtifactRepository`
* `core:artifactstore` — `ArtifactStore` (used by `SqliteEventStore` for flush coordination)

== invariants

* Duplicate `eventId` values are rejected with an error
* Session sequence numbers are strictly monotonic — violations throw `error()`
* `InMemoryEventStore.append()` is synchronized per session for thread safety
* `SqliteEventStore` wraps appends in a JDBC transaction and coordinates with `ArtifactStore.flushBefore()`
* `LiveArtifactRepository` lazily subscribes on first query and never unsubscribes

== PlantUML diagram

[plantuml, infra-persistence, "png"]
----
include::../../diagrams/infra-persistence.puml[]
----




== known issues

* `SqliteEventStore` holds an open JDBC `Connection` for the lifetime of the application
* No connection pooling or retry logic for SQLite busy conditions

== open questions

* Should event stores support snapshot-based recovery for long event streams?
* Should there be a read-replica event store for projections to avoid write-contention?
