A GM's own state store (runtime v2).
Today MoM keeps one blob per game and every event rewrites it, so every write
conflicts with every other. Here the GM owns its state as ONE ROW PER PROTOCOL
(:market, :coseller, :gm, ...), so events on different protocols never touch
the same row. The handler contract is unchanged: a handler is given the whole
state and returns a shallow :updated-state keyed by protocol, which replaces
those protocols' rows wholesale, exactly as MoM merges it today.
Three things live beside the state, all written in the SAME transaction as it:
dispatch stamps a fresh one into [:gm :progress-token]
on every non-query event; written into the :gm row that would make every event
rewrite one row. It is stored on the journal entry instead and read back as the
latest one, so :gm is only written when a handler really changes it.WHAT A HANDLER SEES. State is stored as JSON and read back with keyword keys, the
same round trip it makes over HTTP today (keywords become strings, then
:state-enums normalises the ones that matter), so moving a GM onto the store
does not change what its handlers receive. json-normalize is that round trip.
A GM's own state store (runtime v2). Today MoM keeps one blob per game and every event rewrites it, so every write conflicts with every other. Here the GM owns its state as ONE ROW PER PROTOCOL (`:market`, `:coseller`, `:gm`, ...), so events on different protocols never touch the same row. The handler contract is unchanged: a handler is given the whole state and returns a shallow `:updated-state` keyed by protocol, which replaces those protocols' rows wholesale, exactly as MoM merges it today. Three things live beside the state, all written in the SAME transaction as it: - IDEMPOTENCY RECEIPTS, a table of their own instead of a map inside the state, so they neither grow a hot row forever nor make every event touch it. - a JOURNAL: what happened (event, caller, GM version, outcome). It is not authoritative; the state is. It can be redacted for a privacy request. - the PROGRESS TOKEN. `dispatch` stamps a fresh one into `[:gm :progress-token]` on every non-query event; written into the `:gm` row that would make every event rewrite one row. It is stored on the journal entry instead and read back as the latest one, so `:gm` is only written when a handler really changes it. WHAT A HANDLER SEES. State is stored as JSON and read back with keyword keys, the same round trip it makes over HTTP today (keywords become strings, then `:state-enums` normalises the ones that matter), so moving a GM onto the store does not change what its handlers receive. `json-normalize` is that round trip.
(assemble-state rows token)protocol rows ({name value}) plus the latest progress token -> the state map.
protocol rows ({name value}) plus the latest progress token -> the state map.
(callback-events effect)The events to apply for a completed effect: on-success if it succeeded, else on-failure.
The events to apply for a completed effect: on-success if it succeeded, else on-failure.
(effect-fingerprint type data)Identifies an effect's type and payload, so a key reused with a different payload is caught instead of mistaken for a replay. Callback events are not part of it.
Identifies an effect's type and payload, so a key reused with a different payload is caught instead of mistaken for a replay. Callback events are not part of it.
(effect-key raw-effect)The key an emitted effect is stored under: its trimmed, nonblank idempotency key.
The key an emitted effect is stored under: its trimmed, nonblank idempotency key.
(effect-report {:keys [disposition row existing key type error]})What the caller is told about one emitted effect: {:key :type :disposition :status :result :error}.
What the caller is told about one emitted effect: {:key :type :disposition :status
:result :error}.(transact-event! store lock-protocols f)Runs one event atomically.
Takes a write lock on the rows named in lock-protocols (normally the event's
own protocol), reads the whole state, and calls (f ctx) with
{:state <state> :find-receipt (fn [receipt-id])}. f returns
{:patch {protocol value} ; replaces those rows (optional)
:receipt {:id ".." :data {..}} ; idempotency receipt (optional)
:journal {:event-type .. :event-version .. :idempotency-key .. :actor ..
:caller {..} :gm-version .. :success bool :status int
:error-code .. :detail {..}} ; (optional)
:result anything} ; handed back to the caller
and the patch, receipt and journal entry are committed together or not at all.
f runs INSIDE the transaction, so it must be safe to run again: if it patches
a protocol it did not lock and another event changed that row meanwhile, the
whole event is re-run against fresh state. A throw from f writes nothing.
Returns {:result .. :wrote #{protocol-keyword}}.
Runs one event atomically.
Takes a write lock on the rows named in `lock-protocols` (normally the event's
own protocol), reads the whole state, and calls (f ctx) with
{:state <state> :find-receipt (fn [receipt-id])}. `f` returns
{:patch {protocol value} ; replaces those rows (optional)
:receipt {:id ".." :data {..}} ; idempotency receipt (optional)
:journal {:event-type .. :event-version .. :idempotency-key .. :actor ..
:caller {..} :gm-version .. :success bool :status int
:error-code .. :detail {..}} ; (optional)
:result anything} ; handed back to the caller
and the patch, receipt and journal entry are committed together or not at all.
`f` runs INSIDE the transaction, so it must be safe to run again: if it patches
a protocol it did not lock and another event changed that row meanwhile, the
whole event is re-run against fresh state. A throw from `f` writes nothing.
Returns {:result .. :wrote #{protocol-keyword}}.(prune! store opts)Deletes what has outlived its retention window, oldest first, at most (:limit opts) (default 1000) rows per table per call, and returns {:receipts n :journal n
:outbox n}. (:now-ms opts) is the clock; a table whose window is absent is left
alone.
:receipts-ms idempotency receipts older than this :journal-ms journal rows older than this, EXCEPT the newest one carrying a progress token (the latest token is read from it) :outbox-ms completed outbox rows older than this whose callback has been applied. A number, or {:default ms "ledger/" ms}: a string key is a type prefix and the longest matching prefix wins. A pending effect, or one whose callback is not yet applied, is never deleted.
A receipt pruned and then reused re-runs its event, like any TTL'd idempotency key; MoM's ledger remembers its own keys, so a pruned ledger effect cannot pay twice.
Deletes what has outlived its retention window, oldest first, at most `(:limit
opts)` (default 1000) rows per table per call, and returns {:receipts n :journal n
:outbox n}. `(:now-ms opts)` is the clock; a table whose window is absent is left
alone.
:receipts-ms idempotency receipts older than this
:journal-ms journal rows older than this, EXCEPT the newest one carrying a
progress token (the latest token is read from it)
:outbox-ms completed outbox rows older than this whose callback has been
applied. A number, or {:default ms "ledger/" ms}: a string key is
a type prefix and the longest matching prefix wins. A pending
effect, or one whose callback is not yet applied, is never deleted.
A receipt pruned and then reused re-runs its event, like any TTL'd idempotency
key; MoM's ledger remembers its own keys, so a pruned ledger effect cannot pay twice.(claim-effects! store opts)Leases due, unclaimed :pending effects to (:worker opts) until
now + (:lease-ms opts), oldest first, up to (:limit opts) (default 20). Only
rows with next-attempt-at <= (:now-ms opts) and no live lease are taken, and a
row can be held by one worker at a time. (:keys opts) limits the claim to those
effect keys. Returns the claimed rows, each with :abandoned?: true when an earlier
worker had claimed the row and never finished it (its lease ran out), false for a
fresh row or one an earlier attempt released for retry. It describes this claim; it
is not stored on the row. An effect that must not be repeated after an unknown
outcome uses it: an abandoned one may already have been applied.
Leases due, unclaimed `:pending` effects to `(:worker opts)` until now + `(:lease-ms opts)`, oldest first, up to `(:limit opts)` (default 20). Only rows with next-attempt-at <= `(:now-ms opts)` and no live lease are taken, and a row can be held by one worker at a time. `(:keys opts)` limits the claim to those effect keys. Returns the claimed rows, each with `:abandoned?`: true when an earlier worker had claimed the row and never finished it (its lease ran out), false for a fresh row or one an earlier attempt released for retry. It describes this claim; it is not stored on the row. An effect that must not be repeated after an unknown outcome uses it: an abandoned one may already have been applied.
(list-receipts store)Every idempotency receipt as {receipt-id data}, data normalised like find-receipt.
For the reverse export (rebuilding a MoM-shaped :idempotency map); bounded by
retention.
Every idempotency receipt as {receipt-id data}, data normalised like `find-receipt`.
For the reverse export (rebuilding a MoM-shaped :idempotency map); bounded by
retention.(find-receipt store receipt-id)The idempotency receipt stored under receipt-id, or nil.
The idempotency receipt stored under `receipt-id`, or nil.
(find-effect store effect-key)The outbox row for effect-key (its idempotency key), or nil.
The outbox row for `effect-key` (its idempotency key), or nil.
(import-state! store state)Replaces the entire store with state, an export in the shape MoM's
/api/v1/storage/game/:game/reload returns: protocol rows, plus the legacy
:idempotency map (moved into the receipts table) and [:gm :progress-token]
(recorded on a journal entry). For the one-time move of a game onto the store.
Replaces the entire store with `state`, an export in the shape MoM's /api/v1/storage/game/:game/reload returns: protocol rows, plus the legacy :idempotency map (moved into the receipts table) and [:gm :progress-token] (recorded on a journal entry). For the one-time move of a game onto the store.
(protocol-versions store){protocol-keyword version}: how many times each row has been written.
{protocol-keyword version}: how many times each row has been written.
(read-journal store limit)The most recent limit journal entries, oldest first.
The most recent `limit` journal entries, oldest first.
(finish-effect! store effect-key worker outcome now-ms)Records the result of executing a claimed effect, only if worker still holds
the claim. outcome is {:status :succeeded|:failed :result .. :error ..}, which
completes the effect, or {:retry true :error .. :retry-after-ms n}, which keeps it
pending, counts the attempt and schedules the next one. Returns the updated row,
or nil if the claim was lost.
Records the result of executing a claimed effect, only if `worker` still holds
the claim. `outcome` is {:status :succeeded|:failed :result .. :error ..}, which
completes the effect, or {:retry true :error .. :retry-after-ms n}, which keeps it
pending, counts the attempt and schedules the next one. Returns the updated row,
or nil if the claim was lost.(read-state store)The whole state as a map of protocol -> value, with the latest progress token under [:gm :progress-token]. Receipts are not part of it (see find-receipt).
The whole state as a map of protocol -> value, with the latest progress token under [:gm :progress-token]. Receipts are not part of it (see find-receipt).
(claim-callbacks! store opts)Like claim-effects!, for effects that have completed (:succeeded or :failed) but whose on-success / on-failure events have not been applied yet.
Like claim-effects!, for effects that have completed (:succeeded or :failed) but whose on-success / on-failure events have not been applied yet.
(transact-callback! store effect-key lock-protocols f)transact-event!, for applying an effect's callback events: it also row-locks the
outbox row, calls (f ctx) with the row under :effect added to the usual context,
and marks the callback applied IN THE SAME TRANSACTION as the state change, so
the callback happens exactly once however many workers try. Returns
{:skipped :not-applicable} without calling f when the effect is missing, still
pending, or already applied.
transact-event!, for applying an effect's callback events: it also row-locks the
outbox row, calls (f ctx) with the row under :effect added to the usual context,
and marks the callback applied IN THE SAME TRANSACTION as the state change, so
the callback happens exactly once however many workers try. Returns
{:skipped :not-applicable} without calling `f` when the effect is missing, still
pending, or already applied.(json-normalize v)What v becomes after one trip through JSON: the form a handler sees.
What `v` becomes after one trip through JSON: the form a handler sees.
(lock-names lock-protocols)(outbox-window outbox-ms type)The retention window (ms) for an outbox row of type, from a number or a map of
type prefixes (the longest matching prefix wins) with an optional :default. nil when
there is none, meaning the row is not pruned.
The retention window (ms) for an outbox row of `type`, from a number or a map of type prefixes (the longest matching prefix wins) with an optional :default. nil when there is none, meaning the row is not pruned.
(patch-token patch)The progress token a handler result carries, if any.
The progress token a handler result carries, if any.
(plan-effect existing raw-effect now-ms)What an emitted effect does to the outbox, given the row already stored under its key (or nil). Returns {:disposition d ...}:
:new insert (or reset) :row, status :pending. A prior :failed effect under
the same key is reset and run again, as the router has always done.
:replay a prior :succeeded effect with the same payload: its stored outcome
stands, nothing runs.
:in-flight the same payload is already pending: it is not duplicated.
:conflict the key was used with a different payload (and is succeeded or still
pending): rejected, never executed.
:invalid no type or no idempotency key.
What an emitted effect does to the outbox, given the row already stored under its
key (or nil). Returns {:disposition d ...}:
:new insert (or reset) `:row`, status :pending. A prior :failed effect under
the same key is reset and run again, as the router has always done.
:replay a prior :succeeded effect with the same payload: its stored outcome
stands, nothing runs.
:in-flight the same payload is already pending: it is not duplicated.
:conflict the key was used with a different payload (and is succeeded or still
pending): rejected, never executed.
:invalid no type or no idempotency key.(plan-writes current patch)The rows an event really needs to write: {protocol-name normalized-value}.
current is {protocol-name normalized-value} as read. A protocol whose new value
equals what is stored is skipped (no needless write or version bump), and the
:gm row has the progress token removed first; a :gm patch that is nothing but
the token writes nothing.
The rows an event really needs to write: {protocol-name normalized-value}.
`current` is {protocol-name normalized-value} as read. A protocol whose new value
equals what is stored is skipped (no needless write or version bump), and the
`:gm` row has the progress token removed first; a `:gm` patch that is nothing but
the token writes nothing.(protocol-name k)(split-import state)An export -> {:rows {protocol-name value} :receipts {id data} :token t}.
An export -> {:rows {protocol-name value} :receipts {id data} :token t}.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |