Persistence contract for append-only run events.
Event retention is a backend policy. Lifecycle orchestration calls this store in the same backend transaction as the corresponding run insert or commit so durable state and its retained facts cannot diverge.
Summary
Callbacks
Appends retained events for run_id in sequence order.
Reads one retained event by its assigned positive sequence.
Reads the highest-sequence retained event for a run.
Reads a page of committed retained events for run_id.
Types
@type ctx() :: Docket.Backend.ctx()
@type scope() :: Docket.Backend.scope()
Callbacks
@callback append_events( ctx(), scope(), run_id :: String.t(), events :: [Docket.Event.t()] ) :: :ok | {:error, term()}
Appends retained events for run_id in sequence order.
The operation must be idempotent for an already-persisted {run_id, seq}
containing the same event. Conflicting content for that key is an error.
Every event must belong to run_id; a backend must reject a mismatched
event rather than storing it under the supplied run.
Event identities are assigned before this callback. The store never derives
a sequence from the maximum already-stored sequence, never substitutes the
run's checkpoint sequence, and accepts gaps left by persistence filtering. In particular,
:checkpoint_committed is an ordinary assigned event at this boundary.
For a non-empty append, scope is checked through the owning run and a
tenant mismatch is reported as {:error, :not_found}. An empty event list
validates the scope value but succeeds without a run lookup or storage
change.
@callback fetch_event( ctx(), scope(), run_id :: String.t(), seq :: pos_integer() ) :: {:ok, Docket.Event.t()} | {:error, :not_found} | {:error, term()}
Reads one retained event by its assigned positive sequence.
Ownership is enforced through the owning run. An unknown or out-of-scope
run and an absent or pruned sequence all return {:error, :not_found}.
A present corrupt row returns a typed
{:error, %Docket.Error{type: :corrupt_event_row}} and is never reported as
absent.
seq is trusted to be pre-validated by the caller as a positive integer.
@callback fetch_latest_event( ctx(), scope(), run_id :: String.t() ) :: {:ok, Docket.Event.t() | nil} | {:error, :not_found} | {:error, term()}
Reads the highest-sequence retained event for a run.
Ownership is enforced through the owning run. An unknown or out-of-scope
run returns {:error, :not_found}. A visible run with no retained events
returns {:ok, nil}, including when its complete history has been pruned.
A present corrupt latest row returns a typed
{:error, %Docket.Error{type: :corrupt_event_row}} and is never skipped in
favor of an older event.
@callback list_events( ctx(), scope(), run_id :: String.t(), opts :: %{after_seq: non_neg_integer(), limit: pos_integer()} ) :: {:ok, Docket.EventPage.t()} | {:error, :not_found} | {:error, term()}
Reads a page of committed retained events for run_id.
Events are returned in ascending sequence order, restricted to sequences
greater than opts.after_seq and limited to at most opts.limit rows. This
keyset scan skips pruned and persistence-filtered sequences, so a page and
the retention bounds are not promised contiguous.
Ownership is enforced through the owning run: a wrong tenant and an unknown
run both report {:error, :not_found}. The page rows and the returned
retention bounds are observed from one consistent snapshot. A corrupt or
undecodable stored row is a typed error and is never silently skipped: a
corrupt event row returns {:error, %Docket.Error{type: :corrupt_event_row}},
while a corrupt owning-run row propagates the same way as run reads, by
raising the typed error.
Options are trusted to be pre-validated by the caller: after_seq is a
non-negative integer and limit is a positive integer. A backend may assert
these rather than validate them.