Docket.Backend.EventStore behaviour (docket v0.1.0)

Copy Markdown View Source

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

ctx()

@type ctx() :: Docket.Backend.ctx()

scope()

@type scope() :: Docket.Backend.scope()

Callbacks

append_events(ctx, scope, run_id, events)

@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.

fetch_event(ctx, scope, run_id, seq)

@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.

fetch_latest_event(ctx, scope, run_id)

@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.

list_events(ctx, scope, run_id, opts)

@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.