Alpha Version: You are viewing the ALPHA documentation. This is an experimental version and may contain breaking changes.
Skip to main content

EventLog — Local

The local EventLog is the dev/test counterpart of the EventLog → DynamoDB adapter: append-only, per-aggregate event storage. It ships two backends that implement the same EventLog_Adapter interface:

BackendSourceSelected by
In-memoryEventLog/EventLogStorage_InMemory.resBackend.Memory (default)
SQLiteEventLog/EventLogStorage_Sqlite.resBackend.Sqlite / REVENTLESS_LOCAL_BACKEND=sqlite

In-memory is ephemeral; SQLite persists across restarts. Both expose identical append / replay / replayStream / appendStream operations and the same per-aggregate optimistic-concurrency semantics — only the storage substrate differs. Each backend's Make(Bus, …) functor registers its replay function on the bus so other components (GraphQL resolvers, MCP resources) can look up event history by aggregate name.

Operations

OperationDescription
appendAppends JSON events for an aggregate ID at a given seqNr
replayReturns all events for an aggregate ID as an array, ordered by sequence
replayStreamReturns events as a Stream for API uniformity
appendStreamAppends events from a stream sequentially

In-Memory Backend

Source: EventLogStorage_InMemory.res

Data Structure

Events are stored in a Dict<string, array<JSON.t>> managed by an STM TRef. The key is the aggregate ID; the value is an ordered array of event JSON objects.

Concurrency

append runs inside an STM transaction. In the single-threaded local process this gives serialised, conflict-free writes; the seqNr is used to order events within the aggregate.

SQLite Backend

Source: EventLogStorage_Sqlite.res

Schema

All EventLogs share one table, partitioned by log_name:

CREATE TABLE event_log (
log_name TEXT NOT NULL,
aggregate_id TEXT NOT NULL,
seq_nr INTEGER NOT NULL,
payload TEXT NOT NULL, -- JSON.stringify of one event
PRIMARY KEY (log_name, aggregate_id, seq_nr)
);

Replay

SELECT payload … WHERE log_name = ? AND aggregate_id = ? ORDER BY seq_nr ASC — the primary key already orders events within an aggregate. replayStream materialises the array via all() and wraps it as a Stream (node:sqlite's iterate() could back a lazy stream, but the current Stream API only exposes fromIterable).

Append & Optimistic Concurrency

append runs inside a transaction:

  1. It counts the aggregate's existing events and requires seqNr == currentCount — the caller's seqNr must be "the next free slot."
  2. Each event is inserted at seq_nr = seqNr + i.
  3. If a concurrent writer already inserted at the same (log_name, aggregate_id, seq_nr), the primary-key conflict surfaces as Error("conflict").

So the PK doubles as the optimistic-concurrency guard — the SQLite analogue of the DynamoDB EventLog's conditional write on sequenceNr.

Key Differences from AWS

AspectLocal (in-memory)Local (SQLite)AWS (DynamoDB)
StorageDict via STM TRefShared event_log table, partitioned by log_nameOne table per log, id + sequenceNr keys
ConcurrencySTM transactionseqNr check + PK conflict → conflictConditional write on sequenceNr
PersistenceNone (ephemeral)Durable (across restarts)Durable
StreamingArray wrapped as Streamall() wrapped as StreamPaginated query