/** A machine-specific, stable tag explaining why an event was ignored. */ export type IgnoreReason = Tag; /** Canonical trace reason for a completion belonging to another operation. */ export const STALE_OPERATION_IGNORE_REASON = "stale-operation" as const; export type StaleOperationIgnoreReason = typeof STALE_OPERATION_IGNORE_REASON; /** Transport-neutral metadata that links one dispatch to its causal chain. */ export interface DispatchContext { readonly messageId: string; readonly correlationId?: string; readonly causationId?: string; } /** * The common result shape for pure state-machine transitions. * * Applied transitions may keep the same state reference when they only emit * commands. Ignored transitions always keep the state reference and emit no * commands. */ export type TransitionResult< State, Command, Reason extends IgnoreReason = IgnoreReason, Outcome = never, > = | { kind: "ignored"; state: State; reason: Reason } | { kind: "applied"; state: State; commands: readonly Command[]; outcomes?: readonly Outcome[]; }; /** Explicitly marks a deliberate no-op in a total transition matrix. */ export function ignore< State, Command = never, Reason extends IgnoreReason = IgnoreReason, >(state: State, reason: Reason): TransitionResult { return { kind: "ignored", state, reason }; } /** Apply a transition to a next state, optionally emitting commands. */ export function change( nextState: State, commands?: readonly Command[], ): TransitionResult; export function change( nextState: State, commands: readonly Command[], outcomes: readonly Outcome[], ): TransitionResult; export function change( nextState: State, commands: readonly Command[] = [], outcomes: readonly Outcome[] = [], ): TransitionResult { return outcomes.length === 0 ? { kind: "applied", state: nextState, commands } : { kind: "applied", state: nextState, commands, outcomes }; } /** Apply commands while deliberately retaining the current state reference. */ export function stay( state: State, commands: readonly Command[], ): TransitionResult; export function stay( state: State, commands: readonly Command[], outcomes: readonly Outcome[], ): TransitionResult; export function stay( state: State, commands: readonly Command[], outcomes: readonly Outcome[] = [], ): TransitionResult { return outcomes.length === 0 ? { kind: "applied", state, commands } : { kind: "applied", state, commands, outcomes }; } export interface AppliedTransition { previous: State; event: Event; state: State; commands: readonly Command[]; dispatchContext?: DispatchContext; } export interface IgnoredEvent { state: State; event: Event; reason: Reason; dispatchContext?: DispatchContext; } /** Optional telemetry hooks shared by controllers and registries. */ export interface TransitionObserver< State, Event, Command, Reason extends IgnoreReason = IgnoreReason, > { onTransitionApplied?( transition: AppliedTransition, ): void; onEventIgnored?(event: IgnoredEvent): void; } /** Notify an observer without making controllers duplicate result plumbing. */ export function observeTransition< State, Event, Command, Reason extends IgnoreReason, >( observer: TransitionObserver | undefined, previous: State, event: Event, result: TransitionResult, dispatchContext?: DispatchContext, ): void { if (result.kind === "ignored") { observer?.onEventIgnored?.({ state: previous, event, reason: result.reason, ...(dispatchContext ? { dispatchContext } : {}), }); return; } observer?.onTransitionApplied?.({ previous, event, state: result.state, commands: result.commands, ...(dispatchContext ? { dispatchContext } : {}), }); }