keiro-core-0.6.0.0: src/Keiro/EventStream.hs
-- | The complete description of one persistent event stream.
--
-- An 'EventStream' marries a pure keiki 'SymTransducer' (the decision logic
-- of a symbolic-register state machine) with everything keiro needs to run
-- it against a durable event store: where its initial state and registers
-- come from, how its emitted events are serialized ('Codec'), which physical
-- stream name to read and write, when to snapshot, and how to serialize that
-- snapshot. Command handling ("Keiro.Command") hydrates the machine from
-- stored events (optionally fast-forwarding from a snapshot), steps it with a
-- command, encodes the resulting events, and appends them. Public command
-- runners require a 'Keiro.EventStream.Validate.ValidatedEventStream', obtained
-- from 'Keiro.EventStream.Validate.mkEventStream' or
-- 'Keiro.EventStream.Validate.mkEventStreamOrThrow', rather than a bare record
-- literal.
--
-- The type parameters thread through from the underlying transducer:
--
-- * @phi@ — the guard/predicate alphabet the transducer branches on.
-- * @rs@ — the register set ('RegFile' @rs@ holds the live values).
-- * @s@ — the control state.
-- * @ci@ — the command input the machine consumes.
-- * @co@ — the event output the machine emits (what 'eventCodec' serializes).
module Keiro.EventStream
( EventStream (..),
Terminality (..),
SnapshotPolicy (..),
StateCodec (..),
)
where
import Keiki.Core (RegFile, SymTransducer)
import Keiro.Codec (Codec)
import Keiro.Prelude
import Keiro.Stream (Stream)
import Kiroku.Store.Types (StreamName, StreamVersion)
-- | A self-contained, persistable event stream definition.
--
-- * 'transducer' — the pure keiki state machine that turns a command into
-- emitted events.
-- * 'initialState' \/ 'initialRegisters' — the machine's starting control
-- state and register file, used when hydrating an empty stream.
-- * 'eventCodec' — serializes and migrates the emitted events (@co@) to and
-- from stored payloads.
-- * 'resolveStreamName' — maps a typed 'Stream' handle to the physical
-- 'StreamName' read and appended to in the store.
-- * 'snapshotPolicy' — decides, per append, whether to persist a snapshot of
-- the @(state, registers)@ pair.
-- * 'stateCodec' — how to serialize that snapshot. Set 'snapshotPolicy' and
-- 'stateCodec' coherently; 'Keiro.EventStream.Validate.mkEventStream' rejects
-- a snapshotting policy without a state codec and returns the
-- 'Keiro.EventStream.Validate.ValidatedEventStream' that command runners accept.
data EventStream phi rs s ci co = EventStream
{ transducer :: !(SymTransducer phi rs s ci co),
initialState :: !s,
initialRegisters :: !(RegFile rs),
eventCodec :: !(Codec co),
resolveStreamName :: !(Stream (EventStream phi rs s ci co) -> StreamName),
snapshotPolicy :: !(SnapshotPolicy (s, RegFile rs)),
stateCodec :: !(Maybe (StateCodec (s, RegFile rs)))
}
deriving stock (Generic)
-- | Whether the append that just happened reached a terminal stream state.
data Terminality = Terminal | NotTerminal
deriving stock (Eq, Show, Generic)
-- | When to persist a snapshot of a stream's folded state.
--
-- * 'Never' — never snapshot; always rehydrate from the full event log.
-- * 'Every' @n@ — snapshot whenever the stream version is a multiple of @n@
-- (a non-positive interval disables snapshotting).
-- * 'OnTerminal' — snapshot only when the machine has reached a final state.
-- * 'Custom' — an arbitrary predicate over terminality, folded @state@, and
-- the current 'StreamVersion'.
--
-- See 'Keiro.Snapshot.Policy.shouldSnapshot' for the evaluation rules.
data SnapshotPolicy state
= Never
| Every !Int
| OnTerminal
| Custom !(Terminality -> state -> StreamVersion -> Bool)
deriving stock (Generic)
-- | How to serialize and deserialize a stream's snapshot state.
--
-- 'stateCodecVersion', 'shapeHash', and 'stateShapeHash' together gate snapshot
-- reuse: a stored snapshot is only loaded when all three match the current
-- codec, so incompatible encodings, register layouts, and control-state shapes
-- invalidate older snapshots and force a clean rehydration from events.
--
-- * 'stateCodecVersion' — bumped when the snapshot encoding changes
-- incompatibly, and whenever fold logic changes in a way the structural hashes
-- and any composed fold fingerprint cannot see.
-- * 'shapeHash' — a digest of the register-file layout.
-- * 'stateShapeHash' — a digest of the control-state shape, optionally composed
-- with a fold fingerprint.
-- * 'encode' \/ 'decode' — the JSON serialization of the @(state,
-- registers)@ pair.
--
-- Hand-written guard and update function bodies are not structurally
-- inspectable. Changing them without also changing a composed fold fingerprint
-- MUST bump 'stateCodecVersion'; otherwise an old snapshot can still match and
-- be served as a stale hydration seed.
data StateCodec state = StateCodec
{ stateCodecVersion :: !Int,
shapeHash :: !Text,
stateShapeHash :: !Text,
encode :: !(state -> Value),
decode :: !(Value -> Either Text state)
}
deriving stock (Generic)