machina 6.2.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -8
- package/dist/index.cjs +357 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +656 -221
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +656 -221
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +357 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/emitter.ts","../src/types.ts","../src/behavioral-fsm.ts","../src/fsm.ts"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Listener = (...args: any[]) => void;\ntype WildcardListener = (eventName: string, data: unknown) => void;\n\n/**\n * Returned by `on()`. Call `off()` to remove the subscription.\n * Safe to call multiple times — subsequent calls are no-ops because\n * the underlying Set deduplicates membership.\n */\nexport type Subscription = { off(): void };\n\n/**\n * Minimal typed event emitter used internally by Fsm and BehavioralFsm.\n *\n * Two listener categories exist at runtime: named listeners (keyed by event\n * name) and wildcard listeners (keyed as `\"*\"`). The wildcard fires on EVERY\n * emit, regardless of event name, and its callback receives both the event\n * name and the data. Named listeners receive only the data.\n *\n * @typeParam TEventMap - Record mapping event names to their payload types.\n * Constrains `on()` and `emit()` to matching event/payload pairs.\n */\nexport class Emitter<TEventMap extends Record<string, unknown>> {\n private listeners = new Map<string, Set<Listener>>();\n\n /**\n * Subscribe to a named event or the wildcard.\n *\n * Named overload: callback receives only the typed payload.\n * Wildcard overload (`\"*\"`): callback receives `(eventName, data)` — useful\n * for proxying all events to another emitter, as Fsm does with its bfsm.\n *\n * Wildcard listeners fire BEFORE named listeners. If a wildcard listener\n * removes a named listener via `off()`, the named listener still fires for\n * the current emit — Set iteration is not affected by concurrent deletes.\n *\n * Returns a `Subscription` whose `off()` removes this specific listener.\n */\n on<K extends keyof TEventMap & string>(\n event: K,\n cb: (data: TEventMap[K]) => void\n ): Subscription;\n on(event: \"*\", cb: WildcardListener): Subscription;\n on(event: string, cb: Listener): Subscription {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(cb);\n return {\n off: () => {\n set.delete(cb);\n },\n };\n }\n\n /**\n * Emit a named event, notifying wildcard listeners first, then named listeners.\n *\n * Wildcard listeners receive `(eventName, data)`; named listeners receive\n * only `data`. The firing order (wildcards before named) is intentional —\n * it lets relay listeners (like Fsm's bfsm proxy) observe all events\n * before specific subscribers react.\n */\n emit<K extends keyof TEventMap & string>(event: K, data: TEventMap[K]): void {\n const wildcards = this.listeners.get(\"*\");\n if (wildcards) {\n for (const cb of wildcards) {\n (cb as WildcardListener)(event, data);\n }\n }\n const named = this.listeners.get(event);\n if (named) {\n for (const cb of named) {\n cb(data);\n }\n }\n }\n\n /**\n * Remove all listeners (named and wildcard).\n *\n * Existing `Subscription` objects remain valid — their `off()` closures\n * hold a reference to the now-empty Set, so calling them is harmless.\n * Called by `dispose()` to prevent memory retention after an FSM shuts down.\n */\n clear(): void {\n this.listeners.clear();\n }\n}\n","// =============================================================================\n// types.ts — Shared type foundations for machina v6\n//\n// Pure type definitions. No runtime code. Every type here is consumed by\n// behavioral-fsm.ts, fsm.ts, or both. The factory functions (createFsm,\n// createBehavioralFsm) use these as building blocks for their own generics.\n//\n// =============================================================================\n\n// -----------------------------------------------------------------------------\n// Utility types for state/input name extraction\n//\n// These are the inference primitives that power machina's \"zero-ceremony\"\n// type safety. Given a states config object, they extract the state name\n// union and input name union as string literal types.\n// -----------------------------------------------------------------------------\n\n/**\n * Keys on a state object that have special meaning and are NOT input names.\n * Used by InputNamesOf to filter these out when collecting input names.\n */\ntype SpecialStateKeys = \"_onEnter\" | \"_onExit\" | \"_child\" | \"*\";\n\n/**\n * Extracts state names as a string literal union from a states config object.\n *\n * @example\n * ```ts\n * type S = StateNamesOf<{ green: {...}, yellow: {...}, red: {...} }>;\n * // => \"green\" | \"yellow\" | \"red\"\n * ```\n */\nexport type StateNamesOf<TStates> = keyof TStates & string;\n\n/**\n * Extracts input names as a string literal union from a states config object.\n * Collects all handler keys across ALL states, then strips out lifecycle hooks\n * and special keys (_onEnter, _onExit, _child, *).\n *\n * This is what flows into `handle(inputName)` to provide compile-time\n * validation of input names.\n *\n * @example\n * ```ts\n * type I = InputNamesOf<{\n * idle: { start: \"running\", reset: fn };\n * running: { pause: \"paused\", stop: \"idle\" };\n * }>;\n * // => \"start\" | \"reset\" | \"pause\" | \"stop\"\n * ```\n *\n * How it works:\n * 1. `{ [S in keyof TStates]: keyof TStates[S] & string }` — maps each state\n * to the union of its handler key names\n * 2. `[keyof TStates]` — collapses the mapped type into a flat union of ALL\n * handler keys across every state\n * 3. `Exclude<..., SpecialStateKeys>` — strips lifecycle/special keys\n */\nexport type InputNamesOf<TStates> = Exclude<\n { [S in keyof TStates]: keyof TStates[S] & string }[keyof TStates],\n SpecialStateKeys\n>;\n\n// -----------------------------------------------------------------------------\n// Handler argument object\n//\n// Every handler in machina receives this as its first argument. This replaces\n// the old v5 pattern of binding `this` to the FSM and passing the client as\n// a separate parameter. Benefits:\n// - Works with arrow functions (no `this` caveat)\n// - BehavioralFsm and Fsm handler signatures are identical\n// - Destructure only what you need: tick({ ctx }) { ... }\n// - Easy to mock in tests: just construct the object\n//\n// Handlers RETURN a state name to trigger a transition (or void/undefined\n// to stay in the current state). This replaces the imperative transition()\n// call and mirrors gen_fsm's {next_state, StateName, NewStateData} pattern.\n// -----------------------------------------------------------------------------\n\n/**\n * The single combined object passed to every handler.\n *\n * Handlers return a state name to transition, or void to stay put.\n * This replaces imperative `transition()` calls — closer to gen_fsm's\n * return-based model and symmetrical with string shorthand handlers.\n *\n * @typeParam TCtx - The context type. For Fsm this is the config-defined\n * context object. For BehavioralFsm this is the client object itself.\n * @typeParam TStateNames - String literal union of valid state names.\n * Defaults to `string` for loose usage; the factory functions narrow this\n * to the actual state names inferred from the config.\n *\n * @example\n * ```ts\n * // Conditional transition — return the target state:\n * timeout({ ctx }) {\n * if (ctx.tickCount >= 3) return \"yellow\";\n * }\n *\n * // Side effects without transition — return nothing:\n * tick({ ctx }) {\n * ctx.tickCount++;\n * }\n *\n * // In a catch-all — inputName tells you what was received:\n * \"*\"({ inputName }) {\n * console.log(`unhandled input: ${inputName}`);\n * }\n * ```\n */\nexport interface HandlerArgs<TCtx, TStateNames extends string = string> {\n /** The context (Fsm) or client object (BehavioralFsm) */\n ctx: TCtx;\n\n /**\n * The name of the input currently being handled.\n *\n * Typed as `string` rather than the inferred input union because:\n * 1. Inside a named handler you already know the input name\n * 2. In a catch-all (*) handler it could be anything\n * 3. Narrowing to the literal per-handler would require complex\n * mapped types for zero practical benefit\n */\n inputName: string;\n\n /**\n * Defer the current input for replay after a future transition.\n * Erlang's selective receive, in JS form.\n *\n * @example\n * ```ts\n * // Replay on the next transition to any state\n * defer();\n *\n * // Replay only when entering \"yellow\"\n * defer({ until: \"yellow\" });\n * ```\n */\n defer(opts?: { until: TStateNames }): void;\n\n /**\n * Emit a custom event through the FSM's emitter.\n * Built-in events (transitioning, transitioned, etc.) are emitted\n * automatically by the FSM engine — this is for user-defined events.\n */\n emit(eventName: string, data?: unknown): void;\n}\n\n// -----------------------------------------------------------------------------\n// Handler function type\n//\n// A single callable type for all handler forms: state input handlers,\n// lifecycle hooks (_onEnter, _onExit), and the catch-all (*).\n//\n// Handlers return a state name to trigger a transition, or void/undefined\n// to stay in the current state. This is the dynamic counterpart to string\n// shorthand — same concept (determine next state), two expressions.\n// -----------------------------------------------------------------------------\n\n/**\n * A function handler for state inputs, lifecycle hooks (_onEnter, _onExit),\n * and catch-all (*) handlers.\n *\n * **Return value determines transition:**\n * - Return a valid state name → FSM transitions to that state\n * - Return void/undefined → FSM stays in the current state\n *\n * This mirrors gen_fsm's `{next_state, StateName, NewStateData}` return.\n * Guards are just `if` statements. Actions are just code before the return.\n *\n * The `...extra` rest parameter captures additional arguments passed through\n * `handle(inputName, ...extraArgs)`. These are untyped (`unknown[]`) because\n * correlating per-input arg types with handle() call sites would require\n * prohibitively complex mapped types for minimal benefit.\n *\n * @example\n * ```ts\n * // Side effects only, no transition:\n * tick({ ctx }) { ctx.tickCount++; }\n *\n * // Conditional transition (replaces guard + target):\n * timeout({ ctx }) {\n * if (ctx.tickCount >= 3) return \"yellow\";\n * }\n *\n * // Unconditional transition with side effect (replaces action + target):\n * timeout({ ctx }) {\n * console.log(\"transitioning after\", ctx.tickCount, \"ticks\");\n * return \"yellow\";\n * }\n *\n * // Handler with extra args passed via handle(\"success\", responseData):\n * success({ ctx }, data) { ctx.result = data; }\n * ```\n */\nexport type HandlerFn<TCtx, TStateNames extends string = string> = (\n args: HandlerArgs<TCtx, TStateNames>,\n ...extra: unknown[]\n) => TStateNames | void;\n\n// -----------------------------------------------------------------------------\n// Handler definition forms\n//\n// A handler property on a state object is one of two things:\n// 1. A string — auto-transition shorthand: `timeout: \"yellow\"`\n// 2. A function — returns target state or void: `tick({ ctx }) { ... }`\n//\n// Two forms, one concept: \"a handler determines the next state.\"\n// String is the static case, function is the dynamic case.\n//\n// The FSM engine checks typeof at runtime: \"string\" → immediate transition,\n// \"function\" → call it and transition if it returns a state name.\n// -----------------------------------------------------------------------------\n\n/**\n * The union of valid handler definition forms for a state input.\n *\n * - `TStateNames` — string shorthand, auto-transitions to that state\n * - `HandlerFn` — function that returns a state name (transition) or void (stay)\n *\n * @example\n * ```ts\n * states: {\n * green: {\n * timeout: \"yellow\", // string shorthand\n * tick({ ctx }) { ctx.tickCount++; }, // function, no transition\n * emergency({ ctx }) { // function, conditional transition\n * if (ctx.severity > 5) return \"red\";\n * },\n * },\n * }\n * ```\n */\n/**\n * The union of valid handler definition forms for a state input.\n *\n * - `TStateNames` — string shorthand, auto-transitions to that state\n * - `HandlerFn` — function that returns a state name (transition) or void (stay)\n * - `MachinaInstance` — included to satisfy TypeScript's structural widening\n * when `ValidateStates` falls back to its constraint type. The per-key\n * restriction on `_child` is still enforced by `ValidateStates`; this\n * union member just prevents the inference engine from rejecting\n * `_child: childFsm` before the conditional mapping can evaluate it.\n *\n * @example\n * ```ts\n * states: {\n * green: {\n * timeout: \"yellow\", // string shorthand\n * tick({ ctx }) { ctx.tickCount++; }, // function, no transition\n * emergency({ ctx }) { // function, conditional transition\n * if (ctx.severity > 5) return \"red\";\n * },\n * },\n * }\n * ```\n */\nexport type HandlerDef<TCtx, TStateNames extends string = string> =\n | TStateNames\n | HandlerFn<TCtx, TStateNames>\n | MachinaInstance;\n\n// -----------------------------------------------------------------------------\n// State validation\n//\n// ValidateStates is the mapped type that makes config-inferred type safety\n// work. It re-maps the user's states object, constraining each property to\n// the correct type based on its key name. This is where typos in string\n// shorthand targets get caught at compile time.\n//\n// The key trick: `keyof TStates & string` is self-referential — it derives\n// the state name union FROM the same states object being validated. So when\n// a user writes `timeout: \"yellw\"`, TypeScript checks \"yellw\" against the\n// actual state keys and reports the error.\n// -----------------------------------------------------------------------------\n\n/**\n * Validates and constrains the states object at the type level.\n *\n * This is a mapped type that walks every state and every property within\n * each state, assigning the correct expected type based on the property key:\n *\n * | Key | Expected type |\n * |------------------|-----------------------------------------|\n * | `_onEnter` | HandlerFn (lifecycle hook) |\n * | `_onExit` | HandlerFn (lifecycle hook) |\n * | `_child` | MachinaInstance (Fsm or BehavioralFsm) |\n * | `*` | HandlerFn (catch-all) |\n * | anything else | HandlerDef (string or fn) |\n *\n * @typeParam TCtx - Context/client type, flows into handler signatures\n * @typeParam TStates - The literal states object type captured by the\n * factory function's generic parameter\n */\nexport type ValidateStates<TCtx, TStates extends Record<string, Record<string, unknown>>> = {\n [S in keyof TStates]: {\n [K in keyof TStates[S]]: K extends \"_onEnter\" | \"_onExit\"\n ? HandlerFn<TCtx, keyof TStates & string>\n : K extends \"_child\"\n ? MachinaInstance\n : K extends \"*\"\n ? HandlerFn<TCtx, keyof TStates & string>\n : HandlerDef<TCtx, keyof TStates & string>;\n };\n};\n\n// -----------------------------------------------------------------------------\n// FSM configuration\n//\n// The config shape passed to createFsm() and createBehavioralFsm().\n// Both factory functions share this type — the difference is in how TCtx\n// is resolved:\n// - createFsm: TCtx inferred from the `context` property\n// - createBehavioralFsm: TCtx is the client type (context property ignored)\n// -----------------------------------------------------------------------------\n\n/**\n * Configuration object for creating an FSM.\n *\n * @typeParam TCtx - The context type (Fsm) or client type (BehavioralFsm).\n * For Fsm, this is inferred from the `context` property. For BehavioralFsm,\n * it's the client object type provided explicitly or as a generic parameter.\n *\n * @typeParam TStates - The literal states object type. Captured by the factory\n * function's generic parameter (ideally with `const` to preserve string\n * literal types). Defaults to a loose record for unconstrained usage.\n *\n * @example\n * ```ts\n * // TCtx inferred as { tickCount: number }, TStates inferred from states object:\n * createFsm({\n * id: \"traffic-light\",\n * initialState: \"green\", // validated against state keys\n * context: { tickCount: 0 }, // inference site for TCtx\n * states: {\n * green: { timeout: \"yellow\" }, // \"yellow\" validated against state keys\n * yellow: { timeout: \"red\" },\n * red: { timeout: \"green\" },\n * },\n * });\n * ```\n */\nexport interface FsmConfig<\n TCtx,\n TStates extends Record<string, Record<string, unknown>> = Record<\n string,\n Record<string, unknown>\n >,\n> {\n /** Unique identifier for this FSM */\n id: string;\n\n /**\n * The state to start in. Must be a key of `states`.\n *\n * Wrapped in NoInfer to prevent TypeScript from using this value as an\n * inference site for TStates. Without it, `initialState: \"green\"` could\n * narrow TStates to only have a \"green\" key. We want inference to come\n * exclusively from the `states` property.\n */\n initialState: NoInfer<keyof TStates & string>;\n\n /**\n * Initial context data. The type is inferred from this value and flows\n * into every handler's `ctx` parameter.\n *\n * For BehavioralFsm, this property is optional and serves only as a\n * type constraint — the client object IS the context.\n */\n context?: TCtx;\n\n /** State definitions. Keys become the state name union. */\n states: ValidateStates<TCtx, TStates>;\n}\n\n// -----------------------------------------------------------------------------\n// Type discriminant — runtime distinguishing of Fsm vs BehavioralFsm\n//\n// Used internally by the ChildLink adapter to normalize delegation calls\n// without introducing circular imports. Not exported to users.\n// -----------------------------------------------------------------------------\n\n/**\n * Symbol used as a property key to identify machina FSM instances at runtime.\n * Each class stamps itself with a MachinaType value so the ChildLink adapter\n * can dispatch handle()/canHandle()/reset() correctly without circular imports.\n */\nexport const MACHINA_TYPE = Symbol(\"machina.type\");\n\n/**\n * Discriminant values stamped onto FSM instances via `MACHINA_TYPE`.\n * Used by the `ChildLink` adapter to dispatch calls correctly without\n * importing either class directly (which would create circular dependencies).\n */\nexport type MachinaType = \"Fsm\" | \"BehavioralFsm\";\n\n/** Structural type matching any machina FSM instance (Fsm or BehavioralFsm) */\nexport type MachinaInstance = { readonly [MACHINA_TYPE]: MachinaType };\n\n// -----------------------------------------------------------------------------\n// ChildLink — internal adapter interface\n//\n// Normalizes Fsm (no client arg) vs BehavioralFsm (client arg) delegation.\n// The BehavioralFsm engine uses this internally; never exported to users.\n// -----------------------------------------------------------------------------\n\n/**\n * Internal adapter that wraps either an Fsm or BehavioralFsm child,\n * presenting a uniform API for parent-initiated delegation.\n */\nexport interface ChildLink {\n /** Check if the child's current state can handle this input */\n canHandle(client: object, inputName: string): boolean;\n /** Dispatch the input to the child */\n handle(client: object, inputName: string, ...args: unknown[]): void;\n /** Reset the child to its initialState */\n reset(client: object): void;\n /** Subscribe to all child events (wildcard). Returns unsubscribe fn. */\n onAny(callback: (eventName: string, data: unknown) => void): { off(): void };\n /** The child FSM's compositeState for the given client */\n compositeState(client: object): string;\n /** Dispose the child FSM */\n dispose(): void;\n /**\n * The raw Fsm or BehavioralFsm instance this ChildLink wraps.\n * Exposed for inspection tooling (machina-inspect) — allows external\n * tools to introspect child graph structure without reaching through\n * private fields.\n */\n instance: MachinaInstance;\n}\n\n/**\n * Options for FSM disposal.\n */\nexport interface DisposeOptions {\n /**\n * When true, child FSMs declared via _child are NOT disposed.\n * Default: false (children ARE disposed along with the parent).\n */\n preserveChildren?: boolean;\n}\n\n// -----------------------------------------------------------------------------\n// Built-in FSM event maps\n//\n// machina FSMs emit lifecycle events that external code can subscribe to.\n// These types define the payload shape for each built-in event. Custom\n// events (emitted via emit() in handlers) are untyped — they flow through\n// the emitter's wildcard path with `unknown` payloads.\n//\n// Event naming follows a grammatical pattern:\n// - Present participle = \"about to happen\": transitioning, handling\n// - Past participle = \"just happened\": transitioned, handled\n// -----------------------------------------------------------------------------\n\n/**\n * Built-in event map for Fsm instances.\n * Payloads do NOT include a client reference (Fsm is its own client).\n *\n * @typeParam TStateNames - The state name union, flows into transition\n * event payloads so fromState/toState are narrowed to actual state names.\n */\nexport interface FsmEventMap<TStateNames extends string = string> {\n /** Fired just before a state transition occurs */\n transitioning: { fromState: TStateNames; toState: TStateNames };\n\n /** Fired just after a state transition completes */\n transitioned: { fromState: TStateNames; toState: TStateNames };\n\n /** Fired when an input is about to be dispatched to a handler */\n handling: { inputName: string };\n\n /** Fired after an input has been successfully handled */\n handled: { inputName: string };\n\n /** Fired when an input has no matching handler in the current state */\n nohandler: { inputName: string; args: unknown[] };\n\n /** Fired when a transition targets a state that doesn't exist */\n invalidstate: { stateName: string };\n\n /** Fired when an input is deferred for later replay */\n deferred: { inputName: string };\n}\n\n/**\n * Built-in event map for BehavioralFsm instances.\n * Every payload is intersected with `{ client: TClient }` so subscribers\n * can identify which client the event pertains to.\n *\n * @typeParam TClient - The client object type\n * @typeParam TStateNames - The state name union\n */\nexport type BehavioralFsmEventMap<TClient, TStateNames extends string = string> = {\n [K in keyof FsmEventMap<TStateNames>]: FsmEventMap<TStateNames>[K] & {\n client: TClient;\n };\n};\n\n// -----------------------------------------------------------------------------\n// Internal per-client metadata\n//\n// BehavioralFsm tracks per-client state in a WeakMap<TClient, ClientMeta>.\n// When the client object is garbage collected, its metadata goes with it.\n// This replaces v5's __machina__ property stamping on client objects.\n// -----------------------------------------------------------------------------\n\n/**\n * A deferred input queue entry. Created when a handler calls\n * deferUntilTransition() — the input is stored here and replayed\n * after a future state transition.\n */\nexport interface DeferredInput {\n /** The input name that was deferred */\n inputName: string;\n\n /** The original arguments passed to handle() for this input */\n args: unknown[];\n\n /**\n * If set, only replay when entering this specific state.\n * When undefined, replays on the next transition to any state.\n */\n untilState?: string;\n}\n\n/**\n * Internal metadata stored per client in the WeakMap.\n * This is the bookkeeping the FSM engine needs — NOT user-facing data.\n *\n * @typeParam TStateNames - The state name union. The `state` field is\n * typed to this so internal code gets compile-time validation.\n */\nexport interface ClientMeta<TStateNames extends string = string> {\n /** The client's current state */\n state: TStateNames;\n\n /** Queue of inputs deferred for later replay */\n deferredQueue: DeferredInput[];\n\n /**\n * The args array for the currently-executing handle() call.\n * Captured before handler dispatch so that deferUntilTransition()\n * can snapshot them for later replay. Cleared after dispatch.\n */\n currentActionArgs?: unknown[];\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// =============================================================================\n// behavioral-fsm.ts — The core FSM engine\n//\n// BehavioralFsm defines behavior (states + transitions) while tracking state\n// externally per-client via a WeakMap. One FSM definition can drive any number\n// of independent client objects.\n//\n// =============================================================================\n\nimport { Emitter, type Subscription } from \"./emitter\";\nimport {\n MACHINA_TYPE,\n type FsmConfig,\n type StateNamesOf,\n type InputNamesOf,\n type HandlerArgs,\n type HandlerFn,\n type BehavioralFsmEventMap,\n type ClientMeta,\n type DeferredInput,\n type ChildLink,\n type DisposeOptions,\n} from \"./types\";\n\n// Safety valve for _onEnter → transition loops. Instance-level counter works\n// because the engine is synchronous — only one transition chain is active at\n// a time. Throws on overflow, then resets so the FSM isn't permanently broken.\nconst MAX_TRANSITION_DEPTH = 20;\n\n/**\n * Defines FSM behavior (states + transitions) while tracking per-client state\n * in a `WeakMap`. A single `BehavioralFsm` instance can drive any number of\n * independent client objects simultaneously — each gets its own state,\n * deferred queue, and lifecycle.\n *\n * Prefer `createBehavioralFsm()` over constructing this directly — the factory\n * infers all generic parameters from the config object.\n *\n * All public methods silently no-op after `dispose()` is called.\n *\n * @typeParam TClient - The client object type. Must be an object (non-primitive)\n * so it can serve as a WeakMap key.\n * @typeParam TStateNames - String literal union of valid state names.\n * @typeParam TInputNames - String literal union of valid input names.\n */\nexport class BehavioralFsm<\n TClient extends object,\n TStateNames extends string,\n TInputNames extends string,\n> {\n readonly id: string;\n readonly initialState: TStateNames;\n // Type discriminant — lets ChildLink adapter identify this at runtime\n readonly [MACHINA_TYPE] = \"BehavioralFsm\" as const;\n // Public so inspection tooling (machina-inspect) can read the state graph\n // without private field access. The states object is mutated by wrapChildLinks()\n // at construction time — _child values become ChildLink wrappers in-place.\n readonly states: Record<string, Record<string, unknown>>;\n private readonly emitter = new Emitter<BehavioralFsmEventMap<TClient, TStateNames>>();\n private readonly clients = new WeakMap<TClient, ClientMeta<TStateNames>>();\n // Tracks all initialized clients so the Fsm-child nohandler listener can\n // find which clients need bubbling (Fsm events have no client in payload).\n // WeakRef prevents retention; clients clean themselves up naturally.\n private readonly knownClients: Set<WeakRef<TClient>> = new Set();\n // Subscriptions to child FSM wildcard events, keyed by state name.\n // Set up once during construction, torn down in dispose().\n private readonly childSubscriptions: Array<{ off(): void }> = [];\n private disposed = false;\n private transitionDepth = 0;\n\n constructor(config: FsmConfig<TClient, Record<string, Record<string, unknown>>>) {\n this.id = config.id;\n this.initialState = config.initialState as TStateNames;\n this.states = config.states as Record<string, Record<string, unknown>>;\n this.wrapChildLinks();\n this.setupChildSubscriptions();\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n /**\n * Dispatch an input to the given client's current state handler.\n *\n * Delegation order: if the current state has a `_child` FSM that can\n * handle the input, it is dispatched there. If the child emits `nohandler`,\n * the input bubbles up to this FSM's local handler. If no handler exists\n * here either, `nohandler` is emitted on this FSM's emitter.\n *\n * No-ops silently when disposed.\n */\n handle(client: TClient, inputName: TInputNames, ...args: unknown[]): void {\n if (this.disposed) {\n return;\n }\n\n const meta = this.getOrCreateClientMeta(client);\n meta.currentActionArgs = args;\n\n // Delegation: if the current state has a _child and the child can\n // handle this input, send it there. Otherwise fall through to\n // handleLocally(). No nohandler emitted for the delegation path.\n const stateObj = this.states[meta.state];\n const childLink = stateObj?._child as ChildLink | undefined;\n if (childLink) {\n if (childLink.canHandle(client, inputName)) {\n try {\n childLink.handle(client, inputName, ...args);\n } finally {\n // Clear args even if the child handler throws — stale args\n // on meta would corrupt subsequent handle() calls for this client.\n meta.currentActionArgs = undefined;\n }\n return;\n }\n }\n\n this.handleLocally(client, inputName, args, meta);\n }\n\n /**\n * Returns true if the client's current state has a handler for `inputName`\n * (or a catch-all `\"*\"` handler). Does NOT initialize the client — no\n * `_onEnter`, no events, no side effects. Unseen clients are treated as\n * if they were already in `initialState`. Returns false when disposed.\n */\n canHandle(client: TClient, inputName: string): boolean {\n if (this.disposed) {\n return false;\n }\n // Does NOT initialize the client. We want a pure state check —\n // no side effects, no _onEnter, no events. Use initialState as the\n // fallback for unseen clients (they'd start there anyway).\n const state = this.clients.get(client)?.state ?? this.initialState;\n const stateObj = this.states[state];\n return !!(stateObj?.[inputName] ?? stateObj?.[\"*\"]);\n }\n\n /**\n * Transition the client back to `initialState`, firing `_onEnter` and\n * lifecycle events as if entering it fresh. No-ops when disposed.\n */\n reset(client: TClient): void {\n if (this.disposed) {\n return;\n }\n this.transition(client, this.initialState);\n }\n\n /**\n * Returns the client's current state, or `undefined` if the client has\n * never been initialized (i.e. `handle()`, `transition()`, or `reset()`\n * have never been called for it). Does NOT trigger initialization.\n */\n currentState(client: TClient): TStateNames | undefined {\n // Intentionally uses WeakMap.get() directly — does NOT trigger initialization.\n // Returns undefined for clients the FSM has never seen.\n return this.clients.get(client)?.state;\n }\n\n /**\n * Directly transition `client` to `toState`, running the full lifecycle:\n * `_onExit` for the current state → `transitioning` event → update state →\n * `_onEnter` for new state → `transitioned` event → child reset → deferred\n * queue replay → bounce (if `_onEnter` returned a state name).\n *\n * Same-state transitions are silently ignored. Transitions to unknown state\n * names emit `invalidstate` instead of throwing. Throws if the transition\n * depth exceeds `MAX_TRANSITION_DEPTH` (likely an `_onEnter` → transition loop).\n *\n * No-ops when disposed.\n */\n transition(client: TClient, toState: TStateNames): void {\n if (this.disposed) {\n return;\n }\n\n const meta = this.getOrCreateClientMeta(client);\n const fromState = meta.state;\n\n if (toState === fromState) {\n return;\n }\n\n if (!(toState in this.states)) {\n this.emitter.emit(\"invalidstate\", { stateName: toState, client });\n return;\n }\n\n this.transitionDepth++;\n if (this.transitionDepth > MAX_TRANSITION_DEPTH) {\n this.transitionDepth = 0;\n throw new Error(\n `Max transition depth (${MAX_TRANSITION_DEPTH}) exceeded in FSM \"${this.id}\". ` +\n \"Likely an infinite _onEnter → transition loop.\"\n );\n }\n\n try {\n const curStateObj = this.states[fromState];\n const newStateObj = this.states[toState];\n\n // _onExit for current state. The optional chain handles two cases:\n // 1. First-time initialization — fromState is `undefined as TStateNames`,\n // so this.states[undefined] is undefined; no exit hook to run.\n // 2. States that simply have no _onExit defined — equally valid, equally ignored.\n if (curStateObj?._onExit && typeof curStateObj._onExit === \"function\") {\n const exitArgs = this.buildHandlerArgs(client, \"\", meta);\n (curStateObj._onExit as HandlerFn<TClient, TStateNames>)(exitArgs);\n // _onExit return value is intentionally ignored — you're already leaving\n }\n\n meta.state = toState;\n\n const payload = { fromState, toState, client };\n this.emitter.emit(\"transitioning\", payload);\n\n // _onEnter for new state — return value is a bounce target\n let bounceTarget: TStateNames | void = undefined;\n if (newStateObj?._onEnter && typeof newStateObj._onEnter === \"function\") {\n const enterArgs = this.buildHandlerArgs(client, \"\", meta);\n bounceTarget = (newStateObj._onEnter as HandlerFn<TClient, TStateNames>)(enterArgs);\n }\n\n this.emitter.emit(\"transitioned\", payload);\n\n // Reset child FSM after entering the new state. This ensures the child\n // always starts from its initialState when the parent enters. Happens\n // after _onEnter and transitioned but before deferred queue processing,\n // so deferred inputs see the post-reset child state.\n const childLink = newStateObj?._child as ChildLink | undefined;\n if (childLink) {\n childLink.reset(client);\n }\n\n // Replay deferred inputs targeting this state\n this.processQueue(client, meta);\n\n // Bounce: _onEnter returned a state name. Only fires if we're still\n // in the state _onEnter belongs to — a deferred replay might have\n // already moved us elsewhere.\n if (typeof bounceTarget === \"string\" && meta.state === toState) {\n this.transition(client, bounceTarget);\n }\n } finally {\n this.transitionDepth--;\n }\n }\n\n /**\n * Returns the client's state as a dot-delimited path including any active\n * child FSM states (e.g. `\"active.connecting.retrying\"`). Returns just the\n * current state name when no child is active. Returns `\"\"` for clients that\n * have never been initialized (unlike `currentState()` which returns `undefined`).\n */\n compositeState(client: TClient): string {\n const meta = this.clients.get(client);\n if (!meta) {\n // Returns \"\" (not undefined like currentState()) because compositeState\n // produces dot-delimited paths (\"stateA.child.grandchild\") for hierarchies.\n // Empty string is the correct \"nothing to report\" sentinel for string concat.\n return \"\";\n }\n\n const stateObj = this.states[meta.state];\n const childLink = stateObj?._child as ChildLink | undefined;\n if (childLink) {\n const childComposite = childLink.compositeState(client);\n if (childComposite) {\n return `${meta.state}.${childComposite}`;\n }\n }\n\n return meta.state;\n }\n\n /**\n * Subscribe to a built-in lifecycle event or the wildcard.\n *\n * Named overload: typed payload includes `{ client: TClient }` so you can\n * identify which client the event pertains to. Wildcard (`\"*\"`) receives\n * `(eventName, data)` for every event. Returns a no-op `Subscription`\n * when disposed.\n */\n on<K extends keyof BehavioralFsmEventMap<TClient, TStateNames> & string>(\n eventName: K,\n callback: (data: BehavioralFsmEventMap<TClient, TStateNames>[K]) => void\n ): Subscription;\n on(eventName: \"*\", callback: (eventName: string, data: unknown) => void): Subscription;\n on(eventName: string, callback: (...args: any[]) => void): Subscription {\n if (this.disposed) {\n return { off() {} };\n }\n return this.emitter.on(eventName as any, callback as any);\n }\n\n /**\n * Emit a custom event through the FSM. Built-in lifecycle events are\n * emitted automatically — this is for user-defined events from handlers.\n * No-ops when disposed.\n */\n emit(eventName: string, data?: unknown): void {\n if (this.disposed) {\n return;\n }\n // Public emit is for user-defined custom events. Built-in events are\n // emitted automatically by the engine.\n (this.emitter as any).emit(eventName, data);\n }\n\n /**\n * Permanently shut down this FSM. Irreversible — all subsequent method\n * calls become silent no-ops. Tears down child subscriptions, clears all\n * listeners, and cascades disposal to child FSMs (unless `preserveChildren`\n * is set). The same child appearing in multiple states is disposed once.\n */\n dispose(options?: DisposeOptions): void {\n this.disposed = true;\n // Tear down all child wildcard subscriptions before clearing our emitter.\n for (const sub of this.childSubscriptions) {\n sub.off();\n }\n // Cascade disposal to child FSMs unless explicitly opted out.\n // Deduplication via Set handles the same child appearing in multiple states.\n if (!options?.preserveChildren) {\n const seen = new Set<ChildLink>();\n for (const stateName of Object.keys(this.states)) {\n const childLink = this.states[stateName]?._child as ChildLink | undefined;\n if (childLink && !seen.has(childLink)) {\n seen.add(childLink);\n childLink.dispose();\n }\n }\n }\n this.emitter.clear();\n // WeakMap entries are GC'd naturally — can't iterate, don't need to\n }\n\n // -------------------------------------------------------------------------\n // Internal\n // -------------------------------------------------------------------------\n\n /**\n * Walks all states at construction time, detects raw FSM instances assigned\n * to _child, and wraps them into ChildLink adapters via createChildLink().\n * Must run BEFORE setupChildSubscriptions() so the subscriptions see\n * ChildLink objects, not raw FSM instances.\n */\n private wrapChildLinks(): void {\n for (const stateName of Object.keys(this.states)) {\n const stateObj = this.states[stateName];\n const rawChild = stateObj?._child;\n if (!rawChild) {\n continue;\n }\n if (typeof rawChild !== \"object\") {\n throw new Error(\n `State \"${stateName}\"._child: expected an Fsm or BehavioralFsm instance, got ${String(rawChild)}`\n );\n }\n if (!(MACHINA_TYPE in rawChild)) {\n throw new Error(\n `State \"${stateName}\"._child: expected an Fsm or BehavioralFsm instance, got a plain object`\n );\n }\n stateObj._child = createChildLink(rawChild);\n }\n }\n\n /**\n * Walks all states at construction time, finds states with _child, and\n * subscribes once to each unique child's wildcard events. Subscriptions are\n * stored for cleanup in dispose(). We deduplicate by child reference to\n * avoid double-subscribing when the same child appears in multiple states.\n */\n private setupChildSubscriptions(): void {\n const seenChildren = new Set<ChildLink>();\n\n for (const stateName of Object.keys(this.states)) {\n const stateObj = this.states[stateName];\n const childLink = stateObj?._child as ChildLink | undefined;\n\n if (!childLink || seenChildren.has(childLink)) {\n continue;\n }\n seenChildren.add(childLink);\n\n // Subscribe to all child events. We use the wildcard so we get\n // every event type in a single listener.\n const sub = childLink.onAny((eventName: string, data: unknown) => {\n // Nohandler from child = child couldn't handle the input.\n // Re-dispatch to parent via handleLocally() (not handle(), to\n // avoid re-entering the delegation path and looping).\n if (eventName === \"nohandler\") {\n const payload = data as {\n inputName: string;\n args: unknown[];\n client?: TClient;\n };\n\n if (payload.client !== undefined) {\n // BehavioralFsm child: payload includes the client.\n // Filter stale events: only bubble if this client is\n // currently in a state that has this child.\n this.bubbleNohandler(\n payload.client,\n childLink,\n payload.inputName,\n payload.args ?? []\n );\n } else {\n // Fsm child: single-client, no client in payload.\n // Walk all known clients and bubble for any that are\n // currently in a state with this child.\n for (const ref of this.knownClients) {\n const client = ref.deref();\n if (client === undefined) {\n // GC'd — clean up the dead ref\n this.knownClients.delete(ref);\n continue;\n }\n this.bubbleNohandler(\n client,\n childLink,\n payload.inputName,\n payload.args ?? []\n );\n }\n }\n return;\n }\n\n // All other child events: only relay if at least one client\n // is currently in a parent state that delegates to this child.\n // Prevents stale events from leaking through the parent emitter\n // when the child fires during construction or after the parent\n // has moved to a different state.\n const payload = data as Record<string, unknown> | undefined;\n if (payload && typeof payload === \"object\" && \"client\" in payload) {\n // BehavioralFsm child: payload includes the client.\n if (this.isChildActiveForClient(payload.client as TClient, childLink)) {\n (this.emitter as any).emit(eventName, data);\n }\n } else {\n // Fsm child (or payload without client): walk known clients,\n // relay if ANY client is currently in a state with this child.\n for (const ref of this.knownClients) {\n const client = ref.deref();\n if (!client) {\n this.knownClients.delete(ref);\n continue;\n }\n if (this.isChildActiveForClient(client, childLink)) {\n (this.emitter as any).emit(eventName, data);\n break; // one match is enough — relay once\n }\n }\n }\n });\n\n this.childSubscriptions.push(sub);\n }\n }\n\n /**\n * Bubbles a child nohandler to the parent for the given client.\n * Only fires if the client is currently in a state that has this childLink.\n * Extracted from the lambda in setupChildSubscriptions to keep it readable.\n */\n private bubbleNohandler(\n client: TClient,\n childLink: ChildLink,\n inputName: string,\n args: unknown[]\n ): void {\n if (!this.isChildActiveForClient(client, childLink)) {\n return;\n }\n // Safe to assert — isChildActiveForClient confirmed meta exists AND\n // the client is in a state whose _child matches childLink.\n const meta = this.clients.get(client)!;\n meta.currentActionArgs = args;\n this.handleLocally(client, inputName as TInputNames, args, meta);\n }\n\n /**\n * Returns true if the given client is currently in a parent state whose\n * _child is the specified childLink. Returns false if the client has no\n * meta (never initialized) or is in a state with a different (or no) child.\n */\n private isChildActiveForClient(client: TClient, childLink: ChildLink): boolean {\n const meta = this.clients.get(client);\n if (!meta) {\n return false;\n }\n const parentStateObj = this.states[meta.state];\n return parentStateObj?._child === childLink;\n }\n\n /**\n * The inner handler dispatch — no delegation, no initialization side effects\n * beyond what getOrCreateClientMeta already did. Called by handle() after\n * the delegation check, and by the nohandler child listener for bubbling.\n */\n private handleLocally(\n client: TClient,\n inputName: TInputNames | string,\n args: unknown[],\n meta: ClientMeta<TStateNames>\n ): void {\n const stateObj = this.states[meta.state];\n const handler = stateObj?.[inputName] ?? stateObj?.[\"*\"];\n\n if (!handler) {\n this.emitter.emit(\"nohandler\", { inputName: inputName as string, args, client });\n meta.currentActionArgs = undefined;\n return;\n }\n\n try {\n this.emitter.emit(\"handling\", { inputName: inputName as string, client });\n\n const handlerArgs = this.buildHandlerArgs(client, inputName as string, meta);\n let targetState: TStateNames | void = undefined;\n\n if (typeof handler === \"string\") {\n targetState = handler as TStateNames;\n } else if (typeof handler === \"function\") {\n targetState = (handler as HandlerFn<TClient, TStateNames>)(handlerArgs, ...args);\n }\n\n this.emitter.emit(\"handled\", { inputName: inputName as string, client });\n\n if (typeof targetState === \"string\") {\n this.transition(client, targetState);\n }\n } finally {\n meta.currentActionArgs = undefined;\n }\n }\n\n private getOrCreateClientMeta(client: TClient): ClientMeta<TStateNames> {\n let meta = this.clients.get(client);\n if (meta) {\n return meta;\n }\n\n // State starts as undefined so transition()'s same-state check passes\n // (undefined !== initialState). The transition immediately sets state\n // to initialState before returning, so any code that triggered\n // initialization (handle/reset/transition) sees the correct state.\n // currentState() for uninitialized clients still returns undefined —\n // it reads the WeakMap directly without triggering init.\n meta = {\n state: undefined as unknown as TStateNames,\n deferredQueue: [],\n };\n this.clients.set(client, meta);\n // Track this client weakly so Fsm-child nohandler listeners can find\n // which clients to bubble to (Fsm events carry no client in payload).\n this.knownClients.add(new WeakRef(client));\n\n // v5-style active initialization: full transition into initialState.\n // Fires _onEnter, emits transitioning/transitioned, processes deferred\n // queue. _onExit is skipped (no state object for undefined).\n this.transition(client, this.initialState);\n\n return meta;\n }\n\n private buildHandlerArgs(\n client: TClient,\n inputName: string,\n meta: ClientMeta<TStateNames>\n ): HandlerArgs<TClient, TStateNames> {\n return {\n ctx: client,\n inputName,\n defer: (opts?: { until: TStateNames }) => {\n // Only meaningful inside a handle() context where currentActionArgs\n // is set. No-ops during lifecycle hooks (matches v5 behavior).\n if (!meta.currentActionArgs) {\n return;\n }\n const deferred: DeferredInput = {\n inputName,\n args: [...meta.currentActionArgs],\n untilState: opts?.until,\n };\n meta.deferredQueue.push(deferred);\n this.emitter.emit(\"deferred\", { inputName, client });\n },\n emit: (evtName: string, evtData?: unknown) => {\n (this.emitter as any).emit(evtName, evtData);\n },\n };\n }\n\n private processQueue(client: TClient, meta: ClientMeta<TStateNames>): void {\n const toReplay: DeferredInput[] = [];\n const remaining: DeferredInput[] = [];\n\n for (const item of meta.deferredQueue) {\n if (item.untilState === undefined || item.untilState === meta.state) {\n toReplay.push(item);\n } else {\n remaining.push(item);\n }\n }\n\n meta.deferredQueue = remaining;\n\n for (const item of toReplay) {\n this.handle(client, item.inputName as TInputNames, ...item.args);\n }\n }\n}\n\n// -----------------------------------------------------------------------------\n// createChildLink — internal adapter factory\n//\n// Wraps either an Fsm or BehavioralFsm child and presents a uniform interface\n// for the parent engine to call. Never exported. The MACHINA_TYPE discriminant\n// tells us which call shape to use.\n// -----------------------------------------------------------------------------\n\n/**\n * Internal factory called by `wrapChildLinks()` during construction.\n * Wraps a raw Fsm or BehavioralFsm instance in a uniform `ChildLink` adapter\n * so the parent engine doesn't need to know which type it's talking to.\n *\n * Users never call this directly — they assign an FSM instance to `_child`\n * in their state config and `wrapChildLinks()` handles the wrapping.\n */\nfunction createChildLink(child: any): ChildLink {\n if (!child || typeof child !== \"object\") {\n throw new Error(\n `createChildLink: expected an Fsm or BehavioralFsm instance, got ${String(child)}`\n );\n }\n const childType: string = child[MACHINA_TYPE];\n\n if (childType === \"BehavioralFsm\") {\n return {\n instance: child,\n canHandle(client: object, inputName: string): boolean {\n return child.canHandle(client, inputName);\n },\n handle(client: object, inputName: string, ...args: unknown[]): void {\n child.handle(client, inputName, ...args);\n },\n reset(client: object): void {\n child.transition(client, child.initialState);\n },\n onAny(callback: (eventName: string, data: unknown) => void): { off(): void } {\n return child.on(\"*\", callback);\n },\n compositeState(client: object): string {\n return child.compositeState(client);\n },\n dispose(): void {\n child.dispose();\n },\n };\n }\n\n if (childType === \"Fsm\") {\n return {\n instance: child,\n canHandle(_client: object, inputName: string): boolean {\n return child.canHandle(inputName);\n },\n handle(_client: object, inputName: string, ...args: unknown[]): void {\n child.handle(inputName, ...args);\n },\n reset(_client: object): void {\n child.reset();\n },\n onAny(callback: (eventName: string, data: unknown) => void): { off(): void } {\n return child.on(\"*\", callback);\n },\n compositeState(_client: object): string {\n return child.compositeState();\n },\n dispose(): void {\n child.dispose();\n },\n };\n }\n\n throw new Error(\n `createChildLink: expected an Fsm or BehavioralFsm instance, ` +\n `got [MACHINA_TYPE] = ${String(childType ?? \"undefined\")}`\n );\n}\n\n/**\n * Create a behavioral FSM (one definition, many clients) from a config object.\n *\n * Generic parameters are inferred automatically:\n * - `TClient` must be provided explicitly as a type parameter (it can't be\n * inferred from the config since no `context` property exists at the FSM level).\n * - `TStates` is captured with `const` inference to preserve string literal\n * types, enabling compile-time validation of transition targets and `handle()`\n * input names.\n *\n * State names, input names, and all handler signatures derive from `TStates`.\n *\n * @example\n * ```ts\n * interface Connection { url: string; retries: number; }\n *\n * const connFsm = createBehavioralFsm<Connection>({\n * id: \"connectivity\",\n * initialState: \"disconnected\",\n * states: {\n * disconnected: { connect: \"connecting\" },\n * connecting: { connected: \"online\", failed: \"disconnected\" },\n * online: { disconnect: \"disconnected\" },\n * },\n * });\n *\n * const conn = { url: \"wss://example.com\", retries: 0 };\n * connFsm.handle(conn, \"connect\");\n * ```\n */\nexport function createBehavioralFsm<\n TClient extends object,\n const TStates extends Record<string, Record<string, unknown>>,\n>(\n config: FsmConfig<TClient, TStates>\n): BehavioralFsm<TClient, StateNamesOf<TStates>, InputNamesOf<TStates>> {\n return new BehavioralFsm(config as FsmConfig<TClient, Record<string, Record<string, unknown>>>);\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// =============================================================================\n// fsm.ts — Single-client FSM convenience wrapper\n//\n// Fsm wraps a BehavioralFsm, using the config's `context` object as the\n// internal client. Same engine, same lifecycle, but without the client\n// parameter on every method call.\n//\n// =============================================================================\n\nimport { Emitter, type Subscription } from \"./emitter\";\nimport { BehavioralFsm } from \"./behavioral-fsm\";\nimport {\n MACHINA_TYPE,\n type FsmConfig,\n type FsmEventMap,\n type StateNamesOf,\n type InputNamesOf,\n type DisposeOptions,\n} from \"./types\";\n\n/**\n * Single-client FSM. Wraps a BehavioralFsm and uses the config's `context`\n * object as the implicit client, so callers never pass a client argument.\n *\n * Prefer `createFsm()` over constructing this directly — the factory infers\n * all generic parameters from the config object.\n *\n * All public methods silently no-op after `dispose()` is called.\n *\n * @typeParam TCtx - The context type, inferred from `config.context`.\n * @typeParam TStateNames - String literal union of valid state names.\n * @typeParam TInputNames - String literal union of valid input names.\n */\nexport class Fsm<TCtx extends object, TStateNames extends string, TInputNames extends string> {\n readonly id: string;\n readonly initialState: TStateNames;\n // Type discriminant — lets ChildLink adapter identify this at runtime\n readonly [MACHINA_TYPE] = \"Fsm\" as const;\n // Public so inspection tooling (machina-inspect) can read the state graph.\n // Set from config.states — same object reference as BehavioralFsm.states,\n // so ChildLink wrapping done by wrapChildLinks() is reflected automatically.\n readonly states: Record<string, Record<string, unknown>>;\n private readonly bfsm: BehavioralFsm<TCtx, TStateNames, TInputNames>;\n // Public readonly so external tooling (and walkAll's invariant) can read\n // context without needing to plumb it through every call site. The context\n // is already visible inside handlers via ctx — this just makes it accessible\n // from outside the FSM instance too.\n readonly context: TCtx;\n // FsmEventMap is an interface, which lacks the implicit index signature\n // that Emitter's Record<string, unknown> constraint needs. The mapped type\n // BehavioralFsmEventMap doesn't have this issue. Intersect with Record to\n // satisfy the constraint while preserving typed event signatures.\n private readonly emitter = new Emitter<FsmEventMap<TStateNames> & Record<string, unknown>>();\n private disposed = false;\n\n constructor(config: FsmConfig<TCtx, Record<string, Record<string, unknown>>>) {\n this.id = config.id;\n this.initialState = config.initialState as TStateNames;\n this.context = (config.context ?? {}) as TCtx;\n\n this.bfsm = new BehavioralFsm(config);\n // Assign after bfsm construction so ChildLink wrapping (done by\n // BehavioralFsm.wrapChildLinks) is already reflected in config.states.\n this.states = config.states as Record<string, Record<string, unknown>>;\n\n // Relay events from BehavioralFsm → Fsm emitter, stripping the\n // `client` field from built-in event payloads. Custom events (from\n // handler emit() or public emit()) pass through unchanged.\n this.bfsm.on(\"*\", (eventName: string, data: unknown) => {\n if (data && typeof data === \"object\" && \"client\" in data) {\n const { client: _, ...payload } = data as Record<string, unknown>;\n (this.emitter as any).emit(eventName, payload);\n } else {\n (this.emitter as any).emit(eventName, data);\n }\n });\n\n // Eager init: call bfsm.transition() to enter initialState now.\n // bfsm.transition() calls getOrCreateClientMeta(), which bootstraps\n // the client and calls this.transition() internally (the real init,\n // which fires _onEnter and emits transitioning/transitioned). After\n // that inner call completes, meta.state === initialState, so the\n // original bfsm.transition() call hits the same-state guard and exits.\n this.bfsm.transition(this.context, config.initialState as TStateNames);\n }\n\n // -------------------------------------------------------------------------\n // Public API — same as BehavioralFsm, minus the client parameter\n // -------------------------------------------------------------------------\n\n /**\n * Dispatch an input to the current state's handler.\n * If a `_child` FSM in the current state can handle it, delegation occurs\n * there first; unhandled inputs bubble up to the parent.\n * No-ops silently when disposed.\n */\n handle(inputName: TInputNames, ...args: unknown[]): void {\n if (this.disposed) {\n return;\n }\n this.bfsm.handle(this.context, inputName, ...args);\n }\n\n /**\n * Returns true if the current state has a handler for `inputName`\n * (or a catch-all `\"*\"` handler). Does not trigger initialization\n * or any side effects. Returns false when disposed.\n */\n canHandle(inputName: string): boolean {\n if (this.disposed) {\n return false;\n }\n return this.bfsm.canHandle(this.context, inputName);\n }\n\n /**\n * Transition back to `initialState`, firing `_onEnter` and lifecycle\n * events as if entering it fresh. No-ops silently when disposed.\n */\n reset(): void {\n if (this.disposed) {\n return;\n }\n this.bfsm.reset(this.context);\n }\n\n /**\n * Returns the current state name. Always defined — Fsm eagerly\n * initializes into `initialState` during construction.\n */\n currentState(): TStateNames {\n return this.bfsm.currentState(this.context) as TStateNames;\n }\n\n /**\n * Directly transition to `toState`, firing `_onExit`, `_onEnter`, and\n * lifecycle events. Same-state transitions are silently ignored.\n * No-ops when disposed.\n */\n transition(toState: TStateNames): void {\n if (this.disposed) {\n return;\n }\n this.bfsm.transition(this.context, toState);\n }\n\n /**\n * Returns the current state as a dot-delimited path that includes\n * any active child FSM states (e.g. `\"active.connecting.retrying\"`).\n * Returns just the current state name when no child is active.\n */\n compositeState(): string {\n return this.bfsm.compositeState(this.context);\n }\n\n /**\n * Subscribe to a built-in lifecycle event or the wildcard.\n *\n * Named overload: typed payload, no event name in callback.\n * Wildcard (`\"*\"`): receives `(eventName, data)` for every event.\n * Returns a no-op `Subscription` when disposed.\n */\n on<K extends keyof FsmEventMap<TStateNames> & string>(\n eventName: K,\n callback: (data: FsmEventMap<TStateNames>[K]) => void\n ): Subscription;\n on(eventName: \"*\", callback: (eventName: string, data: unknown) => void): Subscription;\n on(eventName: string, callback: (...args: any[]) => void): Subscription {\n if (this.disposed) {\n return { off() {} };\n }\n return this.emitter.on(eventName as any, callback as any);\n }\n\n /**\n * Emit a custom event through the FSM. Built-in lifecycle events are\n * emitted automatically — this is for user-defined events from handlers.\n * Routes through the BehavioralFsm so all relay paths are consistent.\n * No-ops when disposed.\n */\n emit(eventName: string, data?: unknown): void {\n if (this.disposed) {\n return;\n }\n // Route through BehavioralFsm so custom events from handler emit()\n // and public fsm.emit() take the same path through the relay.\n this.bfsm.emit(eventName, data);\n }\n\n /**\n * Permanently shut down this FSM. Irreversible — all subsequent method\n * calls become silent no-ops. Clears all listeners and cascades disposal\n * to child FSMs (unless `preserveChildren` is set).\n */\n dispose(options?: DisposeOptions): void {\n this.disposed = true;\n this.bfsm.dispose(options);\n this.emitter.clear();\n }\n}\n\n/**\n * Create a single-client FSM from a config object.\n *\n * Generic parameters are inferred automatically:\n * - `TCtx` comes from `config.context` (defaults to `{}` if omitted).\n * - `TStates` is captured with `const` inference to preserve string literal\n * types, enabling compile-time validation of transition targets and `handle()`\n * input names.\n *\n * State names, input names, and all handler signatures derive from `TStates`.\n *\n * @example\n * ```ts\n * const light = createFsm({\n * id: \"traffic-light\",\n * initialState: \"green\",\n * context: { tickCount: 0 },\n * states: {\n * green: { timeout: \"yellow\" },\n * yellow: { timeout: \"red\" },\n * red: { timeout: \"green\" },\n * },\n * });\n *\n * light.handle(\"timeout\"); // transitions green → yellow\n * ```\n */\nexport function createFsm<\n TCtx extends object = Record<string, never>,\n const TStates extends Record<string, Record<string, unknown>> = Record<\n string,\n Record<string, unknown>\n >,\n>(config: FsmConfig<TCtx, TStates>): Fsm<TCtx, StateNamesOf<TStates>, InputNamesOf<TStates>> {\n return new Fsm(config as FsmConfig<TCtx, Record<string, Record<string, unknown>>>);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/emitter.ts","../src/json-safe.ts","../src/types.ts","../src/behavioral-fsm.ts","../src/fsm.ts"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Listener = (...args: any[]) => void;\ntype WildcardListener = (eventName: string, data: unknown) => void;\n\n/**\n * Returned by `on()`. Call `off()` to remove the subscription.\n * Safe to call multiple times — subsequent calls are no-ops because\n * the underlying Set deduplicates membership.\n */\nexport type Subscription = { off(): void };\n\n/**\n * Minimal typed event emitter used internally by Fsm and BehavioralFsm.\n *\n * Two listener categories exist at runtime: named listeners (keyed by event\n * name) and wildcard listeners (keyed as `\"*\"`). The wildcard fires on EVERY\n * emit, regardless of event name, and its callback receives both the event\n * name and the data. Named listeners receive only the data.\n *\n * @typeParam TEventMap - Record mapping event names to their payload types.\n * Constrains `on()` and `emit()` to matching event/payload pairs.\n */\nexport class Emitter<TEventMap extends Record<string, unknown>> {\n private listeners = new Map<string, Set<Listener>>();\n\n /**\n * Subscribe to a named event or the wildcard.\n *\n * Named overload: callback receives only the typed payload.\n * Wildcard overload (`\"*\"`): callback receives `(eventName, data)` — useful\n * for proxying all events to another emitter, as Fsm does with its bfsm.\n *\n * Wildcard listeners fire BEFORE named listeners. If a wildcard listener\n * removes a named listener via `off()`, the named listener still fires for\n * the current emit — Set iteration is not affected by concurrent deletes.\n *\n * Returns a `Subscription` whose `off()` removes this specific listener.\n */\n on<K extends keyof TEventMap & string>(\n event: K,\n cb: (data: TEventMap[K]) => void\n ): Subscription;\n on(event: \"*\", cb: WildcardListener): Subscription;\n on(event: string, cb: Listener): Subscription {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(cb);\n return {\n off: () => {\n set.delete(cb);\n },\n };\n }\n\n /**\n * Emit a named event, notifying wildcard listeners first, then named listeners.\n *\n * Wildcard listeners receive `(eventName, data)`; named listeners receive\n * only `data`. The firing order (wildcards before named) is intentional —\n * it lets relay listeners (like Fsm's bfsm proxy) observe all events\n * before specific subscribers react.\n */\n emit<K extends keyof TEventMap & string>(event: K, data: TEventMap[K]): void {\n const wildcards = this.listeners.get(\"*\");\n if (wildcards) {\n for (const cb of wildcards) {\n (cb as WildcardListener)(event, data);\n }\n }\n const named = this.listeners.get(event);\n if (named) {\n for (const cb of named) {\n cb(data);\n }\n }\n }\n\n /**\n * Remove all listeners (named and wildcard).\n *\n * Existing `Subscription` objects remain valid — their `off()` closures\n * hold a reference to the now-empty Set, so calling them is harmless.\n * Called by `dispose()` to prevent memory retention after an FSM shuts down.\n */\n clear(): void {\n this.listeners.clear();\n }\n}\n","// =============================================================================\n// json-safe.ts — Recursive walk that validates AND deep-clones plain data.\n//\n// dehydrate() needs a hard guarantee: everything it hands back either survives\n// a serialization boundary intact, or the call throws. JSON.stringify can't be\n// trusted for this — it silently drops functions/undefined and mangles Date\n// into a string. Silent data loss is exactly what dehydrate() exists to\n// prevent, so every unsupported value throws instead of vanishing, and the\n// error names precisely where in the structure it was found.\n// =============================================================================\n\n/**\n * Thrown internally when the walk hits a value that can't survive a\n * serialization boundary. Callers catch this to attach FSM/input context\n * before re-throwing a fully descriptive error — see `path`/`label` below.\n */\nexport class NonSerializableValueError extends Error {\n constructor(\n readonly path: string,\n readonly label: string\n ) {\n super(`non-serializable value at ${path} (${label})`);\n }\n}\n\n/**\n * Deep-clones `value`, throwing `NonSerializableValueError` the moment it\n * finds anything that isn't `null`, a boolean, a finite number, a string, a\n * plain array, or a plain object. `rootPath` seeds the path used in error\n * messages — e.g. `\"args\"` so a nested failure reads as `args[1].onComplete`.\n *\n * Cloning (rather than just validating) is what keeps a returned snapshot\n * from aliasing live FSM state: mutating the snapshot afterward — or the\n * original value passed into a deferred `handle()` call — can't reach back\n * into the FSM's internal deferred queue.\n */\nexport const cloneJsonSafe = (value: unknown, rootPath: string): unknown => {\n return cloneNode(value, rootPath, new Set<object>());\n};\n\nconst cloneNode = (value: unknown, path: string, ancestors: Set<object>): unknown => {\n if (value === null) {\n return null;\n }\n\n switch (typeof value) {\n case \"string\":\n case \"boolean\":\n return value;\n case \"number\":\n if (!Number.isFinite(value)) {\n throw new NonSerializableValueError(path, describeNonFiniteNumber(value));\n }\n return value;\n case \"object\":\n return cloneObject(value as object, path, ancestors);\n default:\n // undefined, function, symbol, bigint — none of these survive JSON.\n throw new NonSerializableValueError(path, typeof value);\n }\n};\n\nconst cloneObject = (obj: object, path: string, ancestors: Set<object>): unknown => {\n // Ancestor stack, not a global \"seen\" set — the same object appearing\n // twice in unrelated branches (a shared reference) is fine to serialize\n // twice. Only a value that contains ITSELF is a cycle.\n if (ancestors.has(obj)) {\n throw new NonSerializableValueError(path, \"circular reference\");\n }\n\n if (Array.isArray(obj)) {\n // A dense, index-only array has exactly one own key per element\n // (\"0\", \"1\", ...). A hole (sparse array) or an extra non-index\n // property (obj.extra = ...) breaks that equality — `.map()` would\n // otherwise skip holes and never visit non-index keys, silently\n // reproducing (for holes) or dropping (for extra keys) data instead\n // of throwing.\n if (obj.length !== Object.keys(obj).length) {\n throw new NonSerializableValueError(\n path,\n \"sparse array or array with non-index properties\"\n );\n }\n ancestors.add(obj);\n const cloned = obj.map((item, i) => cloneNode(item, `${path}[${i}]`, ancestors));\n ancestors.delete(obj);\n return cloned;\n }\n\n // Plain objects only — Date/Map/Set/class instances all have a\n // non-Object prototype and would otherwise be silently flattened to\n // \"{}\" by a naive walk.\n const proto = Object.getPrototypeOf(obj);\n if (proto !== Object.prototype && proto !== null) {\n throw new NonSerializableValueError(\n path,\n (obj as { constructor?: { name?: string } }).constructor?.name ?? \"object\"\n );\n }\n\n ancestors.add(obj);\n const cloned: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n const value = cloneNode((obj as Record<string, unknown>)[key], `${path}.${key}`, ancestors);\n // Object.defineProperty, not `cloned[key] = value` — a key literally\n // named \"__proto__\" (producible via JSON.parse, unlike object-literal\n // syntax) would otherwise invoke Object.prototype's inherited\n // accessor and swap cloned's prototype instead of storing the data.\n Object.defineProperty(cloned, key, {\n value,\n enumerable: true,\n writable: true,\n configurable: true,\n });\n }\n ancestors.delete(obj);\n return cloned;\n};\n\nconst describeNonFiniteNumber = (n: number): string => {\n if (Number.isNaN(n)) {\n return \"NaN\";\n }\n return n > 0 ? \"Infinity\" : \"-Infinity\";\n};\n\n/**\n * Deep-clones `value` WITHOUT validating it — unlike `cloneJsonSafe`, this\n * never throws. Plain objects/arrays are cloned recursively (so a caller\n * can't alias state back into whatever holds the result); anything else\n * (functions, `Date`/`Map`/class instances, symbols, non-finite numbers,\n * etc.) is passed through by reference as-is.\n *\n * This is `rehydrate()`'s side of the aliasing guarantee: `dehydrate()`\n * validates-and-clones on the way OUT (via `cloneJsonSafe`), but `rehydrate()`\n * trusts the snapshot it's given is already valid data — re-validating on\n * the way IN would be a redundant, unwanted asymmetry (see the build plan's\n * disclosed known gaps). We still need the clone so mutating the caller's\n * snapshot object after `rehydrate()` returns can't reach into the live\n * FSM's internal deferred queue.\n *\n * Cycle-safe: a value that contains itself is returned as-is (by reference)\n * rather than cloned infinitely — there's no validation step here to make\n * that throw, so silently keeping the shared reference is the only option\n * that doesn't hang.\n */\nexport const cloneDeep = (value: unknown, ancestors: Set<object> = new Set<object>()): unknown => {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (ancestors.has(value)) {\n return value;\n }\n\n if (Array.isArray(value)) {\n ancestors.add(value);\n const cloned = value.map(item => cloneDeep(item, ancestors));\n ancestors.delete(value);\n return cloned;\n }\n\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) {\n // Not a plain object (Date, Map, class instance, ...) — nothing to\n // safely walk into, and there's no validation step here to reject it.\n return value;\n }\n\n ancestors.add(value);\n const cloned: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n const clonedValue = cloneDeep((value as Record<string, unknown>)[key], ancestors);\n // Same fix as cloneObject's loop in cloneJsonSafe: a key literally\n // named \"__proto__\" would otherwise reproto the clone via bracket\n // assignment instead of being stored as a data property.\n Object.defineProperty(cloned, key, {\n value: clonedValue,\n enumerable: true,\n writable: true,\n configurable: true,\n });\n }\n ancestors.delete(value);\n return cloned;\n};\n","// =============================================================================\n// types.ts — Shared type foundations for machina v6\n//\n// Pure type definitions. No runtime code. Every type here is consumed by\n// behavioral-fsm.ts, fsm.ts, or both. The factory functions (createFsm,\n// createBehavioralFsm) use these as building blocks for their own generics.\n//\n// =============================================================================\n\n// -----------------------------------------------------------------------------\n// Utility types for state/input name extraction\n//\n// These are the inference primitives that power machina's \"zero-ceremony\"\n// type safety. Given a states config object, they extract the state name\n// union and input name union as string literal types.\n// -----------------------------------------------------------------------------\n\n/**\n * Keys on a state object that have special meaning and are NOT input names.\n * Used by InputNamesOf to filter these out when collecting input names.\n *\n * @internal Exported from this module only so the factory return types in\n * fsm.ts / behavioral-fsm.ts can inline InputNamesOf's definition for\n * readable error text; not re-exported via index.ts.\n */\nexport type SpecialStateKeys = \"_onEnter\" | \"_onExit\" | \"_child\" | \"*\";\n\n/**\n * Extracts state names as a string literal union from a states config object.\n *\n * @example\n * ```ts\n * type S = StateNamesOf<{ green: {...}, yellow: {...}, red: {...} }>;\n * // => \"green\" | \"yellow\" | \"red\"\n * ```\n */\nexport type StateNamesOf<TStates> = keyof TStates & string;\n\n/**\n * Extracts a state config's OWN input names — the handler keys declared\n * directly on its states, before any child FSM's inputs are folded in.\n * Collects all handler keys across ALL states, then strips out lifecycle\n * hooks and special keys (_onEnter, _onExit, _child, *).\n *\n * Split out from `InputNamesOf` so the child-coverage machinery (`CoverageOf`)\n * can ask \"what does this FSM handle locally?\" without pulling in every\n * descendant's inputs too — coverage is about what THIS level absorbs, not\n * what its children happen to also expose.\n *\n * @example\n * ```ts\n * type I = OwnInputNamesOf<{\n * idle: { start: \"running\", reset: fn };\n * running: { pause: \"paused\", stop: \"idle\" };\n * }>;\n * // => \"start\" | \"reset\" | \"pause\" | \"stop\"\n * ```\n *\n * How it works:\n * 1. `{ [S in keyof TStates]: keyof TStates[S] & string }` — maps each state\n * to the union of its handler key names\n * 2. `[keyof TStates]` — collapses the mapped type into a flat union of ALL\n * handler keys across every state\n * 3. `Exclude<..., SpecialStateKeys>` — strips lifecycle/special keys\n */\nexport type OwnInputNamesOf<TStates> = Exclude<\n { [S in keyof TStates]: keyof TStates[S] & string }[keyof TStates],\n SpecialStateKeys\n>;\n\n/**\n * Extracts input names as a string literal union from a states config object.\n * This is what flows into `handle(inputName)` to provide compile-time\n * validation of input names.\n *\n * A parent's `handle()` genuinely accepts everything its `_child` FSMs accept:\n * the engine checks the child first and only falls through to the parent's\n * own handlers if the child can't handle the input (see `BehavioralFsm.handle`).\n * `InputNamesOf` mirrors that at the type level by unioning the config's own\n * input names with every declared child's input names (recursively —\n * grandchildren ride along because each child's own `InputNamesOfInstance`\n * already folded in ITS children when that child was created).\n *\n * @example\n * ```ts\n * type I = InputNamesOf<{\n * idle: { start: \"running\", reset: fn };\n * running: { pause: \"paused\", stop: \"idle\" };\n * }>;\n * // => \"start\" | \"reset\" | \"pause\" | \"stop\"\n * ```\n */\nexport type InputNamesOf<TStates> = OwnInputNamesOf<TStates> | ChildInputNamesOf<TStates>;\n\n/**\n * Walks every state looking for a `_child`, and folds in that child instance's\n * own input names (via `InputNamesOfInstance`). Not exported — `InputNamesOf`\n * is the public surface; this is just the \"look inside _child\" half of it.\n */\ntype ChildInputNamesOf<TStates> = {\n [S in keyof TStates]: TStates[S] extends { _child: infer C } ? InputNamesOfInstance<C> : never;\n}[keyof TStates];\n\n/**\n * Extracts state names from a concrete machina FSM instance.\n *\n * Unlike `keyof TFsm`, this reads the class generic that stores user-defined\n * state names, so adapter signatures stay tied to the configured states\n * instead of widening to method/property names.\n */\nexport type StateNamesOfInstance<TFsm> = TFsm extends import(\"./fsm\").Fsm<\n infer _TCtx extends object,\n infer TStateNames extends string,\n infer _TInputNames extends string\n>\n ? TStateNames\n : TFsm extends import(\"./behavioral-fsm\").BehavioralFsm<\n infer _TClient extends object,\n infer TStateNames extends string,\n infer _TInputNames extends string\n >\n ? TStateNames\n : never;\n\n/**\n * Extracts input names from a concrete machina FSM instance.\n */\nexport type InputNamesOfInstance<TFsm> = TFsm extends import(\"./fsm\").Fsm<\n infer _TCtx extends object,\n infer _TStateNames extends string,\n infer TInputNames extends string\n>\n ? TInputNames\n : TFsm extends import(\"./behavioral-fsm\").BehavioralFsm<\n infer _TClient extends object,\n infer _TStateNames extends string,\n infer TInputNames extends string\n >\n ? TInputNames\n : never;\n\n/**\n * Extracts the context object type from a concrete single-client FSM instance.\n */\nexport type ContextOf<TFsm> = TFsm extends import(\"./fsm\").Fsm<\n infer TCtx extends object,\n infer _TStateNames extends string,\n infer _TInputNames extends string\n>\n ? TCtx\n : never;\n\n/**\n * Extracts the client object type from a concrete behavioral FSM instance.\n */\nexport type ClientOf<TFsm> = TFsm extends import(\"./behavioral-fsm\").BehavioralFsm<\n infer TClient extends object,\n infer _TStateNames extends string,\n infer _TInputNames extends string\n>\n ? TClient\n : never;\n\n/**\n * Extracts the declared `bubbles` union from a concrete machina FSM instance —\n * the inputs that FSM fires at itself without handling, expecting a `_child`\n * mount point to catch them (see `ChildCoverage`). `never` for an instance\n * that declared no `bubbles` (the common case) or for a non-machina type.\n */\nexport type BubblesOfInstance<TFsm> = TFsm extends import(\"./fsm\").Fsm<\n infer _TCtx extends object,\n infer _TStateNames extends string,\n infer _TInputNames extends string,\n infer TBubbles extends string\n>\n ? TBubbles\n : TFsm extends import(\"./behavioral-fsm\").BehavioralFsm<\n infer _TClient extends object,\n infer _TStateNames extends string,\n infer _TInputNames extends string,\n infer TBubbles extends string\n >\n ? TBubbles\n : never;\n\n// -----------------------------------------------------------------------------\n// Handler argument object\n//\n// Every handler in machina receives this as its first argument. This replaces\n// the old v5 pattern of binding `this` to the FSM and passing the client as\n// a separate parameter. Benefits:\n// - Works with arrow functions (no `this` caveat)\n// - BehavioralFsm and Fsm handler signatures are identical\n// - Destructure only what you need: tick({ ctx }) { ... }\n// - Easy to mock in tests: just construct the object\n//\n// Handlers RETURN a state name to trigger a transition (or void/undefined\n// to stay in the current state). This replaces the imperative transition()\n// call and mirrors gen_fsm's {next_state, StateName, NewStateData} pattern.\n// -----------------------------------------------------------------------------\n\n/**\n * The single combined object passed to every handler.\n *\n * Handlers return a state name to transition, or void to stay put.\n * This replaces imperative `transition()` calls — closer to gen_fsm's\n * return-based model and symmetrical with string shorthand handlers.\n *\n * @typeParam TCtx - The context type. For Fsm this is the config-defined\n * context object. For BehavioralFsm this is the client object itself.\n * @typeParam TStateNames - String literal union of valid state names.\n * Defaults to `string` for loose usage; the factory functions narrow this\n * to the actual state names inferred from the config.\n *\n * @example\n * ```ts\n * // Conditional transition — return the target state:\n * timeout({ ctx }) {\n * if (ctx.tickCount >= 3) return \"yellow\";\n * }\n *\n * // Side effects without transition — return nothing:\n * tick({ ctx }) {\n * ctx.tickCount++;\n * }\n *\n * // In a catch-all — inputName tells you what was received:\n * \"*\"({ inputName }) {\n * console.log(`unhandled input: ${inputName}`);\n * }\n * ```\n */\nexport interface HandlerArgs<TCtx, TStateNames extends string = string> {\n /** The context (Fsm) or client object (BehavioralFsm) */\n ctx: TCtx;\n\n /**\n * The name of the input currently being handled.\n *\n * Typed as `string` rather than the inferred input union because:\n * 1. Inside a named handler you already know the input name\n * 2. In a catch-all (*) handler it could be anything\n * 3. Narrowing to the literal per-handler would require complex\n * mapped types for zero practical benefit\n */\n inputName: string;\n\n /**\n * Defer the current input for replay after a future transition.\n * Erlang's selective receive, in JS form.\n *\n * @example\n * ```ts\n * // Replay on the next transition to any state\n * defer();\n *\n * // Replay only when entering \"yellow\"\n * defer({ until: \"yellow\" });\n * ```\n */\n defer(opts?: { until: TStateNames }): void;\n\n /**\n * Emit a custom event through the FSM's emitter.\n * Built-in events (transitioning, transitioned, etc.) are emitted\n * automatically by the FSM engine — this is for user-defined events.\n */\n emit(eventName: string, data?: unknown): void;\n}\n\n// -----------------------------------------------------------------------------\n// Handler function type\n//\n// A single callable type for all handler forms: state input handlers,\n// lifecycle hooks (_onEnter, _onExit), and the catch-all (*).\n//\n// Handlers return a state name to trigger a transition, or void/undefined\n// to stay in the current state. This is the dynamic counterpart to string\n// shorthand — same concept (determine next state), two expressions.\n// -----------------------------------------------------------------------------\n\n/**\n * A function handler for state inputs, lifecycle hooks (_onEnter, _onExit),\n * and catch-all (*) handlers.\n *\n * **Return value determines transition:**\n * - Return a valid state name → FSM transitions to that state\n * - Return void/undefined → FSM stays in the current state\n *\n * This mirrors gen_fsm's `{next_state, StateName, NewStateData}` return.\n * Guards are just `if` statements. Actions are just code before the return.\n *\n * The `...extra` rest parameter captures additional arguments passed through\n * `handle(inputName, ...extraArgs)`. These are untyped (`unknown[]`) because\n * correlating per-input arg types with handle() call sites would require\n * prohibitively complex mapped types for minimal benefit.\n *\n * @example\n * ```ts\n * // Side effects only, no transition:\n * tick({ ctx }) { ctx.tickCount++; }\n *\n * // Conditional transition (replaces guard + target):\n * timeout({ ctx }) {\n * if (ctx.tickCount >= 3) return \"yellow\";\n * }\n *\n * // Unconditional transition with side effect (replaces action + target):\n * timeout({ ctx }) {\n * console.log(\"transitioning after\", ctx.tickCount, \"ticks\");\n * return \"yellow\";\n * }\n *\n * // Handler with extra args passed via handle(\"success\", responseData):\n * success({ ctx }, data) { ctx.result = data; }\n * ```\n */\nexport type HandlerFn<TCtx, TStateNames extends string = string> = (\n args: HandlerArgs<TCtx, TStateNames>,\n ...extra: unknown[]\n) => TStateNames | void;\n\n// -----------------------------------------------------------------------------\n// Handler definition forms\n//\n// A handler property on a state object is one of two things:\n// 1. A string — auto-transition shorthand: `timeout: \"yellow\"`\n// 2. A function — returns target state or void: `tick({ ctx }) { ... }`\n//\n// Two forms, one concept: \"a handler determines the next state.\"\n// String is the static case, function is the dynamic case.\n//\n// The FSM engine checks typeof at runtime: \"string\" → immediate transition,\n// \"function\" → call it and transition if it returns a state name.\n// -----------------------------------------------------------------------------\n\n/**\n * The union of valid handler definition forms for a state input.\n *\n * - `TStateNames` — string shorthand, auto-transitions to that state\n * - `HandlerFn` — function that returns a state name (transition) or void (stay)\n * - `MachinaInstance` — NOT a handler you'd write under an input key. It's here\n * because `ValidateStates` types every state as the SAME `Record` value type,\n * which combines named properties (`_onEnter`, `_onExit`, `_child`, `\"*\"`)\n * with a `[input: string]: HandlerDef<...>` index signature covering\n * everything else. TypeScript requires every named property's type to be\n * assignable to the index signature's type, so `_child?: MachinaInstance`\n * only type-checks if `HandlerDef` itself includes `MachinaInstance` in its\n * union. Remove this member and `_child: someChildFsm` stops compiling.\n *\n * @example\n * ```ts\n * states: {\n * green: {\n * timeout: \"yellow\", // string shorthand\n * tick({ ctx }) { ctx.tickCount++; }, // function, no transition\n * emergency({ ctx }) { // function, conditional transition\n * if (ctx.severity > 5) return \"red\";\n * },\n * },\n * }\n * ```\n */\nexport type HandlerDef<TCtx, TStateNames extends string = string> =\n | TStateNames\n | HandlerFn<TCtx, TStateNames>\n | MachinaInstance;\n\n// -----------------------------------------------------------------------------\n// State validation\n//\n// ValidateStates is the mapped type that makes config-inferred type safety\n// work. This is where typos in string shorthand targets get caught at\n// compile time. Getting there needs THREE separate mechanisms, not one:\n//\n// 1. Literal capture — TStates itself is inferred from a naked intersection\n// member on FsmConfig (`states: TStates & ...`), so it just holds whatever\n// object literal the user wrote. Plain object-literal inference doesn't\n// care what's INSIDE a state, so an empty state (`{}`) or a function-only\n// state (only _onEnter/_onExit, no string shorthand) can't poison it —\n// there's nothing here for TypeScript to \"give up\" on (see #188/#189\n// history below for why this matters).\n//\n// 2. Early state-name capture — TStateNames is a SEPARATE generic parameter,\n// defaulted to `keyof TStates & string` but inferred keys-only through\n// ValidateStates' `Record<TStateNames, ...>` shape. \"Keys-only\" is the\n// load-bearing part: this inference pass completes BEFORE handler bodies\n// are checked (handler bodies are context-sensitive — TypeScript defers\n// them until surrounding inference settles). That ordering is what fixes\n// #189: `defer({ until })`, lifecycle return values, and shorthand targets\n// all see the REAL literal state-name union while a handler body is being\n// type-checked, not a placeholder that hasn't resolved yet.\n//\n// 3. Validation — THIS type. A `Record` keyed by TStateNames (not\n// `keyof TStates` — re-deriving the key union from TStates here would\n// re-enter the same self-referential inference that broke #188/#189 in\n// the first place), with named special keys (_onEnter, _onExit, _child,\n// \"*\") plus an index signature for everything else. Every VALUE-position\n// reference to TStateNames is wrapped in `NoInfer`: without it, shorthand\n// VALUES (not just keys) become inference candidates for TStateNames, and\n// a typo'd target (`timeout: \"yellw\"`) would add itself to the union and\n// legalize itself — the typo silently becomes a \"valid\" state name instead\n// of an error.\n//\n// History: #188 filed \"an empty state disables validation config-wide.\"\n// #189 filed \"defer({ until }) isn't narrowed.\" Both traced back to the same\n// root cause — one type (the old single-parameter ValidateStates) trying to\n// both INFER TStates and VALIDATE it against itself. Splitting inference\n// (mechanisms 1-2) from validation (mechanism 3) fixes both at once.\n// -----------------------------------------------------------------------------\n\n/**\n * Validates and constrains the states object at the type level.\n *\n * A `Record` keyed by `TStateNames` (see the module-level comment above for\n * why it's keyed by this dedicated parameter rather than re-deriving keys\n * from `TStates`). Every state gets the same value shape: named optional\n * keys for lifecycle hooks / `_child` / catch-all, plus an index signature\n * for ordinary inputs.\n *\n * | Key | Expected type |\n * |------------------|-----------------------------------------|\n * | `_onEnter` | HandlerFn (lifecycle hook) |\n * | `_onExit` | HandlerFn (lifecycle hook) |\n * | `_child` | MachinaInstance (Fsm or BehavioralFsm) |\n * | `*` | HandlerFn (catch-all) |\n * | anything else | HandlerDef (string or fn) |\n *\n * @typeParam TCtx - Context/client type, flows into handler signatures\n * @typeParam TStates - The literal states object type. Only used here to\n * default `TStateNames` — every other reference in this type uses\n * `TStateNames` directly, never `TStates`, to avoid re-entering inference.\n * @typeParam TStateNames - The state-name union. Defaults to\n * `keyof TStates & string`, but callers that already resolved it\n * (the factory functions) pass it through explicitly.\n */\nexport type ValidateStates<\n TCtx,\n TStates extends Record<string, Record<string, unknown>>,\n TStateNames extends string = keyof TStates & string,\n> = Record<\n TStateNames,\n {\n _onEnter?: HandlerFn<TCtx, NoInfer<TStateNames>>;\n _onExit?: HandlerFn<TCtx, NoInfer<TStateNames>>;\n _child?: MachinaInstance;\n \"*\"?: HandlerFn<TCtx, NoInfer<TStateNames>>;\n } & {\n [input: string]: HandlerDef<TCtx, NoInfer<TStateNames>>;\n }\n>;\n\n// -----------------------------------------------------------------------------\n// D2 — bubbled-input coverage contract\n//\n// An FSM can declare `bubbles: [...]` — inputs it fires at itself expecting a\n// container to catch them via nohandler bubbling (see BehavioralFsm.handle's\n// delegation order). Any config that mounts that FSM via `_child` must\n// account for every declared bubble: handle it directly (any state), re-declare\n// it in the mounting FSM's own `bubbles` (passing the obligation up a level),\n// or carry a `\"*\"` catch-all somewhere. ChildCoverage enforces this on the\n// `states` property itself, so a violation surfaces as a compile error\n// anchored on the `_child` key that introduced the uncovered mount.\n// -----------------------------------------------------------------------------\n\n/**\n * True when at least one state in `TStates` declares a `\"*\"` catch-all —\n * a catch-all absorbs every input, including any bubbled one, so it counts\n * as coverage for the whole FSM regardless of which state it lives on.\n */\ntype HasCatchAll<TStates> = {\n [S in keyof TStates]: \"*\" extends keyof TStates[S] ? true : never;\n}[keyof TStates] extends never\n ? false\n : true;\n\n/**\n * What this FSM absorbs, for the purpose of covering a mounted child's\n * bubbled inputs: everything it handles locally (`OwnInputNamesOf`), plus\n * everything it re-declares as its OWN `bubbles` (re-exporting the\n * obligation to whatever mounts THIS FSM), plus — if a catch-all exists\n * anywhere — literally anything.\n */\ntype CoverageOf<TStates, TBubbles extends string> =\n | OwnInputNamesOf<TStates>\n | TBubbles\n | (HasCatchAll<TStates> extends true ? string : never);\n\n/**\n * For state `S`, if it mounts a `_child`, the child's declared bubbles that\n * this FSM's `CoverageOf` doesn't account for. `never` when the state has no\n * `_child`, or when every bubble is covered.\n */\ntype UncoveredChildBubbles<\n TStates extends Record<string, Record<string, unknown>>,\n S extends string,\n TBubbles extends string,\n> = TStates[S] extends { _child: infer C }\n ? Exclude<BubblesOfInstance<C>, CoverageOf<TStates, TBubbles>>\n : never;\n\n/**\n * Enforces the bubbled-input wiring contract across an entire `states`\n * config. Intersected onto `FsmConfig.states` alongside `TStates` and\n * `ValidateStates`, so a violation reports on the `states` object itself.\n *\n * Per state: `unknown` (a no-op intersection member) when that state's\n * `_child` mount — if any — has no uncovered bubbles. Otherwise, an\n * impossible-to-satisfy `_child` type whose single property name IS the\n * error message, naming exactly which inputs are uncovered. Coverage is\n * FSM-wide rather than mount-state-local: a bubbled input re-dispatches\n * against whatever state the parent happens to be in when the bubble\n * fires, which can be any state — so coverage declared ANYWHERE in the\n * config (or in this FSM's own `bubbles`, or via a `\"*\"` anywhere) counts,\n * matching that runtime reality exactly.\n */\nexport type ChildCoverage<\n TStates extends Record<string, Record<string, unknown>>,\n TStateNames extends string,\n TBubbles extends string,\n> = {\n [S in TStateNames]: UncoveredChildBubbles<NoInfer<TStates>, S, NoInfer<TBubbles>> extends never\n ? unknown\n : {\n _child: {\n \"child bubbles up inputs this FSM neither handles nor re-declares\": UncoveredChildBubbles<\n NoInfer<TStates>,\n S,\n NoInfer<TBubbles>\n >;\n };\n };\n};\n\n// -----------------------------------------------------------------------------\n// FSM configuration\n//\n// The config shape passed to createFsm() and createBehavioralFsm().\n// Both factory functions share this type — the difference is in how TCtx\n// is resolved:\n// - createFsm: TCtx inferred from the `context` property\n// - createBehavioralFsm: TCtx is the client type (context property ignored)\n// -----------------------------------------------------------------------------\n\n/**\n * Configuration object for creating an FSM.\n *\n * @typeParam TCtx - The context type (Fsm) or client type (BehavioralFsm).\n * For Fsm, this is inferred from the `context` property. For BehavioralFsm,\n * it's the client object type provided explicitly or as a generic parameter.\n *\n * @typeParam TStates - The literal states object type. Captured directly from\n * the naked `states: TStates & ...` intersection member below (ideally with\n * `const` on the factory's generic to preserve string literal types) —\n * deliberately NOT derived from `ValidateStates`, so an empty or\n * function-only state can't disable inference (see `ValidateStates`'\n * module comment for the full mechanism). Defaults to a loose record for\n * unconstrained usage.\n *\n * @typeParam TStateNames - The state-name union, defaulted from `TStates` but\n * captured as its OWN parameter (see `ValidateStates`) so it's available,\n * fully resolved, while handler bodies are still being type-checked.\n *\n * @typeParam TBubbles - The union of inputs this FSM declares via `bubbles`.\n * Defaults to `never` — most FSMs bubble nothing.\n *\n * @example\n * ```ts\n * // TCtx inferred as { tickCount: number }, TStates inferred from states object:\n * createFsm({\n * id: \"traffic-light\",\n * initialState: \"green\", // validated against state keys\n * context: { tickCount: 0 }, // inference site for TCtx\n * states: {\n * green: { timeout: \"yellow\" }, // \"yellow\" validated against state keys\n * yellow: { timeout: \"red\" },\n * red: { timeout: \"green\" },\n * },\n * });\n * ```\n *\n * @example Declaring bubbled inputs for a child FSM\n * ```ts\n * // This FSM fires \"phaseComplete\" at itself but never handles it — it\n * // expects whatever mounts it via `_child` to catch the bubble.\n * const phaseController = createFsm({\n * id: \"phase-controller\",\n * initialState: \"green\",\n * bubbles: [\"phaseComplete\"],\n * states: {\n * green: { advance: \"red\" },\n * red: {\n * _onEnter({ ctx }) {\n * setTimeout(() => phaseController.handle(\"phaseComplete\"), 0);\n * },\n * },\n * },\n * });\n *\n * // Mounting it without handling \"phaseComplete\" (and without re-declaring\n * // it in this FSM's own `bubbles`) is a compile error on `_child` below.\n * const intersection = createFsm({\n * id: \"intersection\",\n * initialState: \"northSouth\",\n * states: {\n * northSouth: {\n * _child: phaseController,\n * phaseComplete: \"clearance\", // this line is what covers the bubble\n * },\n * clearance: { advance: \"northSouth\" },\n * },\n * });\n * ```\n */\nexport interface FsmConfig<\n TCtx,\n TStates extends Record<string, Record<string, unknown>> = Record<\n string,\n Record<string, unknown>\n >,\n TStateNames extends string = keyof TStates & string,\n TBubbles extends string = never,\n> {\n /** Unique identifier for this FSM */\n id: string;\n\n /**\n * The state to start in. Must be a key of `states`.\n *\n * Wrapped in NoInfer to prevent TypeScript from using this value as an\n * inference site for TStateNames. Without it, `initialState: \"green\"`\n * could narrow the state-name union to only have a \"green\" key. We want\n * inference to come exclusively from the `states` property.\n */\n initialState: NoInfer<TStateNames>;\n\n /**\n * Initial context data. The type is inferred from this value and flows\n * into every handler's `ctx` parameter.\n *\n * For BehavioralFsm, this property is optional and serves only as a\n * type constraint — the client object IS the context.\n */\n context?: TCtx;\n\n /**\n * Inputs this FSM fires at itself without handling them — expecting\n * whatever mounts it via `_child` to catch them through machina's\n * nohandler-bubbling mechanism (see the \"Input delegation\" section of the\n * hierarchical states guide). Declaring a bubble does two things:\n *\n * 1. Joins this FSM's own typed input union, so a self-directed\n * `fsm.handle(\"phaseComplete\")` (e.g. from inside `_onEnter`)\n * type-checks without a cast.\n * 2. Becomes part of this FSM's mounting contract: any config that\n * mounts it via `_child` must handle every declared bubble in some\n * state, re-declare it in its OWN `bubbles` (passing the obligation\n * up another level), or carry a `\"*\"` catch-all — enforced at compile\n * time by `ChildCoverage`, which is intersected onto `states` below.\n *\n * A bubble name is NOT a state name — it can't be used as a string\n * shorthand transition target. Omit `bubbles` entirely (the default) for\n * an FSM that never expects a container to catch anything from it — such\n * an FSM can be mounted via `_child` anywhere with no obligations.\n */\n bubbles?: readonly TBubbles[];\n\n /**\n * State definitions. Keys become the state name union.\n *\n * The intersection with the bare `TStates` is what makes literal-type\n * capture work for empty and function-only states (see `ValidateStates`'\n * module comment) — `ValidateStates` and `ChildCoverage` layer validation\n * and the bubble-coverage contract on top without becoming the inference\n * source themselves.\n */\n states: TStates &\n ValidateStates<TCtx, TStates, TStateNames> &\n ChildCoverage<TStates, TStateNames, TBubbles>;\n}\n\n// -----------------------------------------------------------------------------\n// Type discriminant — runtime distinguishing of Fsm vs BehavioralFsm\n//\n// Used internally by the ChildLink adapter to normalize delegation calls\n// without introducing circular imports. Not exported to users.\n// -----------------------------------------------------------------------------\n\n/**\n * Symbol used as a property key to identify machina FSM instances at runtime.\n * Each class stamps itself with a MachinaType value so the ChildLink adapter\n * can dispatch handle()/canHandle()/reset() correctly without circular imports.\n */\nexport const MACHINA_TYPE = Symbol(\"machina.type\");\n\n/**\n * Discriminant values stamped onto FSM instances via `MACHINA_TYPE`.\n * Used by the `ChildLink` adapter to dispatch calls correctly without\n * importing either class directly (which would create circular dependencies).\n */\nexport type MachinaType = \"Fsm\" | \"BehavioralFsm\";\n\n/** Structural type matching any machina FSM instance (Fsm or BehavioralFsm) */\nexport type MachinaInstance = { readonly [MACHINA_TYPE]: MachinaType };\n\n// -----------------------------------------------------------------------------\n// ChildLink — internal adapter interface\n//\n// Normalizes Fsm (no client arg) vs BehavioralFsm (client arg) delegation.\n// The BehavioralFsm engine uses this internally; never exported to users.\n// -----------------------------------------------------------------------------\n\n/**\n * Internal adapter that wraps either an Fsm or BehavioralFsm child,\n * presenting a uniform API for parent-initiated delegation.\n */\nexport interface ChildLink {\n /** Check if the child's current state can handle this input */\n canHandle(client: object, inputName: string): boolean;\n /** Dispatch the input to the child */\n handle(client: object, inputName: string, ...args: unknown[]): void;\n /** Reset the child to its initialState */\n reset(client: object): void;\n /** Subscribe to all child events (wildcard). Returns unsubscribe fn. */\n onAny(callback: (eventName: string, data: unknown) => void): { off(): void };\n /** The child FSM's compositeState for the given client */\n compositeState(client: object): string;\n /**\n * Silently place `client` at the given composite state within the child hierarchy.\n * Throws for Fsm children (no per-client state to rehydrate).\n */\n rehydrate(client: object, compositeState: string): void;\n /**\n * Snapshot everything this child tracks for `client`, recursing into its own\n * children. `undefined` when the child has never seen this client. Throws for\n * Fsm children that are on the active path — an Fsm owns its own context, so\n * there's nothing per-client to snapshot. `isOnActivePath` is threaded down\n * from the true root's dehydrate() call: it's `false` whenever ANY ancestor\n * declaring state didn't match its own parent's active state, so a nested\n * BehavioralFsm child correctly treats every one of its own Fsm children as\n * off-path once the child itself is off-path, rather than recomputing\n * reachability from its own (possibly dormant) state alone.\n */\n dehydrate(client: object, isOnActivePath: boolean): ClientSnapshot | undefined;\n /**\n * Validates `snapshot` against this child's state graph (recursing into its\n * own children) and returns write thunks to run only once the ENTIRE tree\n * validates — nothing is written here. Throws for Fsm children, matching\n * `dehydrate()`.\n */\n planRehydrate(client: object, snapshot: ClientSnapshot): Array<() => void>;\n /** Dispose the child FSM */\n dispose(): void;\n /**\n * The raw Fsm or BehavioralFsm instance this ChildLink wraps.\n * Exposed for inspection tooling (machina-inspect) — allows external\n * tools to introspect child graph structure without reaching through\n * private fields.\n */\n instance: MachinaInstance;\n}\n\n/**\n * Options for FSM disposal.\n */\nexport interface DisposeOptions {\n /**\n * When true, child FSMs declared via _child are NOT disposed.\n * Default: false (children ARE disposed along with the parent).\n */\n preserveChildren?: boolean;\n}\n\n// -----------------------------------------------------------------------------\n// Built-in FSM event maps\n//\n// machina FSMs emit lifecycle events that external code can subscribe to.\n// These types define the payload shape for each built-in event. Custom\n// events (emitted via emit() in handlers) are untyped — they flow through\n// the emitter's wildcard path with `unknown` payloads.\n//\n// Event naming follows a grammatical pattern:\n// - Present participle = \"about to happen\": transitioning, handling\n// - Past participle = \"just happened\": transitioned, handled\n// -----------------------------------------------------------------------------\n\n/**\n * Built-in event map for Fsm instances.\n * Payloads do NOT include a client reference (Fsm is its own client).\n *\n * @typeParam TStateNames - The state name union, flows into transition\n * event payloads so fromState/toState are narrowed to actual state names.\n */\nexport interface FsmEventMap<TStateNames extends string = string> {\n /** Fired just before a state transition occurs */\n transitioning: { fromState: TStateNames; toState: TStateNames };\n\n /** Fired just after a state transition completes */\n transitioned: { fromState: TStateNames; toState: TStateNames };\n\n /** Fired when an input is about to be dispatched to a handler */\n handling: { inputName: string };\n\n /** Fired after an input has been successfully handled */\n handled: { inputName: string };\n\n /** Fired when an input has no matching handler in the current state */\n nohandler: { inputName: string; args: unknown[] };\n\n /** Fired when a transition targets a state that doesn't exist */\n invalidstate: { stateName: string };\n\n /** Fired when an input is deferred for later replay */\n deferred: { inputName: string };\n}\n\n/**\n * Built-in event map for BehavioralFsm instances.\n * Every payload is intersected with `{ client: TClient }` so subscribers\n * can identify which client the event pertains to.\n *\n * @typeParam TClient - The client object type\n * @typeParam TStateNames - The state name union\n */\nexport type BehavioralFsmEventMap<TClient, TStateNames extends string = string> = {\n [K in keyof FsmEventMap<TStateNames>]: FsmEventMap<TStateNames>[K] & {\n client: TClient;\n };\n};\n\n// -----------------------------------------------------------------------------\n// Internal per-client metadata\n//\n// BehavioralFsm tracks per-client state in a WeakMap<TClient, ClientMeta>.\n// When the client object is garbage collected, its metadata goes with it.\n// This replaces v5's __machina__ property stamping on client objects.\n// -----------------------------------------------------------------------------\n\n/**\n * A deferred input queue entry. Created when a handler calls\n * deferUntilTransition() — the input is stored here and replayed\n * after a future state transition.\n */\nexport interface DeferredInput {\n /** The input name that was deferred */\n inputName: string;\n\n /** The original arguments passed to handle() for this input */\n args: unknown[];\n\n /**\n * If set, only replay when entering this specific state.\n * When undefined, replays on the next transition to any state.\n */\n untilState?: string;\n}\n\n/**\n * Internal metadata stored per client in the WeakMap.\n * This is the bookkeeping the FSM engine needs — NOT user-facing data.\n *\n * @typeParam TStateNames - The state name union. The `state` field is\n * typed to this so internal code gets compile-time validation.\n */\nexport interface ClientMeta<TStateNames extends string = string> {\n /** The client's current state */\n state: TStateNames;\n\n /** Queue of inputs deferred for later replay */\n deferredQueue: DeferredInput[];\n\n /**\n * The args array for the currently-executing handle() call.\n * Captured before handler dispatch so that deferUntilTransition()\n * can snapshot them for later replay. Cleared after dispatch.\n */\n currentActionArgs?: unknown[];\n}\n\n// -----------------------------------------------------------------------------\n// ClientSnapshot — the dehydrate()/rehydrate() persistence contract\n//\n// Everything machina tracks for one client, as plain serializable data.\n// Nested per hierarchy level: each FSM in the `_child` chain contributes its\n// own node, keyed by the PARENT state name that declares it. Deliberately\n// excludes `currentActionArgs` — that field only exists mid-handle() and has\n// no meaning for a client \"at rest\" between calls.\n// -----------------------------------------------------------------------------\n\n/**\n * Plain-data snapshot of everything machina tracks for one client at a given\n * level of the FSM hierarchy. Produced by `dehydrate()`, consumed by the\n * object-overload of `rehydrate()`.\n *\n * `children` covers every state whose `_child` holds tracking data for this\n * client — active or not. A child the client never reached has no entry\n * (there's nothing to restore). This full-fidelity walk is what makes a\n * rehydrated client behaviorally indistinguishable from one that never left\n * memory: off-path child state and its pending deferrals travel too, and\n * replay/reset exactly as they would have in-memory on the parent's next\n * re-entry.\n */\nexport interface ClientSnapshot {\n /** This level's state name (not the composite dot-path). */\n state: string;\n\n /** This level's pending deferred inputs, in FIFO replay order. */\n deferred: DeferredInput[];\n\n /**\n * One entry per state (at this level) whose `_child` has tracking data\n * for this client, keyed by that state's name. Omitted entirely when no\n * state's child has ever seen this client.\n */\n children?: { [stateName: string]: ClientSnapshot };\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// =============================================================================\n// behavioral-fsm.ts — The core FSM engine\n//\n// BehavioralFsm defines behavior (states + transitions) while tracking state\n// externally per-client via a WeakMap. One FSM definition can drive any number\n// of independent client objects.\n//\n// =============================================================================\n\nimport { Emitter, type Subscription } from \"./emitter\";\nimport { cloneDeep, cloneJsonSafe, NonSerializableValueError } from \"./json-safe\";\nimport {\n MACHINA_TYPE,\n type FsmConfig,\n type InputNamesOfInstance,\n type HandlerArgs,\n type HandlerFn,\n type BehavioralFsmEventMap,\n type ClientMeta,\n type DeferredInput,\n type ChildLink,\n type DisposeOptions,\n type ClientSnapshot,\n type MachinaInstance,\n type SpecialStateKeys,\n} from \"./types\";\n\n// Safety valve for _onEnter → transition loops. Instance-level counter works\n// because the engine is synchronous — only one transition chain is active at\n// a time. Throws on overflow, then resets so the FSM isn't permanently broken.\nconst MAX_TRANSITION_DEPTH = 20;\n\n/**\n * Defines FSM behavior (states + transitions) while tracking per-client state\n * in a `WeakMap`. A single `BehavioralFsm` instance can drive any number of\n * independent client objects simultaneously — each gets its own state,\n * deferred queue, and lifecycle.\n *\n * Prefer `createBehavioralFsm()` over constructing this directly — the factory\n * infers all generic parameters from the config object.\n *\n * All public methods silently no-op after `dispose()` is called.\n *\n * @typeParam TClient - The client object type. Must be an object (non-primitive)\n * so it can serve as a WeakMap key.\n * @typeParam TStateNames - String literal union of valid state names.\n * @typeParam TInputNames - String literal union of valid input names.\n * @typeParam TBubbles - String literal union of inputs this FSM declares via\n * `bubbles`. Type-only — carried so `BubblesOfInstance` can extract it from\n * a constructed instance; nothing at runtime reads this generic.\n */\nexport class BehavioralFsm<\n TClient extends object,\n TStateNames extends string,\n TInputNames extends string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- phantom marker: never referenced in the class body, only read back out externally via BubblesOfInstance's `infer`.\n TBubbles extends string = never,\n> {\n readonly id: string;\n readonly initialState: TStateNames;\n // Type discriminant — lets ChildLink adapter identify this at runtime\n readonly [MACHINA_TYPE] = \"BehavioralFsm\" as const;\n // Public so inspection tooling (machina-inspect) can read the state graph\n // without private field access. The states object is mutated by wrapChildLinks()\n // at construction time — _child values become ChildLink wrappers in-place.\n readonly states: Record<string, Record<string, unknown>>;\n private readonly emitter = new Emitter<BehavioralFsmEventMap<TClient, TStateNames>>();\n private readonly clients = new WeakMap<TClient, ClientMeta<TStateNames>>();\n // Tracks all initialized clients so the Fsm-child nohandler listener can\n // find which clients need bubbling (Fsm events have no client in payload).\n // WeakRef prevents retention; clients clean themselves up naturally.\n private readonly knownClients: Set<WeakRef<TClient>> = new Set();\n // Subscriptions to child FSM wildcard events, keyed by state name.\n // Set up once during construction, torn down in dispose().\n private readonly childSubscriptions: Array<{ off(): void }> = [];\n private disposed = false;\n private transitionDepth = 0;\n\n constructor(config: FsmConfig<TClient, Record<string, Record<string, unknown>>>) {\n this.id = config.id;\n this.initialState = config.initialState as TStateNames;\n this.states = config.states as Record<string, Record<string, unknown>>;\n this.wrapChildLinks();\n this.setupChildSubscriptions();\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n /**\n * Dispatch an input to the given client's current state handler.\n *\n * Delegation order: if the current state has a `_child` FSM that can\n * handle the input, it is dispatched there. If the child emits `nohandler`,\n * the input bubbles up to this FSM's local handler. If no handler exists\n * here either, `nohandler` is emitted on this FSM's emitter.\n *\n * No-ops silently when disposed.\n */\n handle(client: TClient, inputName: TInputNames, ...args: unknown[]): void {\n if (this.disposed) {\n return;\n }\n\n const meta = this.getOrCreateClientMeta(client);\n meta.currentActionArgs = args;\n\n // Delegation: if the current state has a _child and the child can\n // handle this input, send it there. Otherwise fall through to\n // handleLocally(). No nohandler emitted for the delegation path.\n const stateObj = this.states[meta.state];\n const childLink = stateObj?._child as ChildLink | undefined;\n if (childLink) {\n if (childLink.canHandle(client, inputName)) {\n try {\n childLink.handle(client, inputName, ...args);\n } finally {\n // Clear args even if the child handler throws — stale args\n // on meta would corrupt subsequent handle() calls for this client.\n meta.currentActionArgs = undefined;\n }\n return;\n }\n }\n\n this.handleLocally(client, inputName, args, meta);\n }\n\n /**\n * Returns true if the client's current state has a handler for `inputName`\n * (or a catch-all `\"*\"` handler), or if the current state's `_child` chain\n * can handle it — the check recurses to the same depth `handle()`'s\n * delegation can actually reach, so a grandchild-only input answers true\n * from the root. Does NOT initialize the client — no `_onEnter`, no\n * events, no side effects. Unseen clients are treated as if they were\n * already in `initialState`. Returns false when disposed.\n */\n canHandle(client: TClient, inputName: string): boolean {\n if (this.disposed) {\n return false;\n }\n // Does NOT initialize the client. We want a pure state check —\n // no side effects, no _onEnter, no events. Use initialState as the\n // fallback for unseen clients (they'd start there anyway).\n const state = this.clients.get(client)?.state ?? this.initialState;\n const stateObj = this.states[state];\n if (stateObj?.[inputName] ?? stateObj?.[\"*\"]) {\n return true;\n }\n // Recurse into the current state's child so the answer matches\n // handle()'s real delegation reach: handle() forwards one level, the\n // child's own handle() forwards another, and so on. Without this, a\n // grandchild-only input answers \"no\" at the top even though the\n // delegation chain would carry it all the way down — which made\n // D1-surfaced grandchild inputs type-check and then silently no-op.\n const childLink = stateObj?._child as ChildLink | undefined;\n return childLink ? childLink.canHandle(client, inputName) : false;\n }\n\n /**\n * Transition the client back to `initialState`, firing `_onEnter` and\n * lifecycle events as if entering it fresh. No-ops when disposed.\n */\n reset(client: TClient): void {\n if (this.disposed) {\n return;\n }\n this.transition(client, this.initialState);\n }\n\n /**\n * Returns the client's current state, or `undefined` if the client has\n * never been initialized (i.e. `handle()`, `transition()`, or `reset()`\n * have never been called for it). Does NOT trigger initialization.\n */\n currentState(client: TClient): TStateNames | undefined {\n // Intentionally uses WeakMap.get() directly — does NOT trigger initialization.\n // Returns undefined for clients the FSM has never seen.\n return this.clients.get(client)?.state;\n }\n\n /**\n * Directly transition `client` to `toState`, running the full lifecycle:\n * `_onExit` for the current state → `transitioning` event → update state →\n * `_onEnter` for new state → `transitioned` event → child reset → deferred\n * queue replay → bounce (if `_onEnter` returned a state name).\n *\n * Same-state transitions are silently ignored. Transitions to unknown state\n * names emit `invalidstate` instead of throwing. Throws if the transition\n * depth exceeds `MAX_TRANSITION_DEPTH` (likely an `_onEnter` → transition loop).\n *\n * No-ops when disposed.\n */\n transition(client: TClient, toState: TStateNames): void {\n if (this.disposed) {\n return;\n }\n\n const meta = this.getOrCreateClientMeta(client);\n const fromState = meta.state;\n\n if (toState === fromState) {\n return;\n }\n\n // Object.hasOwn (not `in`) — `in` walks the prototype chain, so\n // toState: \"__proto__\" (or \"constructor\", \"toString\", ...) would pass\n // this check via the inherited Object.prototype member even though no\n // state literally named that was ever declared. Same fix as\n // planSnapshotWrites() got in #184, applied here for parity.\n if (!Object.hasOwn(this.states, toState)) {\n this.emitter.emit(\"invalidstate\", { stateName: toState, client });\n return;\n }\n\n this.transitionDepth++;\n if (this.transitionDepth > MAX_TRANSITION_DEPTH) {\n this.transitionDepth = 0;\n throw new Error(\n `Max transition depth (${MAX_TRANSITION_DEPTH}) exceeded in FSM \"${this.id}\". ` +\n \"Likely an infinite _onEnter → transition loop.\"\n );\n }\n\n try {\n const curStateObj = this.states[fromState];\n const newStateObj = this.states[toState];\n\n // _onExit for current state. The optional chain handles two cases:\n // 1. First-time initialization — fromState is `undefined as TStateNames`,\n // so this.states[undefined] is undefined; no exit hook to run.\n // 2. States that simply have no _onExit defined — equally valid, equally ignored.\n if (curStateObj?._onExit && typeof curStateObj._onExit === \"function\") {\n const exitArgs = this.buildHandlerArgs(client, \"\", meta);\n (curStateObj._onExit as HandlerFn<TClient, TStateNames>)(exitArgs);\n // _onExit return value is intentionally ignored — you're already leaving\n }\n\n meta.state = toState;\n\n const payload = { fromState, toState, client };\n this.emitter.emit(\"transitioning\", payload);\n\n // _onEnter for new state — return value is a bounce target\n let bounceTarget: TStateNames | void = undefined;\n if (newStateObj?._onEnter && typeof newStateObj._onEnter === \"function\") {\n const enterArgs = this.buildHandlerArgs(client, \"\", meta);\n bounceTarget = (newStateObj._onEnter as HandlerFn<TClient, TStateNames>)(enterArgs);\n }\n\n this.emitter.emit(\"transitioned\", payload);\n\n // Reset child FSM after entering the new state. This ensures the child\n // always starts from its initialState when the parent enters. Happens\n // after _onEnter and transitioned but before deferred queue processing,\n // so deferred inputs see the post-reset child state.\n const childLink = newStateObj?._child as ChildLink | undefined;\n if (childLink) {\n childLink.reset(client);\n }\n\n // Replay deferred inputs targeting this state\n this.processQueue(client, meta);\n\n // Bounce: _onEnter returned a state name. Only fires if we're still\n // in the state _onEnter belongs to — a deferred replay might have\n // already moved us elsewhere.\n if (typeof bounceTarget === \"string\" && meta.state === toState) {\n this.transition(client, bounceTarget);\n }\n } finally {\n this.transitionDepth--;\n }\n }\n\n /**\n * Returns the client's state as a dot-delimited path including any active\n * child FSM states (e.g. `\"active.connecting.retrying\"`). Returns just the\n * current state name when no child is active. Returns `\"\"` for clients that\n * have never been initialized (unlike `currentState()` which returns `undefined`).\n */\n compositeState(client: TClient): string {\n const meta = this.clients.get(client);\n if (!meta) {\n // Returns \"\" (not undefined like currentState()) because compositeState\n // produces dot-delimited paths (\"stateA.child.grandchild\") for hierarchies.\n // Empty string is the correct \"nothing to report\" sentinel for string concat.\n return \"\";\n }\n\n const stateObj = this.states[meta.state];\n const childLink = stateObj?._child as ChildLink | undefined;\n if (childLink) {\n const childComposite = childLink.compositeState(client);\n if (childComposite) {\n return `${meta.state}.${childComposite}`;\n }\n }\n\n return meta.state;\n }\n\n /**\n * Silently place `client` at `compositeState` with no lifecycle activity —\n * no `_onEnter`, no `_onExit`, no `transitioning`/`transitioned` events.\n * Designed to work with `compositeState()`, which produces the dot-path\n * string that the string form consumes.\n *\n * The snapshot form (pass a `ClientSnapshot` from `dehydrate()`) additionally\n * requeues each level's pending deferred inputs, so a client resumes with the\n * same replay-on-next-transition behavior it had when dehydrated. Both forms\n * validate the ENTIRE hierarchy before writing anything — a throw at any level\n * leaves every level, including this one, unwritten.\n *\n * Throws synchronously for unknown state names, missing `_child` at an inner\n * level, or Fsm children in the hierarchy (Fsm owns its own context; nothing\n * per-client to rehydrate there).\n *\n * No-ops silently when disposed.\n */\n rehydrate(client: TClient, compositeState: string): void;\n rehydrate(client: TClient, snapshot: ClientSnapshot): void;\n rehydrate(client: TClient, input: string | ClientSnapshot): void {\n if (this.disposed) {\n return;\n }\n\n if (typeof input === \"string\") {\n this.rehydrateCompositePath(client, input);\n return;\n }\n\n // Object form: collect write thunks for the WHOLE tree first (this level\n // AND every descendant), then run them only once nothing has thrown.\n // The string-form's children-first recursion gets atomicity \"for free\"\n // because each level writes only after its child already succeeded — but\n // that couples validation to writing. Here we need every level validated\n // before ANY level writes, since a leaf failing three levels down must\n // not leave levels above it holding a stale write.\n const writes = this.planSnapshotWrites(client, input);\n for (const write of writes) {\n write();\n }\n }\n\n /**\n * Snapshot everything machina tracks for `client`: current state, pending\n * deferred inputs, and — recursively — the same for every `_child` that has\n * ever seen this client, active or not. Feed the result to the object form\n * of `rehydrate()` to restore it later, deferrals included.\n *\n * Returns `undefined` for a client this FSM has never seen (mirrors\n * `currentState()`) — the call does NOT trigger initialization.\n *\n * Throws if any deferred input's args contain a non-serializable value\n * (function, undefined, symbol, bigint, non-finite number, Date/Map/class\n * instance, or a circular reference) — naming the input, its `until` target\n * if any, the FSM id, and the exact value path. Throws for an Fsm child\n * that's on `client`'s active path *relative to the true root* (consistent\n * with `rehydrate()`'s conditional throw) — an Fsm owns its own context, so\n * there's nothing per-client to snapshot. An Fsm child declared at a state\n * `client` never visited, OR nested under a `BehavioralFsm` child that is\n * itself off-path from the root, is skipped rather than throwing — neither\n * has any per-client state to lose, so one Fsm child anywhere in the\n * hierarchy doesn't disable `dehydrate()` for clients that never reach that\n * branch, no matter how deeply nested the Fsm child is.\n *\n * Meant for clients at rest between `handle()` calls — `currentActionArgs`\n * (the in-flight args mid-handler) has no meaning here and is excluded.\n *\n * @param isOnActivePath - @internal Whether this FSM itself is currently\n * reachable from the true root's active path. Defaults to `true` for the\n * public entry point (this FSM IS the root from its own perspective); the\n * `ChildLink` adapter passes `false` down when recursing into a nested\n * `BehavioralFsm` child that is itself off-path, so that child's own\n * Fsm-child checks don't recompute reachability from its dormant local\n * state alone.\n */\n dehydrate(client: TClient, isOnActivePath = true): ClientSnapshot | undefined {\n const meta = this.clients.get(client);\n if (!meta) {\n return undefined;\n }\n\n const snapshot: ClientSnapshot = {\n state: meta.state,\n deferred: meta.deferredQueue.map(item => this.snapshotDeferredInput(item)),\n };\n\n const children = this.collectChildSnapshots(client, meta.state, isOnActivePath);\n if (children) {\n snapshot.children = children;\n }\n\n return snapshot;\n }\n\n /**\n * @internal\n * Validates `snapshot` against this level's state graph and recurses into\n * every declared child, returning write thunks to run only once the WHOLE\n * tree — every level — validates successfully. Nothing is written here.\n * Called by the object-form of `rehydrate()` and, recursively, by ChildLink\n * so a nested BehavioralFsm participates in the same validate-then-write\n * pass. Not part of the public persistence API — call `rehydrate()` instead.\n */\n planSnapshotWrites(client: TClient, snapshot: ClientSnapshot): Array<() => void> {\n if (this.disposed) {\n // Mirrors the string form: a disposed FSM mid-hierarchy silently\n // contributes no writes rather than blocking the rest of the tree.\n return [];\n }\n\n const { state, deferred, children } = snapshot;\n\n // Object.hasOwn (not `in`) — `in` walks the prototype chain, so a\n // snapshot with state: \"__proto__\" (or \"constructor\", \"toString\", ...)\n // would pass validation via the inherited Object.prototype member even\n // though no state literally named that was ever declared.\n if (!Object.hasOwn(this.states, state)) {\n throw new Error(\n `rehydrate: unknown state \"${state}\" in FSM \"${this.id}\". ` +\n `Valid states: ${Object.keys(this.states).join(\", \")}`\n );\n }\n\n const writes: Array<() => void> = [];\n\n if (children) {\n for (const stateName of Object.keys(children)) {\n if (!Object.hasOwn(this.states, stateName)) {\n throw new Error(\n `rehydrate: unknown state \"${stateName}\" in FSM \"${this.id}\" ` +\n `referenced by snapshot.children.`\n );\n }\n\n const childLink = this.states[stateName]?._child as ChildLink | undefined;\n if (!childLink) {\n throw new Error(\n `rehydrate: state \"${stateName}\" in FSM \"${this.id}\" has no _child, ` +\n `but the snapshot has a children[\"${stateName}\"] entry.`\n );\n }\n\n writes.push(...childLink.planRehydrate(client, children[stateName]));\n }\n }\n\n // Deep-clone args so the caller's snapshot object isn't aliased into live\n // FSM state — a shallow spread only protects the array itself, not any\n // nested objects/arrays inside it. cloneDeep() is validation-free (unlike\n // dehydrate()'s cloneJsonSafe): rehydrate() trusts the snapshot is already\n // valid data rather than re-validating on the way in.\n const deferredQueue: DeferredInput[] = deferred.map(item => ({\n inputName: item.inputName,\n args: cloneDeep(item.args) as unknown[],\n ...(item.untilState !== undefined ? { untilState: item.untilState } : {}),\n }));\n\n writes.push(() => {\n // Guard knownClients before writing meta — if the client is already\n // tracked, skip adding another WeakRef to avoid accumulating duplicates.\n if (!this.clients.has(client)) {\n this.knownClients.add(new WeakRef(client));\n }\n this.clients.set(client, { state: state as TStateNames, deferredQueue });\n });\n\n return writes;\n }\n\n private rehydrateCompositePath(client: TClient, compositeState: string): void {\n const [state, ...rest] = compositeState.split(\".\");\n\n // Object.hasOwn (not `in`) — same rationale as transition()'s check above.\n if (!Object.hasOwn(this.states, state)) {\n throw new Error(\n `rehydrate: unknown state \"${state}\" in FSM \"${this.id}\". ` +\n `Valid states: ${Object.keys(this.states).join(\", \")}`\n );\n }\n\n // Validate and delegate to children BEFORE writing to the parent WeakMap.\n // This ensures a throw mid-hierarchy doesn't leave a half-registered client.\n if (rest.length > 0) {\n const childPath = rest.join(\".\");\n const stateObj = this.states[state];\n const childLink = stateObj?._child as ChildLink | undefined;\n\n if (!childLink) {\n throw new Error(\n `rehydrate: state \"${state}\" in FSM \"${this.id}\" has no _child, ` +\n `but composite path \"${compositeState}\" requires one.`\n );\n }\n\n childLink.rehydrate(client, childPath);\n }\n\n // Guard knownClients before writing meta — if the client is already tracked,\n // skip adding another WeakRef to avoid accumulating duplicate live refs.\n if (!this.clients.has(client)) {\n this.knownClients.add(new WeakRef(client));\n }\n this.clients.set(client, { state: state as TStateNames, deferredQueue: [] });\n }\n\n /**\n * Walks every declared state's `_child`, dehydrating each one that has ever\n * seen `client`. The same child instance can be declared under multiple\n * state names (shared child) — it's dehydrated once and the result is\n * reused under every declaring state name, matching how the engine already\n * treats shared children elsewhere (dispose, event subscriptions).\n *\n * Declaring names are grouped by `childLink.instance` (the actual FSM\n * instance, not the `ChildLink` wrapper — `wrapChildLinks()` mints a fresh\n * wrapper per declaring state, so grouping by wrapper would never hit for a\n * shared child) BEFORE any `onPath` is computed or any child is dehydrated.\n * A shared child's `onPath` is the OR of `stateName === activeState` across\n * every declaring name in its group: at most one declaring name can ever\n * equal `activeState`, so this combined flag is correct and, critically,\n * independent of `Object.keys(this.states)` iteration order — computing\n * `onPath` per-declaring-name and caching whichever one happened to run\n * first would silently launder an on-path client's Fsm-child throw through\n * an unrelated off-path declaring name.\n *\n * An off-path Fsm child (declared only at states other than `activeState`,\n * OR nested anywhere under a `BehavioralFsm` child that is itself off-path\n * relative to the true root) is skipped entirely rather than dehydrated:\n * Fsm state isn't tracked per-client to begin with, so an off-path Fsm\n * child has nothing to lose by being skipped — unlike a BehavioralFsm\n * child's off-path meta, which is real per-client data. `isOnActivePath`\n * is the inherited \"am I even reachable from the root\" flag; combining it\n * with the group's `stateNames.includes(activeState)` check (rather than\n * using that check alone) is what keeps a nested Fsm grandchild from\n * throwing when its immediate BehavioralFsm parent is itself off-path —\n * the parent's own dormant `activeState` is irrelevant once the parent\n * isn't reachable. This keeps one Fsm child anywhere in the hierarchy from\n * disabling `dehydrate()` for every client, only for clients actually on\n * that branch.\n */\n private collectChildSnapshots(\n client: TClient,\n activeState: string,\n isOnActivePath: boolean\n ): Record<string, ClientSnapshot> | undefined {\n // Pass 1: group every declaring state name by the child instance it\n // resolves to, so a shared child's combined onPath can be computed\n // before dehydrate() is ever called for it.\n const declaringNamesByInstance = new Map<\n MachinaInstance,\n { childLink: ChildLink; stateNames: string[] }\n >();\n\n for (const stateName of Object.keys(this.states)) {\n const childLink = this.states[stateName]?._child as ChildLink | undefined;\n if (!childLink) {\n continue;\n }\n\n const entry = declaringNamesByInstance.get(childLink.instance);\n if (entry) {\n entry.stateNames.push(stateName);\n } else {\n declaringNamesByInstance.set(childLink.instance, {\n childLink,\n stateNames: [stateName],\n });\n }\n }\n\n // Pass 2: dehydrate each unique instance exactly once, using the\n // combined onPath, then fan the result out to every declaring name.\n let children: Record<string, ClientSnapshot> | undefined;\n\n for (const { childLink, stateNames } of declaringNamesByInstance.values()) {\n const onPath = isOnActivePath && stateNames.includes(activeState);\n const isOffPathFsmChild = childLink.instance[MACHINA_TYPE] === \"Fsm\" && !onPath;\n if (isOffPathFsmChild) {\n continue;\n }\n\n // Throws for an Fsm child that IS on the active path — propagates\n // straight through, consistent with rehydrate()'s Fsm-child throw.\n // `onPath` is passed down so a nested BehavioralFsm child applies\n // the same root-relative reachability to its own Fsm children.\n const childSnapshot = childLink.dehydrate(client, onPath);\n if (childSnapshot) {\n children ??= {};\n for (const stateName of stateNames) {\n children[stateName] = childSnapshot;\n }\n }\n }\n\n return children;\n }\n\n private snapshotDeferredInput(item: DeferredInput): DeferredInput {\n let clonedArgs: unknown[];\n try {\n clonedArgs = cloneJsonSafe(item.args, \"args\") as unknown[];\n } catch (err) {\n if (!(err instanceof NonSerializableValueError)) {\n throw err;\n }\n const untilPart = item.untilState ? ` (until \"${item.untilState}\")` : \"\";\n throw new Error(\n `dehydrate: deferred input \"${item.inputName}\"${untilPart} in FSM \"${this.id}\" ` +\n `has a non-serializable value at ${err.path} (${err.label})`\n );\n }\n\n const snapshot: DeferredInput = { inputName: item.inputName, args: clonedArgs };\n if (item.untilState !== undefined) {\n snapshot.untilState = item.untilState;\n }\n return snapshot;\n }\n\n /**\n * Subscribe to a built-in lifecycle event or the wildcard.\n *\n * Named overload: typed payload includes `{ client: TClient }` so you can\n * identify which client the event pertains to. Wildcard (`\"*\"`) receives\n * `(eventName, data)` for every event. Returns a no-op `Subscription`\n * when disposed.\n */\n on<K extends keyof BehavioralFsmEventMap<TClient, TStateNames> & string>(\n eventName: K,\n callback: (data: BehavioralFsmEventMap<TClient, TStateNames>[K]) => void\n ): Subscription;\n on(eventName: \"*\", callback: (eventName: string, data: unknown) => void): Subscription;\n on(eventName: string, callback: (...args: any[]) => void): Subscription {\n if (this.disposed) {\n return { off() {} };\n }\n return this.emitter.on(eventName as any, callback as any);\n }\n\n /**\n * Emit a custom event through the FSM. Built-in lifecycle events are\n * emitted automatically — this is for user-defined events from handlers.\n * No-ops when disposed.\n */\n emit(eventName: string, data?: unknown): void {\n if (this.disposed) {\n return;\n }\n // Public emit is for user-defined custom events. Built-in events are\n // emitted automatically by the engine.\n (this.emitter as any).emit(eventName, data);\n }\n\n /**\n * Permanently shut down this FSM. Irreversible — all subsequent method\n * calls become silent no-ops. Tears down child subscriptions, clears all\n * listeners, and cascades disposal to child FSMs (unless `preserveChildren`\n * is set). The same child appearing in multiple states is disposed once.\n */\n dispose(options?: DisposeOptions): void {\n this.disposed = true;\n // Tear down all child wildcard subscriptions before clearing our emitter.\n for (const sub of this.childSubscriptions) {\n sub.off();\n }\n // Cascade disposal to child FSMs unless explicitly opted out.\n // Dedup keys on the underlying instance, not the ChildLink wrapper —\n // wrapChildLinks() mints a fresh wrapper per declaring state, so a\n // wrapper-keyed Set never matches for a shared child (the same\n // identity bug setupChildSubscriptions() and collectChildSnapshots()\n // had). Double-dispose was harmless (dispose() is idempotent), but\n // one dedup idiom everywhere beats a latent inconsistency.\n if (!options?.preserveChildren) {\n const seen = new Set<MachinaInstance>();\n for (const stateName of Object.keys(this.states)) {\n const childLink = this.states[stateName]?._child as ChildLink | undefined;\n if (childLink && !seen.has(childLink.instance)) {\n seen.add(childLink.instance);\n childLink.dispose();\n }\n }\n }\n this.emitter.clear();\n // WeakMap entries are GC'd naturally — can't iterate, don't need to\n }\n\n // -------------------------------------------------------------------------\n // Internal\n // -------------------------------------------------------------------------\n\n /**\n * Walks all states at construction time, detects raw FSM instances assigned\n * to _child, and wraps them into ChildLink adapters via createChildLink().\n * Must run BEFORE setupChildSubscriptions() so the subscriptions see\n * ChildLink objects, not raw FSM instances.\n */\n private wrapChildLinks(): void {\n for (const stateName of Object.keys(this.states)) {\n const stateObj = this.states[stateName];\n const rawChild = stateObj?._child;\n if (!rawChild) {\n continue;\n }\n if (typeof rawChild !== \"object\") {\n throw new Error(\n `State \"${stateName}\"._child: expected an Fsm or BehavioralFsm instance, got ${String(rawChild)}`\n );\n }\n if (!(MACHINA_TYPE in rawChild)) {\n throw new Error(\n `State \"${stateName}\"._child: expected an Fsm or BehavioralFsm instance, got a plain object`\n );\n }\n stateObj._child = createChildLink(rawChild);\n }\n }\n\n /**\n * Walks all states at construction time, finds states with _child, and\n * subscribes once to each unique child's wildcard events. Subscriptions are\n * stored for cleanup in dispose(). We deduplicate by `childLink.instance`\n * (the underlying Fsm/BehavioralFsm instance), not the `ChildLink` wrapper —\n * `wrapChildLinks()` mints a fresh wrapper per declaring state, so a child\n * shared across states would otherwise get one subscription PER declaring\n * state, each independently walking known clients and relaying events. That\n * silently double-fires client-less relays (Fsm-child events, or a\n * BehavioralFsm child's custom `emit()` with no `client` in the payload)\n * whenever two different clients are active on two different declaring\n * names at once — the same wrapper-vs-instance identity bug\n * `collectChildSnapshots()` had before its #184 fix.\n */\n private setupChildSubscriptions(): void {\n const seenInstances = new Set<MachinaInstance>();\n\n for (const stateName of Object.keys(this.states)) {\n const stateObj = this.states[stateName];\n const childLink = stateObj?._child as ChildLink | undefined;\n\n if (!childLink || seenInstances.has(childLink.instance)) {\n continue;\n }\n seenInstances.add(childLink.instance);\n\n // Subscribe to all child events. We use the wildcard so we get\n // every event type in a single listener.\n const sub = childLink.onAny((eventName: string, data: unknown) => {\n // Nohandler from child = child couldn't handle the input.\n // Re-dispatch to parent via handleLocally() (not handle(), to\n // avoid re-entering the delegation path and looping).\n if (eventName === \"nohandler\") {\n const payload = data as {\n inputName: string;\n args: unknown[];\n client?: TClient;\n };\n\n if (payload.client !== undefined) {\n // BehavioralFsm child: payload includes the client.\n // Filter stale events: only bubble if this client is\n // currently in a state that has this child.\n this.bubbleNohandler(\n payload.client,\n childLink,\n payload.inputName,\n payload.args ?? []\n );\n } else {\n // Fsm child: single-client, no client in payload.\n // Walk all known clients and bubble for any that are\n // currently in a state with this child.\n for (const ref of this.knownClients) {\n const client = ref.deref();\n if (client === undefined) {\n // GC'd — clean up the dead ref\n this.knownClients.delete(ref);\n continue;\n }\n this.bubbleNohandler(\n client,\n childLink,\n payload.inputName,\n payload.args ?? []\n );\n }\n }\n return;\n }\n\n // All other child events: only relay if at least one client\n // is currently in a parent state that delegates to this child.\n // Prevents stale events from leaking through the parent emitter\n // when the child fires during construction or after the parent\n // has moved to a different state.\n const payload = data as Record<string, unknown> | undefined;\n if (payload && typeof payload === \"object\" && \"client\" in payload) {\n // BehavioralFsm child: payload includes the client.\n if (this.isChildActiveForClient(payload.client as TClient, childLink)) {\n (this.emitter as any).emit(eventName, data);\n }\n } else {\n // Fsm child (or payload without client): walk known clients,\n // relay if ANY client is currently in a state with this child.\n for (const ref of this.knownClients) {\n const client = ref.deref();\n if (!client) {\n this.knownClients.delete(ref);\n continue;\n }\n if (this.isChildActiveForClient(client, childLink)) {\n (this.emitter as any).emit(eventName, data);\n break; // one match is enough — relay once\n }\n }\n }\n });\n\n this.childSubscriptions.push(sub);\n }\n }\n\n /**\n * Bubbles a child nohandler to the parent for the given client.\n * Only fires if the client is currently in a state that has this childLink.\n * Extracted from the lambda in setupChildSubscriptions to keep it readable.\n */\n private bubbleNohandler(\n client: TClient,\n childLink: ChildLink,\n inputName: string,\n args: unknown[]\n ): void {\n if (!this.isChildActiveForClient(client, childLink)) {\n return;\n }\n // Safe to assert — isChildActiveForClient confirmed meta exists AND\n // the client is in a state whose _child matches childLink.\n const meta = this.clients.get(client)!;\n meta.currentActionArgs = args;\n this.handleLocally(client, inputName as TInputNames, args, meta);\n }\n\n /**\n * Returns true if the given client is currently in a parent state whose\n * _child resolves to the same underlying instance as childLink. Returns\n * false if the client has no meta (never initialized) or is in a state\n * with a different (or no) child.\n *\n * Compares `.instance`, not the `ChildLink` wrapper itself — setupChildSubscriptions()\n * dedupes subscriptions by instance and keeps only ONE representative wrapper\n * per shared child, so the client's actual active declaring state may hold a\n * DIFFERENT wrapper for that same instance (wrapChildLinks() mints one per\n * declaring state). Comparing wrappers directly would only ever match the one\n * declaring state whose wrapper happened to be kept for the subscription,\n * silently breaking relay for every other declaring name of a shared child.\n */\n private isChildActiveForClient(client: TClient, childLink: ChildLink): boolean {\n const meta = this.clients.get(client);\n if (!meta) {\n return false;\n }\n const parentStateObj = this.states[meta.state];\n const activeChildLink = parentStateObj?._child as ChildLink | undefined;\n return activeChildLink?.instance === childLink.instance;\n }\n\n /**\n * The inner handler dispatch — no delegation, no initialization side effects\n * beyond what getOrCreateClientMeta already did. Called by handle() after\n * the delegation check, and by the nohandler child listener for bubbling.\n */\n private handleLocally(\n client: TClient,\n inputName: TInputNames | string,\n args: unknown[],\n meta: ClientMeta<TStateNames>\n ): void {\n const stateObj = this.states[meta.state];\n const handler = stateObj?.[inputName] ?? stateObj?.[\"*\"];\n\n if (!handler) {\n this.emitter.emit(\"nohandler\", { inputName: inputName as string, args, client });\n meta.currentActionArgs = undefined;\n return;\n }\n\n try {\n this.emitter.emit(\"handling\", { inputName: inputName as string, client });\n\n const handlerArgs = this.buildHandlerArgs(client, inputName as string, meta);\n let targetState: TStateNames | void = undefined;\n\n if (typeof handler === \"string\") {\n targetState = handler as TStateNames;\n } else if (typeof handler === \"function\") {\n targetState = (handler as HandlerFn<TClient, TStateNames>)(handlerArgs, ...args);\n }\n\n this.emitter.emit(\"handled\", { inputName: inputName as string, client });\n\n if (typeof targetState === \"string\") {\n this.transition(client, targetState);\n }\n } finally {\n meta.currentActionArgs = undefined;\n }\n }\n\n private getOrCreateClientMeta(client: TClient): ClientMeta<TStateNames> {\n let meta = this.clients.get(client);\n if (meta) {\n return meta;\n }\n\n // State starts as undefined so transition()'s same-state check passes\n // (undefined !== initialState). The transition immediately sets state\n // to initialState before returning, so any code that triggered\n // initialization (handle/reset/transition) sees the correct state.\n // currentState() for uninitialized clients still returns undefined —\n // it reads the WeakMap directly without triggering init.\n meta = {\n state: undefined as unknown as TStateNames,\n deferredQueue: [],\n };\n this.clients.set(client, meta);\n // Track this client weakly so Fsm-child nohandler listeners can find\n // which clients to bubble to (Fsm events carry no client in payload).\n this.knownClients.add(new WeakRef(client));\n\n // v5-style active initialization: full transition into initialState.\n // Fires _onEnter, emits transitioning/transitioned, processes deferred\n // queue. _onExit is skipped (no state object for undefined).\n this.transition(client, this.initialState);\n\n return meta;\n }\n\n private buildHandlerArgs(\n client: TClient,\n inputName: string,\n meta: ClientMeta<TStateNames>\n ): HandlerArgs<TClient, TStateNames> {\n return {\n ctx: client,\n inputName,\n defer: (opts?: { until: TStateNames }) => {\n // Only meaningful inside a handle() context where currentActionArgs\n // is set. No-ops during lifecycle hooks (matches v5 behavior).\n if (!meta.currentActionArgs) {\n return;\n }\n const deferred: DeferredInput = {\n inputName,\n args: [...meta.currentActionArgs],\n untilState: opts?.until,\n };\n meta.deferredQueue.push(deferred);\n this.emitter.emit(\"deferred\", { inputName, client });\n },\n emit: (evtName: string, evtData?: unknown) => {\n (this.emitter as any).emit(evtName, evtData);\n },\n };\n }\n\n private processQueue(client: TClient, meta: ClientMeta<TStateNames>): void {\n const toReplay: DeferredInput[] = [];\n const remaining: DeferredInput[] = [];\n\n for (const item of meta.deferredQueue) {\n if (item.untilState === undefined || item.untilState === meta.state) {\n toReplay.push(item);\n } else {\n remaining.push(item);\n }\n }\n\n meta.deferredQueue = remaining;\n\n for (const item of toReplay) {\n this.handle(client, item.inputName as TInputNames, ...item.args);\n }\n }\n}\n\n// -----------------------------------------------------------------------------\n// createChildLink — internal adapter factory\n//\n// Wraps either an Fsm or BehavioralFsm child and presents a uniform interface\n// for the parent engine to call. Never exported. The MACHINA_TYPE discriminant\n// tells us which call shape to use.\n// -----------------------------------------------------------------------------\n\n/**\n * Internal factory called by `wrapChildLinks()` during construction.\n * Wraps a raw Fsm or BehavioralFsm instance in a uniform `ChildLink` adapter\n * so the parent engine doesn't need to know which type it's talking to.\n *\n * Users never call this directly — they assign an FSM instance to `_child`\n * in their state config and `wrapChildLinks()` handles the wrapping.\n */\nfunction createChildLink(child: any): ChildLink {\n if (!child || typeof child !== \"object\") {\n throw new Error(\n `createChildLink: expected an Fsm or BehavioralFsm instance, got ${String(child)}`\n );\n }\n const childType: string = child[MACHINA_TYPE];\n\n if (childType === \"BehavioralFsm\") {\n return {\n instance: child,\n canHandle(client: object, inputName: string): boolean {\n return child.canHandle(client, inputName);\n },\n handle(client: object, inputName: string, ...args: unknown[]): void {\n child.handle(client, inputName, ...args);\n },\n reset(client: object): void {\n child.transition(client, child.initialState);\n },\n onAny(callback: (eventName: string, data: unknown) => void): { off(): void } {\n return child.on(\"*\", callback);\n },\n compositeState(client: object): string {\n return child.compositeState(client);\n },\n rehydrate(client: object, compositeState: string): void {\n child.rehydrate(client, compositeState);\n },\n dehydrate(client: object, isOnActivePath: boolean): ClientSnapshot | undefined {\n return child.dehydrate(client, isOnActivePath);\n },\n planRehydrate(client: object, snapshot: ClientSnapshot): Array<() => void> {\n return child.planSnapshotWrites(client, snapshot);\n },\n dispose(): void {\n child.dispose();\n },\n };\n }\n\n if (childType === \"Fsm\") {\n return {\n instance: child,\n canHandle(_client: object, inputName: string): boolean {\n return child.canHandle(inputName);\n },\n handle(_client: object, inputName: string, ...args: unknown[]): void {\n child.handle(inputName, ...args);\n },\n reset(_client: object): void {\n child.reset();\n },\n onAny(callback: (eventName: string, data: unknown) => void): { off(): void } {\n return child.on(\"*\", callback);\n },\n compositeState(_client: object): string {\n return child.compositeState();\n },\n rehydrate(_client: object, _compositeState: string): void {\n // Fsm owns its context internally — there is no per-client state to restore.\n // Rehydrating into a BehavioralFsm hierarchy that has Fsm children is a\n // structural mismatch; throw immediately so the caller gets a clear signal.\n throw new Error(\n `rehydrate: cannot rehydrate an Fsm child. Fsm owns its own context; ` +\n `rehydrate is only valid for BehavioralFsm hierarchies.`\n );\n },\n dehydrate(_client: object, _isOnActivePath: boolean): ClientSnapshot | undefined {\n // Fsm owns a single global context shared across every client of the\n // parent BehavioralFsm — there's no per-client slice of it to snapshot.\n // collectChildSnapshots() only reaches this call when onPath is true\n // (it skips the call entirely otherwise), so this always throws.\n throw new Error(\n `dehydrate: cannot dehydrate an Fsm child. Fsm owns its own context; ` +\n `dehydrate is only valid for BehavioralFsm hierarchies.`\n );\n },\n planRehydrate(_client: object, _snapshot: ClientSnapshot): Array<() => void> {\n throw new Error(\n `rehydrate: cannot rehydrate an Fsm child. Fsm owns its own context; ` +\n `rehydrate is only valid for BehavioralFsm hierarchies.`\n );\n },\n dispose(): void {\n child.dispose();\n },\n };\n }\n\n throw new Error(\n `createChildLink: expected an Fsm or BehavioralFsm instance, ` +\n `got [MACHINA_TYPE] = ${String(childType ?? \"undefined\")}`\n );\n}\n\n/**\n * Create a behavioral FSM (one definition, many clients) from a config object.\n *\n * `TClient` can't be inferred from the config — there's no `context` property\n * at the FSM level for it to hook into, unlike `createFsm`'s `TCtx`. Supplying\n * it as a single explicit type argument (`createBehavioralFsm<Connection>({...})`)\n * doesn't work either: TypeScript has no partial type-argument inference, so\n * providing one of two type parameters disables inference for the other,\n * widening `TStates` to `Record<string, Record<string, unknown>>` and\n * discarding all literal-type validation.\n *\n * The fix is currying: call with zero arguments to fix `TClient`, then call\n * the returned function with the config to infer `TStates` from it, `const`\n * literal capture and all. This is the recommended way to type a client.\n *\n * If you'd rather skip the type argument entirely, annotate `ctx` inline on\n * at least one handler (e.g. `disconnect({ ctx }: { ctx: Connection }) {...}`)\n * and call with zero type arguments — `TClient` is then inferred from that\n * annotation. This only works when the annotation is visible directly on a\n * handler; it does NOT work through object spread (`...guards`), since\n * TypeScript won't look inside a spread for the annotation.\n *\n * @example\n * ```ts\n * interface Connection { url: string; retries: number; }\n *\n * const connFsm = createBehavioralFsm<Connection>()({\n * id: \"connectivity\",\n * initialState: \"disconnected\",\n * states: {\n * disconnected: { connect: \"connecting\" },\n * connecting: { connected: \"online\", failed: \"disconnected\" },\n * online: { disconnect: \"disconnected\" },\n * },\n * });\n *\n * const conn = { url: \"wss://example.com\", retries: 0 };\n * connFsm.handle(conn, \"connect\");\n * ```\n *\n * @example Inline-annotated `ctx`, zero type arguments\n * ```ts\n * const connFsm = createBehavioralFsm({\n * id: \"connectivity\",\n * initialState: \"disconnected\",\n * states: {\n * disconnected: {\n * connect({ ctx }: { ctx: Connection }) { return \"connecting\"; },\n * },\n * connecting: { connected: \"online\", failed: \"disconnected\" },\n * online: { disconnect: \"disconnected\" },\n * },\n * });\n * ```\n *\n * @example Declaring bubbled inputs under the curried form\n * ```ts\n * // childFsm fires \"phaseComplete\" at itself but never handles it — it\n * // expects whatever mounts it via `_child` to catch the bubble.\n * const childFsm = createBehavioralFsm<Connection>()({\n * id: \"child\",\n * initialState: \"green\",\n * bubbles: [\"phaseComplete\"],\n * states: {\n * green: { advance: \"red\" },\n * red: {},\n * },\n * });\n *\n * // Mounting it without handling (or re-declaring) \"phaseComplete\" is a\n * // compile error on `_child` below.\n * const parentFsm = createBehavioralFsm<Connection>()({\n * id: \"parent\",\n * initialState: \"active\",\n * states: {\n * active: {\n * _child: childFsm,\n * phaseComplete: \"cooldown\", // covers the bubble\n * },\n * cooldown: { advance: \"active\" },\n * },\n * });\n * ```\n */\nexport function createBehavioralFsm<TClient extends object>(): <\n const TStates extends Record<string, Record<string, unknown>>,\n TStateNames extends string = keyof TStates & string,\n TBubbles extends string = never,\n>(\n config: FsmConfig<TClient, TStates, TStateNames, TBubbles>\n) => BehavioralFsm<\n TClient,\n // Both unions inlined rather than written as StateNamesOf/InputNamesOf —\n // see the comment in fsm.ts's createFsm: a named alias reference keeps its\n // alias symbol in compiler diagnostics, so rejected handle()/transition()\n // calls would display \"InputNamesOf<{...}>\" instead of the flat union.\n keyof TStates & string,\n | Exclude<{ [S in keyof TStates]: keyof TStates[S] & string }[keyof TStates], SpecialStateKeys>\n | {\n [S in keyof TStates]: TStates[S] extends { _child: infer C }\n ? InputNamesOfInstance<C>\n : never;\n }[keyof TStates]\n | TBubbles,\n TBubbles\n>;\nexport function createBehavioralFsm<\n TClient extends object,\n const TStates extends Record<string, Record<string, unknown>>,\n TStateNames extends string = keyof TStates & string,\n TBubbles extends string = never,\n>(\n config: FsmConfig<TClient, TStates, TStateNames, TBubbles>\n): BehavioralFsm<\n TClient,\n // Both unions inlined rather than written as StateNamesOf/InputNamesOf —\n // see the comment in fsm.ts's createFsm: a named alias reference keeps its\n // alias symbol in compiler diagnostics, so rejected handle()/transition()\n // calls would display \"InputNamesOf<{...}>\" instead of the flat union.\n keyof TStates & string,\n | Exclude<{ [S in keyof TStates]: keyof TStates[S] & string }[keyof TStates], SpecialStateKeys>\n | {\n [S in keyof TStates]: TStates[S] extends { _child: infer C }\n ? InputNamesOfInstance<C>\n : never;\n }[keyof TStates]\n | TBubbles,\n TBubbles\n>;\nexport function createBehavioralFsm<\n TClient extends object,\n const TStates extends Record<string, Record<string, unknown>>,\n TStateNames extends string = keyof TStates & string,\n TBubbles extends string = never,\n>(config?: FsmConfig<TClient, TStates, TStateNames, TBubbles>) {\n if (config === undefined) {\n return (curriedConfig: FsmConfig<TClient, TStates, TStateNames, TBubbles>) =>\n new BehavioralFsm(\n curriedConfig as FsmConfig<TClient, Record<string, Record<string, unknown>>>\n );\n }\n return new BehavioralFsm(config as FsmConfig<TClient, Record<string, Record<string, unknown>>>);\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// =============================================================================\n// fsm.ts — Single-client FSM convenience wrapper\n//\n// Fsm wraps a BehavioralFsm, using the config's `context` object as the\n// internal client. Same engine, same lifecycle, but without the client\n// parameter on every method call.\n//\n// =============================================================================\n\nimport { Emitter, type Subscription } from \"./emitter\";\nimport { BehavioralFsm } from \"./behavioral-fsm\";\nimport {\n MACHINA_TYPE,\n type FsmConfig,\n type FsmEventMap,\n type InputNamesOfInstance,\n type DisposeOptions,\n type SpecialStateKeys,\n} from \"./types\";\n\n/**\n * Single-client FSM. Wraps a BehavioralFsm and uses the config's `context`\n * object as the implicit client, so callers never pass a client argument.\n *\n * Prefer `createFsm()` over constructing this directly — the factory infers\n * all generic parameters from the config object.\n *\n * All public methods silently no-op after `dispose()` is called.\n *\n * @typeParam TCtx - The context type, inferred from `config.context`.\n * @typeParam TStateNames - String literal union of valid state names.\n * @typeParam TInputNames - String literal union of valid input names.\n * @typeParam TBubbles - String literal union of inputs this FSM declares via\n * `bubbles`. Type-only — carried so `BubblesOfInstance` can extract it from\n * a constructed instance; nothing at runtime reads this generic.\n */\nexport class Fsm<\n TCtx extends object,\n TStateNames extends string,\n TInputNames extends string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- phantom marker: never referenced in the class body, only read back out externally via BubblesOfInstance's `infer`.\n TBubbles extends string = never,\n> {\n readonly id: string;\n readonly initialState: TStateNames;\n // Type discriminant — lets ChildLink adapter identify this at runtime\n readonly [MACHINA_TYPE] = \"Fsm\" as const;\n // Public so inspection tooling (machina-inspect) can read the state graph.\n // Set from config.states — same object reference as BehavioralFsm.states,\n // so ChildLink wrapping done by wrapChildLinks() is reflected automatically.\n readonly states: Record<string, Record<string, unknown>>;\n private readonly bfsm: BehavioralFsm<TCtx, TStateNames, TInputNames>;\n // Public readonly so external tooling (and walkAll's invariant) can read\n // context without needing to plumb it through every call site. The context\n // is already visible inside handlers via ctx — this just makes it accessible\n // from outside the FSM instance too.\n readonly context: TCtx;\n // FsmEventMap is an interface, which lacks the implicit index signature\n // that Emitter's Record<string, unknown> constraint needs. The mapped type\n // BehavioralFsmEventMap doesn't have this issue. Intersect with Record to\n // satisfy the constraint while preserving typed event signatures.\n private readonly emitter = new Emitter<FsmEventMap<TStateNames> & Record<string, unknown>>();\n private disposed = false;\n\n constructor(config: FsmConfig<TCtx, Record<string, Record<string, unknown>>>) {\n this.id = config.id;\n this.initialState = config.initialState as TStateNames;\n this.context = (config.context ?? {}) as TCtx;\n\n this.bfsm = new BehavioralFsm(config);\n // Assign after bfsm construction so ChildLink wrapping (done by\n // BehavioralFsm.wrapChildLinks) is already reflected in config.states.\n this.states = config.states as Record<string, Record<string, unknown>>;\n\n // Relay events from BehavioralFsm → Fsm emitter, stripping the\n // `client` field from built-in event payloads. Custom events (from\n // handler emit() or public emit()) pass through unchanged.\n this.bfsm.on(\"*\", (eventName: string, data: unknown) => {\n if (data && typeof data === \"object\" && \"client\" in data) {\n const { client: _, ...payload } = data as Record<string, unknown>;\n (this.emitter as any).emit(eventName, payload);\n } else {\n (this.emitter as any).emit(eventName, data);\n }\n });\n\n // Eager init: call bfsm.transition() to enter initialState now.\n // bfsm.transition() calls getOrCreateClientMeta(), which bootstraps\n // the client and calls this.transition() internally (the real init,\n // which fires _onEnter and emits transitioning/transitioned). After\n // that inner call completes, meta.state === initialState, so the\n // original bfsm.transition() call hits the same-state guard and exits.\n this.bfsm.transition(this.context, config.initialState as TStateNames);\n }\n\n // -------------------------------------------------------------------------\n // Public API — same as BehavioralFsm, minus the client parameter\n // -------------------------------------------------------------------------\n\n /**\n * Dispatch an input to the current state's handler.\n * If a `_child` FSM in the current state can handle it, delegation occurs\n * there first; unhandled inputs bubble up to the parent.\n * No-ops silently when disposed.\n */\n handle(inputName: TInputNames, ...args: unknown[]): void {\n if (this.disposed) {\n return;\n }\n this.bfsm.handle(this.context, inputName, ...args);\n }\n\n /**\n * Returns true if the current state has a handler for `inputName`\n * (or a catch-all `\"*\"` handler), or if the current state's `_child`\n * chain can handle it (checked recursively, matching `handle()`'s\n * delegation reach). Does not trigger initialization or any side\n * effects. Returns false when disposed.\n */\n canHandle(inputName: string): boolean {\n if (this.disposed) {\n return false;\n }\n return this.bfsm.canHandle(this.context, inputName);\n }\n\n /**\n * Transition back to `initialState`, firing `_onEnter` and lifecycle\n * events as if entering it fresh. No-ops silently when disposed.\n */\n reset(): void {\n if (this.disposed) {\n return;\n }\n this.bfsm.reset(this.context);\n }\n\n /**\n * Returns the current state name. Always defined — Fsm eagerly\n * initializes into `initialState` during construction.\n */\n currentState(): TStateNames {\n return this.bfsm.currentState(this.context) as TStateNames;\n }\n\n /**\n * Directly transition to `toState`, firing `_onExit`, `_onEnter`, and\n * lifecycle events. Same-state transitions are silently ignored.\n * No-ops when disposed.\n */\n transition(toState: TStateNames): void {\n if (this.disposed) {\n return;\n }\n this.bfsm.transition(this.context, toState);\n }\n\n /**\n * Returns the current state as a dot-delimited path that includes\n * any active child FSM states (e.g. `\"active.connecting.retrying\"`).\n * Returns just the current state name when no child is active.\n */\n compositeState(): string {\n return this.bfsm.compositeState(this.context);\n }\n\n /**\n * Subscribe to a built-in lifecycle event or the wildcard.\n *\n * Named overload: typed payload, no event name in callback.\n * Wildcard (`\"*\"`): receives `(eventName, data)` for every event.\n * Returns a no-op `Subscription` when disposed.\n */\n on<K extends keyof FsmEventMap<TStateNames> & string>(\n eventName: K,\n callback: (data: FsmEventMap<TStateNames>[K]) => void\n ): Subscription;\n on(eventName: \"*\", callback: (eventName: string, data: unknown) => void): Subscription;\n on(eventName: string, callback: (...args: any[]) => void): Subscription {\n if (this.disposed) {\n return { off() {} };\n }\n return this.emitter.on(eventName as any, callback as any);\n }\n\n /**\n * Emit a custom event through the FSM. Built-in lifecycle events are\n * emitted automatically — this is for user-defined events from handlers.\n * Routes through the BehavioralFsm so all relay paths are consistent.\n * No-ops when disposed.\n */\n emit(eventName: string, data?: unknown): void {\n if (this.disposed) {\n return;\n }\n // Route through BehavioralFsm so custom events from handler emit()\n // and public fsm.emit() take the same path through the relay.\n this.bfsm.emit(eventName, data);\n }\n\n /**\n * Permanently shut down this FSM. Irreversible — all subsequent method\n * calls become silent no-ops. Clears all listeners and cascades disposal\n * to child FSMs (unless `preserveChildren` is set).\n */\n dispose(options?: DisposeOptions): void {\n this.disposed = true;\n this.bfsm.dispose(options);\n this.emitter.clear();\n }\n}\n\n/**\n * Create a single-client FSM from a config object.\n *\n * Generic parameters are inferred automatically:\n * - `TCtx` comes from `config.context` (defaults to `{}` if omitted).\n * - `TStates` is captured with `const` inference to preserve string literal\n * types, enabling compile-time validation of transition targets and `handle()`\n * input names.\n *\n * State names, input names, and all handler signatures derive from `TStates`.\n *\n * @example\n * ```ts\n * const light = createFsm({\n * id: \"traffic-light\",\n * initialState: \"green\",\n * context: { tickCount: 0 },\n * states: {\n * green: { timeout: \"yellow\" },\n * yellow: { timeout: \"red\" },\n * red: { timeout: \"green\" },\n * },\n * });\n *\n * light.handle(\"timeout\"); // transitions green → yellow\n * ```\n */\nexport function createFsm<\n TCtx extends object = Record<string, never>,\n const TStates extends Record<string, Record<string, unknown>> = Record<\n string,\n Record<string, unknown>\n >,\n TStateNames extends string = keyof TStates & string,\n TBubbles extends string = never,\n>(\n config: FsmConfig<TCtx, TStates, TStateNames, TBubbles>\n): Fsm<\n TCtx,\n // Both unions below are inlined rather than written as `StateNamesOf<TStates>`\n // / `InputNamesOf<TStates>`: a named alias reference keeps its alias symbol\n // in compiler diagnostics, so a rejected handle()/transition() call would\n // display \"InputNamesOf<{...entire config...}>\" instead of the flat literal\n // union. Inlining the aliases' right-hand sides forces eager evaluation and\n // keeps error text readable. Display-only — hover types are identical.\n keyof TStates & string,\n | Exclude<{ [S in keyof TStates]: keyof TStates[S] & string }[keyof TStates], SpecialStateKeys>\n | {\n [S in keyof TStates]: TStates[S] extends { _child: infer C }\n ? InputNamesOfInstance<C>\n : never;\n }[keyof TStates]\n | TBubbles,\n TBubbles\n> {\n return new Fsm(config as FsmConfig<TCtx, Record<string, Record<string, unknown>>>);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machina",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Focused finite state machines for JavaScript and TypeScript. States in, states out.",
|
|
5
5
|
"author": "Jim Cowart (http://ifandelse.com)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"sideEffects": false,
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"eslint": "^9.39.
|
|
48
|
-
"jest": "^30.
|
|
47
|
+
"eslint": "^9.39.4",
|
|
48
|
+
"jest": "^30.3.0",
|
|
49
49
|
"ts-jest": "^29.4.6",
|
|
50
50
|
"tsdown": "^0.20.3",
|
|
51
51
|
"typescript": "^5.9.3"
|