//#region src/emitter.d.ts /** * Returned by `on()`. Call `off()` to remove the subscription. * Safe to call multiple times — subsequent calls are no-ops because * the underlying Set deduplicates membership. */ type Subscription = { off(): void; }; //#endregion //#region src/types.d.ts /** * Keys on a state object that have special meaning and are NOT input names. * Used by InputNamesOf to filter these out when collecting input names. */ type SpecialStateKeys = "_onEnter" | "_onExit" | "_child" | "*"; /** * Extracts state names as a string literal union from a states config object. * * @example * ```ts * type S = StateNamesOf<{ green: {...}, yellow: {...}, red: {...} }>; * // => "green" | "yellow" | "red" * ``` */ type StateNamesOf = keyof TStates & string; /** * Extracts input names as a string literal union from a states config object. * Collects all handler keys across ALL states, then strips out lifecycle hooks * and special keys (_onEnter, _onExit, _child, *). * * This is what flows into `handle(inputName)` to provide compile-time * validation of input names. * * @example * ```ts * type I = InputNamesOf<{ * idle: { start: "running", reset: fn }; * running: { pause: "paused", stop: "idle" }; * }>; * // => "start" | "reset" | "pause" | "stop" * ``` * * How it works: * 1. `{ [S in keyof TStates]: keyof TStates[S] & string }` — maps each state * to the union of its handler key names * 2. `[keyof TStates]` — collapses the mapped type into a flat union of ALL * handler keys across every state * 3. `Exclude<..., SpecialStateKeys>` — strips lifecycle/special keys */ type InputNamesOf = Exclude<{ [S in keyof TStates]: keyof TStates[S] & string }[keyof TStates], SpecialStateKeys>; /** * The single combined object passed to every handler. * * Handlers return a state name to transition, or void to stay put. * This replaces imperative `transition()` calls — closer to gen_fsm's * return-based model and symmetrical with string shorthand handlers. * * @typeParam TCtx - The context type. For Fsm this is the config-defined * context object. For BehavioralFsm this is the client object itself. * @typeParam TStateNames - String literal union of valid state names. * Defaults to `string` for loose usage; the factory functions narrow this * to the actual state names inferred from the config. * * @example * ```ts * // Conditional transition — return the target state: * timeout({ ctx }) { * if (ctx.tickCount >= 3) return "yellow"; * } * * // Side effects without transition — return nothing: * tick({ ctx }) { * ctx.tickCount++; * } * * // In a catch-all — inputName tells you what was received: * "*"({ inputName }) { * console.log(`unhandled input: ${inputName}`); * } * ``` */ interface HandlerArgs { /** The context (Fsm) or client object (BehavioralFsm) */ ctx: TCtx; /** * The name of the input currently being handled. * * Typed as `string` rather than the inferred input union because: * 1. Inside a named handler you already know the input name * 2. In a catch-all (*) handler it could be anything * 3. Narrowing to the literal per-handler would require complex * mapped types for zero practical benefit */ inputName: string; /** * Defer the current input for replay after a future transition. * Erlang's selective receive, in JS form. * * @example * ```ts * // Replay on the next transition to any state * defer(); * * // Replay only when entering "yellow" * defer({ until: "yellow" }); * ``` */ defer(opts?: { until: TStateNames; }): void; /** * Emit a custom event through the FSM's emitter. * Built-in events (transitioning, transitioned, etc.) are emitted * automatically by the FSM engine — this is for user-defined events. */ emit(eventName: string, data?: unknown): void; } /** * A function handler for state inputs, lifecycle hooks (_onEnter, _onExit), * and catch-all (*) handlers. * * **Return value determines transition:** * - Return a valid state name → FSM transitions to that state * - Return void/undefined → FSM stays in the current state * * This mirrors gen_fsm's `{next_state, StateName, NewStateData}` return. * Guards are just `if` statements. Actions are just code before the return. * * The `...extra` rest parameter captures additional arguments passed through * `handle(inputName, ...extraArgs)`. These are untyped (`unknown[]`) because * correlating per-input arg types with handle() call sites would require * prohibitively complex mapped types for minimal benefit. * * @example * ```ts * // Side effects only, no transition: * tick({ ctx }) { ctx.tickCount++; } * * // Conditional transition (replaces guard + target): * timeout({ ctx }) { * if (ctx.tickCount >= 3) return "yellow"; * } * * // Unconditional transition with side effect (replaces action + target): * timeout({ ctx }) { * console.log("transitioning after", ctx.tickCount, "ticks"); * return "yellow"; * } * * // Handler with extra args passed via handle("success", responseData): * success({ ctx }, data) { ctx.result = data; } * ``` */ type HandlerFn = (args: HandlerArgs, ...extra: unknown[]) => TStateNames | void; /** * The union of valid handler definition forms for a state input. * * - `TStateNames` — string shorthand, auto-transitions to that state * - `HandlerFn` — function that returns a state name (transition) or void (stay) * * @example * ```ts * states: { * green: { * timeout: "yellow", // string shorthand * tick({ ctx }) { ctx.tickCount++; }, // function, no transition * emergency({ ctx }) { // function, conditional transition * if (ctx.severity > 5) return "red"; * }, * }, * } * ``` */ /** * The union of valid handler definition forms for a state input. * * - `TStateNames` — string shorthand, auto-transitions to that state * - `HandlerFn` — function that returns a state name (transition) or void (stay) * - `MachinaInstance` — included to satisfy TypeScript's structural widening * when `ValidateStates` falls back to its constraint type. The per-key * restriction on `_child` is still enforced by `ValidateStates`; this * union member just prevents the inference engine from rejecting * `_child: childFsm` before the conditional mapping can evaluate it. * * @example * ```ts * states: { * green: { * timeout: "yellow", // string shorthand * tick({ ctx }) { ctx.tickCount++; }, // function, no transition * emergency({ ctx }) { // function, conditional transition * if (ctx.severity > 5) return "red"; * }, * }, * } * ``` */ type HandlerDef = TStateNames | HandlerFn | MachinaInstance; /** * Validates and constrains the states object at the type level. * * This is a mapped type that walks every state and every property within * each state, assigning the correct expected type based on the property key: * * | Key | Expected type | * |------------------|-----------------------------------------| * | `_onEnter` | HandlerFn (lifecycle hook) | * | `_onExit` | HandlerFn (lifecycle hook) | * | `_child` | MachinaInstance (Fsm or BehavioralFsm) | * | `*` | HandlerFn (catch-all) | * | anything else | HandlerDef (string or fn) | * * @typeParam TCtx - Context/client type, flows into handler signatures * @typeParam TStates - The literal states object type captured by the * factory function's generic parameter */ type ValidateStates>> = { [S in keyof TStates]: { [K in keyof TStates[S]]: K extends "_onEnter" | "_onExit" ? HandlerFn : K extends "_child" ? MachinaInstance : K extends "*" ? HandlerFn : HandlerDef } }; /** * Configuration object for creating an FSM. * * @typeParam TCtx - The context type (Fsm) or client type (BehavioralFsm). * For Fsm, this is inferred from the `context` property. For BehavioralFsm, * it's the client object type provided explicitly or as a generic parameter. * * @typeParam TStates - The literal states object type. Captured by the factory * function's generic parameter (ideally with `const` to preserve string * literal types). Defaults to a loose record for unconstrained usage. * * @example * ```ts * // TCtx inferred as { tickCount: number }, TStates inferred from states object: * createFsm({ * id: "traffic-light", * initialState: "green", // validated against state keys * context: { tickCount: 0 }, // inference site for TCtx * states: { * green: { timeout: "yellow" }, // "yellow" validated against state keys * yellow: { timeout: "red" }, * red: { timeout: "green" }, * }, * }); * ``` */ interface FsmConfig> = Record>> { /** Unique identifier for this FSM */ id: string; /** * The state to start in. Must be a key of `states`. * * Wrapped in NoInfer to prevent TypeScript from using this value as an * inference site for TStates. Without it, `initialState: "green"` could * narrow TStates to only have a "green" key. We want inference to come * exclusively from the `states` property. */ initialState: NoInfer; /** * Initial context data. The type is inferred from this value and flows * into every handler's `ctx` parameter. * * For BehavioralFsm, this property is optional and serves only as a * type constraint — the client object IS the context. */ context?: TCtx; /** State definitions. Keys become the state name union. */ states: ValidateStates; } /** * Symbol used as a property key to identify machina FSM instances at runtime. * Each class stamps itself with a MachinaType value so the ChildLink adapter * can dispatch handle()/canHandle()/reset() correctly without circular imports. */ declare const MACHINA_TYPE: unique symbol; /** * Discriminant values stamped onto FSM instances via `MACHINA_TYPE`. * Used by the `ChildLink` adapter to dispatch calls correctly without * importing either class directly (which would create circular dependencies). */ type MachinaType = "Fsm" | "BehavioralFsm"; /** Structural type matching any machina FSM instance (Fsm or BehavioralFsm) */ type MachinaInstance = { readonly [MACHINA_TYPE]: MachinaType; }; /** * Internal adapter that wraps either an Fsm or BehavioralFsm child, * presenting a uniform API for parent-initiated delegation. */ interface ChildLink { /** Check if the child's current state can handle this input */ canHandle(client: object, inputName: string): boolean; /** Dispatch the input to the child */ handle(client: object, inputName: string, ...args: unknown[]): void; /** Reset the child to its initialState */ reset(client: object): void; /** Subscribe to all child events (wildcard). Returns unsubscribe fn. */ onAny(callback: (eventName: string, data: unknown) => void): { off(): void; }; /** The child FSM's compositeState for the given client */ compositeState(client: object): string; /** * Silently place `client` at the given composite state within the child hierarchy. * Throws for Fsm children (no per-client state to rehydrate). */ rehydrate(client: object, compositeState: string): void; /** Dispose the child FSM */ dispose(): void; /** * The raw Fsm or BehavioralFsm instance this ChildLink wraps. * Exposed for inspection tooling (machina-inspect) — allows external * tools to introspect child graph structure without reaching through * private fields. */ instance: MachinaInstance; } /** * Options for FSM disposal. */ interface DisposeOptions { /** * When true, child FSMs declared via _child are NOT disposed. * Default: false (children ARE disposed along with the parent). */ preserveChildren?: boolean; } /** * Built-in event map for Fsm instances. * Payloads do NOT include a client reference (Fsm is its own client). * * @typeParam TStateNames - The state name union, flows into transition * event payloads so fromState/toState are narrowed to actual state names. */ interface FsmEventMap { /** Fired just before a state transition occurs */ transitioning: { fromState: TStateNames; toState: TStateNames; }; /** Fired just after a state transition completes */ transitioned: { fromState: TStateNames; toState: TStateNames; }; /** Fired when an input is about to be dispatched to a handler */ handling: { inputName: string; }; /** Fired after an input has been successfully handled */ handled: { inputName: string; }; /** Fired when an input has no matching handler in the current state */ nohandler: { inputName: string; args: unknown[]; }; /** Fired when a transition targets a state that doesn't exist */ invalidstate: { stateName: string; }; /** Fired when an input is deferred for later replay */ deferred: { inputName: string; }; } /** * Built-in event map for BehavioralFsm instances. * Every payload is intersected with `{ client: TClient }` so subscribers * can identify which client the event pertains to. * * @typeParam TClient - The client object type * @typeParam TStateNames - The state name union */ type BehavioralFsmEventMap = { [K in keyof FsmEventMap]: FsmEventMap[K] & { client: TClient; } }; //#endregion //#region src/fsm.d.ts /** * Single-client FSM. Wraps a BehavioralFsm and uses the config's `context` * object as the implicit client, so callers never pass a client argument. * * Prefer `createFsm()` over constructing this directly — the factory infers * all generic parameters from the config object. * * All public methods silently no-op after `dispose()` is called. * * @typeParam TCtx - The context type, inferred from `config.context`. * @typeParam TStateNames - String literal union of valid state names. * @typeParam TInputNames - String literal union of valid input names. */ declare class Fsm { readonly id: string; readonly initialState: TStateNames; readonly [MACHINA_TYPE]: "Fsm"; readonly states: Record>; private readonly bfsm; readonly context: TCtx; private readonly emitter; private disposed; constructor(config: FsmConfig>>); /** * Dispatch an input to the current state's handler. * If a `_child` FSM in the current state can handle it, delegation occurs * there first; unhandled inputs bubble up to the parent. * No-ops silently when disposed. */ handle(inputName: TInputNames, ...args: unknown[]): void; /** * Returns true if the current state has a handler for `inputName` * (or a catch-all `"*"` handler). Does not trigger initialization * or any side effects. Returns false when disposed. */ canHandle(inputName: string): boolean; /** * Transition back to `initialState`, firing `_onEnter` and lifecycle * events as if entering it fresh. No-ops silently when disposed. */ reset(): void; /** * Returns the current state name. Always defined — Fsm eagerly * initializes into `initialState` during construction. */ currentState(): TStateNames; /** * Directly transition to `toState`, firing `_onExit`, `_onEnter`, and * lifecycle events. Same-state transitions are silently ignored. * No-ops when disposed. */ transition(toState: TStateNames): void; /** * Returns the current state as a dot-delimited path that includes * any active child FSM states (e.g. `"active.connecting.retrying"`). * Returns just the current state name when no child is active. */ compositeState(): string; /** * Subscribe to a built-in lifecycle event or the wildcard. * * Named overload: typed payload, no event name in callback. * Wildcard (`"*"`): receives `(eventName, data)` for every event. * Returns a no-op `Subscription` when disposed. */ on & string>(eventName: K, callback: (data: FsmEventMap[K]) => void): Subscription; on(eventName: "*", callback: (eventName: string, data: unknown) => void): Subscription; /** * Emit a custom event through the FSM. Built-in lifecycle events are * emitted automatically — this is for user-defined events from handlers. * Routes through the BehavioralFsm so all relay paths are consistent. * No-ops when disposed. */ emit(eventName: string, data?: unknown): void; /** * Permanently shut down this FSM. Irreversible — all subsequent method * calls become silent no-ops. Clears all listeners and cascades disposal * to child FSMs (unless `preserveChildren` is set). */ dispose(options?: DisposeOptions): void; } /** * Create a single-client FSM from a config object. * * Generic parameters are inferred automatically: * - `TCtx` comes from `config.context` (defaults to `{}` if omitted). * - `TStates` is captured with `const` inference to preserve string literal * types, enabling compile-time validation of transition targets and `handle()` * input names. * * State names, input names, and all handler signatures derive from `TStates`. * * @example * ```ts * const light = createFsm({ * id: "traffic-light", * initialState: "green", * context: { tickCount: 0 }, * states: { * green: { timeout: "yellow" }, * yellow: { timeout: "red" }, * red: { timeout: "green" }, * }, * }); * * light.handle("timeout"); // transitions green → yellow * ``` */ declare function createFsm, const TStates extends Record> = Record>>(config: FsmConfig): Fsm, InputNamesOf>; //#endregion //#region src/behavioral-fsm.d.ts /** * Defines FSM behavior (states + transitions) while tracking per-client state * in a `WeakMap`. A single `BehavioralFsm` instance can drive any number of * independent client objects simultaneously — each gets its own state, * deferred queue, and lifecycle. * * Prefer `createBehavioralFsm()` over constructing this directly — the factory * infers all generic parameters from the config object. * * All public methods silently no-op after `dispose()` is called. * * @typeParam TClient - The client object type. Must be an object (non-primitive) * so it can serve as a WeakMap key. * @typeParam TStateNames - String literal union of valid state names. * @typeParam TInputNames - String literal union of valid input names. */ declare class BehavioralFsm { readonly id: string; readonly initialState: TStateNames; readonly [MACHINA_TYPE]: "BehavioralFsm"; readonly states: Record>; private readonly emitter; private readonly clients; private readonly knownClients; private readonly childSubscriptions; private disposed; private transitionDepth; constructor(config: FsmConfig>>); /** * Dispatch an input to the given client's current state handler. * * Delegation order: if the current state has a `_child` FSM that can * handle the input, it is dispatched there. If the child emits `nohandler`, * the input bubbles up to this FSM's local handler. If no handler exists * here either, `nohandler` is emitted on this FSM's emitter. * * No-ops silently when disposed. */ handle(client: TClient, inputName: TInputNames, ...args: unknown[]): void; /** * Returns true if the client's current state has a handler for `inputName` * (or a catch-all `"*"` handler). Does NOT initialize the client — no * `_onEnter`, no events, no side effects. Unseen clients are treated as * if they were already in `initialState`. Returns false when disposed. */ canHandle(client: TClient, inputName: string): boolean; /** * Transition the client back to `initialState`, firing `_onEnter` and * lifecycle events as if entering it fresh. No-ops when disposed. */ reset(client: TClient): void; /** * Returns the client's current state, or `undefined` if the client has * never been initialized (i.e. `handle()`, `transition()`, or `reset()` * have never been called for it). Does NOT trigger initialization. */ currentState(client: TClient): TStateNames | undefined; /** * Directly transition `client` to `toState`, running the full lifecycle: * `_onExit` for the current state → `transitioning` event → update state → * `_onEnter` for new state → `transitioned` event → child reset → deferred * queue replay → bounce (if `_onEnter` returned a state name). * * Same-state transitions are silently ignored. Transitions to unknown state * names emit `invalidstate` instead of throwing. Throws if the transition * depth exceeds `MAX_TRANSITION_DEPTH` (likely an `_onEnter` → transition loop). * * No-ops when disposed. */ transition(client: TClient, toState: TStateNames): void; /** * Returns the client's state as a dot-delimited path including any active * child FSM states (e.g. `"active.connecting.retrying"`). Returns just the * current state name when no child is active. Returns `""` for clients that * have never been initialized (unlike `currentState()` which returns `undefined`). */ compositeState(client: TClient): string; /** * Silently place `client` at `compositeState` with no lifecycle activity. * Designed to work with `compositeState()`, which produces the dot-path * string that `rehydrate()` consumes. * * No `_onEnter`, no `_onExit`, no `transitioning`/`transitioned` events. * If `compositeState` is a dot-delimited path (`"active.connecting"`), the client * is placed at each level of the hierarchy in turn. Subsequent `handle()` calls * proceed as if the client had reached that state through normal transitions. * * Throws synchronously for unknown state names, missing `_child` at an inner level, * or Fsm children in the hierarchy (Fsm owns its context; nothing to rehydrate there). * * No-ops silently when disposed. */ rehydrate(client: TClient, compositeState: string): void; /** * Subscribe to a built-in lifecycle event or the wildcard. * * Named overload: typed payload includes `{ client: TClient }` so you can * identify which client the event pertains to. Wildcard (`"*"`) receives * `(eventName, data)` for every event. Returns a no-op `Subscription` * when disposed. */ on & string>(eventName: K, callback: (data: BehavioralFsmEventMap[K]) => void): Subscription; on(eventName: "*", callback: (eventName: string, data: unknown) => void): Subscription; /** * Emit a custom event through the FSM. Built-in lifecycle events are * emitted automatically — this is for user-defined events from handlers. * No-ops when disposed. */ emit(eventName: string, data?: unknown): void; /** * Permanently shut down this FSM. Irreversible — all subsequent method * calls become silent no-ops. Tears down child subscriptions, clears all * listeners, and cascades disposal to child FSMs (unless `preserveChildren` * is set). The same child appearing in multiple states is disposed once. */ dispose(options?: DisposeOptions): void; /** * Walks all states at construction time, detects raw FSM instances assigned * to _child, and wraps them into ChildLink adapters via createChildLink(). * Must run BEFORE setupChildSubscriptions() so the subscriptions see * ChildLink objects, not raw FSM instances. */ private wrapChildLinks; /** * Walks all states at construction time, finds states with _child, and * subscribes once to each unique child's wildcard events. Subscriptions are * stored for cleanup in dispose(). We deduplicate by child reference to * avoid double-subscribing when the same child appears in multiple states. */ private setupChildSubscriptions; /** * Bubbles a child nohandler to the parent for the given client. * Only fires if the client is currently in a state that has this childLink. * Extracted from the lambda in setupChildSubscriptions to keep it readable. */ private bubbleNohandler; /** * Returns true if the given client is currently in a parent state whose * _child is the specified childLink. Returns false if the client has no * meta (never initialized) or is in a state with a different (or no) child. */ private isChildActiveForClient; /** * The inner handler dispatch — no delegation, no initialization side effects * beyond what getOrCreateClientMeta already did. Called by handle() after * the delegation check, and by the nohandler child listener for bubbling. */ private handleLocally; private getOrCreateClientMeta; private buildHandlerArgs; private processQueue; } /** * Create a behavioral FSM (one definition, many clients) from a config object. * * Generic parameters are inferred automatically: * - `TClient` must be provided explicitly as a type parameter (it can't be * inferred from the config since no `context` property exists at the FSM level). * - `TStates` is captured with `const` inference to preserve string literal * types, enabling compile-time validation of transition targets and `handle()` * input names. * * State names, input names, and all handler signatures derive from `TStates`. * * @example * ```ts * interface Connection { url: string; retries: number; } * * const connFsm = createBehavioralFsm({ * id: "connectivity", * initialState: "disconnected", * states: { * disconnected: { connect: "connecting" }, * connecting: { connected: "online", failed: "disconnected" }, * online: { disconnect: "disconnected" }, * }, * }); * * const conn = { url: "wss://example.com", retries: 0 }; * connFsm.handle(conn, "connect"); * ``` */ declare function createBehavioralFsm>>(config: FsmConfig): BehavioralFsm, InputNamesOf>; //#endregion export { BehavioralFsm, type BehavioralFsmEventMap, type ChildLink, type DisposeOptions, Fsm, type FsmConfig, type FsmEventMap, type HandlerArgs, type HandlerDef, type HandlerFn, type InputNamesOf, MACHINA_TYPE, type MachinaInstance, type StateNamesOf, type Subscription, createBehavioralFsm, createFsm }; //# sourceMappingURL=index.d.cts.map