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

DcbEventLog — Local

The local DcbEventLog is the dev/test counterpart of the AWS DcbEventLog → DynamoDB adapter: the shared, tag-routed event store that backs a plugin's DCB slices. It ships two backends that implement the same DcbEventLog_Adapter interface:

BackendSourceSelected by
In-memoryDcbEventLogStorage_InMemory.resBackend.Memory (default)
SQLiteDcbEventLogStorage_Sqlite.resBackend.Sqlite / REVENTLESS_LOCAL_BACKEND=sqlite

In-memory is ephemeral (state lives for the lifetime of the process) and is the default for tests; SQLite persists across restarts and is selected for local runs that need durability. Both expose identical read / append / readStream operations and the same tag-query and conflict semantics — only the storage substrate differs.

The Make(Bus) functor lives only on the in-memory module; dispatch is centralised there so callers wire a single module regardless of backend.

Operations​

OperationDescription
readReturns events matching a tag-based query, optionally filtering by position (~after)
appendAppends events with optimistic-concurrency conflict detection via appendCondition
readStreamStream variant of read

Tag-Based Query Matching​

Both backends apply the same query semantics as the DynamoDB adapter:

  • An empty query matches all events.
  • Each query item can filter by eventTypes and/or tags.
  • A query item matches if all its tag constraints match (AND) and the event type is in the allowed list.
  • Multiple query items are combined with OR — an event matches if any query item matches.

In-Memory Backend​

Source: DcbEventLogStorage_InMemory.res

Data Structure​

Events are stored in a ref<array<rawSequencedEvent>>. A monotonically increasing ref<int> position counter assigns a unique position to each appended event.

Conflict Detection​

append supports optimistic concurrency via appendCondition:

  • Re-evaluates condition.query against events recorded after condition.after.
  • Returns Error("conflict: condition check failed") if any matching event exists.
  • Returns Ok(position) otherwise.

Bus Integration​

When created via Make(Bus), the adapter registers its read function on the bus so that MCP resources and other components can look up DCB event history.

SQLite Backend​

Source: DcbEventLogStorage_Sqlite.res

Schema​

Events and their tags are split across two tables, mirroring the tags-as-separate-rows shape the DynamoDB adapter folds into one item:

CREATE TABLE dcb_event (
log_name TEXT NOT NULL,
position INTEGER NOT NULL, -- per-log sequence (see below)
event_type TEXT NOT NULL,
data TEXT NOT NULL, -- event payload (JSON)
meta TEXT NOT NULL, -- message meta (JSON)
recorded_at TEXT NOT NULL,
PRIMARY KEY (log_name, position)
);

CREATE TABLE dcb_tag (
log_name TEXT NOT NULL,
position INTEGER NOT NULL,
tag_key TEXT NOT NULL,
tag_value TEXT NOT NULL
);

CREATE INDEX dcb_tag_by_kv ON dcb_tag(log_name, tag_key, tag_value, position);

position is a per-log_name integer, allocated as MAX(position) + 1 inside the append transaction — a simple monotonic sequence rather than DynamoDB's <timestamp>-<uuid> string. The dcb_tag_by_kv index is what makes tag-filtered reads efficient.

Read​

buildQuerySql compiles a DCB query into one SELECT … FROM dcb_event:

  • The ~after filter becomes position > ?.
  • Each clause's eventTypes becomes an event_type IN (…) list.
  • Each tag constraint becomes an EXISTS (SELECT 1 FROM dcb_tag …) subquery correlated on position — so a clause with N tags ANDs N EXISTS subqueries.
  • Clauses are joined with OR; results are ORDER BY position ASC.

Tags for each returned event are loaded from dcb_tag and reattached; meta is parsed back through Message.metaSchema.

Append & Conflict Detection​

append runs inside a single SQLite transaction:

  1. If a condition is supplied, it runs the equivalent of read(condition.query, after=condition.after); any returned event aborts the transaction with Error("conflict: condition check failed").
  2. Each event is inserted under a freshly allocated position, with its tags written to dcb_tag.
  3. The last inserted position is returned as a string.

There are no fence sentinels — the literal re-read inside the transaction is the concurrency check, which the single-writer local process makes sufficient. This filters the append condition by tag and event type, exactly the semantics the DynamoDB backend now mirrors with its per-type (pos#<eventType>) fences — so a slice reading a subset of a partition's event types behaves identically on both backends. This is the key simplification over the DynamoDB backend's TransactWriteItems + per-type fences.

readStream is materialised eagerly via read for now; true streaming via statement.iterate() would need a Stream.fromIteratorEffect the current ReScript Stream module does not expose. The ~strongConsistency parameter is accepted for interface parity and ignored — a local SQLite file is always consistent.

Key Differences from AWS​

AspectLocal (in-memory)Local (SQLite)AWS (DynamoDB)
Storageref<array>dcb_event + dcb_tag tablesOne table: events + per-type fences
PositionInteger counterPer-log MAX(position)+1<timestamp>-<uuid> string
Tag indexArray filterdcb_tag_by_kv index + EXISTS subqueriestag_<key> / tag_composite GSIs
ConcurrencyArray scan after positionRe-read inside a transactionTransactWriteItems + per-type fences
DurabilityEphemeralPersists across restartsPersistent

See the AWS DcbEventLog adapter for the fence/consistency mechanics that the local backends deliberately simplify away.