Raxol. AgentClientProtocol. Ext. Journal. Mem
(Raxol Agent Client Protocol v0.1.0)
Copy Markdown
View Source
ETS-backed in-memory journal — an implementation of
Raxol.AgentClientProtocol.Ext.Journal bound by its OFFSET LAW.
Layout
One :ordered_set ETS table per session, keyed by offset
({offset, %Record{}}). Because the table is an :ordered_set keyed on the
integer offset, :ets.last/1 is the durable high-watermark in O(log n) and
read/3 is an offset-ordered range select.
Ownership (why it survives Writer death)
open/2 creates the table owned by the CALLING process (the app supervisor in
production, the test process in tests) — deliberately NOT the Writer. The
journal therefore outlives any single Writer crash/restart, which is the
precondition for the Writer-restart tip-fold orphan repair.
Protection level (:public, deliberate)
The table is :public, not :protected. :protected would forbid all
non-owner writes, but the single-publisher Writer is a DIFFERENT process from
the owner (the owner OUTLIVES the Writer, per the ownership note above), so
:protected would break every append. Offset integrity
is instead structural: exactly one Writer per session (the unique Writer
Registry) is the sole appender in production. Records are stored in PLAINTEXT —
co-resident BEAM processes are trusted; this store is not a confidentiality
boundary.
Atomic offset assignment — no counter
append/2 computes offset = high_watermark(j) + 1 by reading the table max
(:ets.last/1), NEVER a cached counter (OFFSET LAW rule 2). Atomicity across
concurrent appenders is the Writer's job: a single Writer
process per session serializes every append through one mailbox, so the
read-then-insert is race-free by construction (the behaviour may assume a
single writer per session).
Summary
Functions
Assign the next offset atomically (max + 1, read from the store), stamp
session_id + ts_hook, persist, and return the COMPLETE %Record{}.
Delete the backing table. Idempotent-ish: a double close raises like any ETS misuse.
Last DURABLE offset, read from the store (:ets.last/1); 0 for an empty
session. NEVER a cached counter (OFFSET LAW rule 2).
Open a fresh per-session handle. The ETS table is owned by the caller (see the
ownership note in the moduledoc). opts is accepted for behaviour conformance
and ignored.
Offset-ordered records in [from, to]. An empty or inverted range yields
{:ok, []}. Reads directly from the store — value-identical to what append
stored.
Types
Functions
@spec append(t(), Raxol.AgentClientProtocol.Ext.Journal.entry()) :: {:ok, Raxol.AgentClientProtocol.Ext.Journal.Record.t()}
Assign the next offset atomically (max + 1, read from the store), stamp
session_id + ts_hook, persist, and return the COMPLETE %Record{}.
@spec close(t()) :: :ok
Delete the backing table. Idempotent-ish: a double close raises like any ETS misuse.
@spec high_watermark(t()) :: non_neg_integer()
Last DURABLE offset, read from the store (:ets.last/1); 0 for an empty
session. NEVER a cached counter (OFFSET LAW rule 2).
Open a fresh per-session handle. The ETS table is owned by the caller (see the
ownership note in the moduledoc). opts is accepted for behaviour conformance
and ignored.
@spec read(t(), pos_integer(), non_neg_integer()) :: {:ok, [Raxol.AgentClientProtocol.Ext.Journal.Record.t()]}
Offset-ordered records in [from, to]. An empty or inverted range yields
{:ok, []}. Reads directly from the store — value-identical to what append
stored.