@ue-too/being 0.12.0 → 0.13.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/hierarchical-example.d.ts +13 -0
- package/hierarchical.d.ts +132 -0
- package/index.d.ts +2 -0
- package/index.js +2 -2
- package/index.js.map +6 -4
- package/interface.d.ts +27 -13
- package/package.json +1 -1
- package/schema-factory-example.d.ts +9 -0
- package/schema-factory.d.ts +267 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Hierarchical State Machine POC
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This example demonstrates how to use hierarchical state machines with
|
|
6
|
+
* composite states that contain child state machines.
|
|
7
|
+
*
|
|
8
|
+
* Scenario: A media player with hierarchical states
|
|
9
|
+
* - Top level: IDLE, PLAYING, PAUSED
|
|
10
|
+
* - PLAYING contains: BUFFERING, STREAMING, ERROR
|
|
11
|
+
* - Events can be handled at child or parent level
|
|
12
|
+
*/
|
|
13
|
+
export declare function runHierarchicalStateMachineExample(): void;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hierarchical State Machine POC
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This module provides support for hierarchical (nested) state machines where
|
|
6
|
+
* states can contain child state machines. This enables modeling complex
|
|
7
|
+
* stateful behavior with parent-child relationships.
|
|
8
|
+
*
|
|
9
|
+
* ## Key Features
|
|
10
|
+
*
|
|
11
|
+
* - **Composite States**: States that contain their own internal state machine
|
|
12
|
+
* - **Event Propagation**: Events bubble from child to parent if unhandled
|
|
13
|
+
* - **Default Child States**: Automatic entry into default child state
|
|
14
|
+
* - **History States**: Remember last active child state (optional)
|
|
15
|
+
* - **State Path Tracking**: Full hierarchical path (e.g., "PARENT.CHILD")
|
|
16
|
+
*
|
|
17
|
+
* @category Hierarchical State Machines
|
|
18
|
+
*/
|
|
19
|
+
import { BaseContext, StateMachine, TemplateStateMachine, TemplateState, EventResult, DefaultOutputMapping, EventArgs } from "./interface";
|
|
20
|
+
/**
|
|
21
|
+
* Represents a hierarchical state path using dot notation.
|
|
22
|
+
* Example: "PARENT.CHILD" means we're in CHILD state within PARENT state.
|
|
23
|
+
*/
|
|
24
|
+
export type HierarchicalStatePath<ParentStates extends string, ChildStates extends string> = ParentStates | `${ParentStates}.${ChildStates}`;
|
|
25
|
+
/**
|
|
26
|
+
* Configuration for a composite state's child state machine.
|
|
27
|
+
*
|
|
28
|
+
* @typeParam EventPayloadMapping - Event payload mapping
|
|
29
|
+
* @typeParam Context - Context type
|
|
30
|
+
* @typeParam ChildStates - Child state names
|
|
31
|
+
* @typeParam EventOutputMapping - Event output mapping
|
|
32
|
+
*/
|
|
33
|
+
export interface ChildStateMachineConfig<EventPayloadMapping = any, Context extends BaseContext = BaseContext, ChildStates extends string = string, EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> {
|
|
34
|
+
/** The child state machine instance */
|
|
35
|
+
stateMachine: StateMachine<EventPayloadMapping, Context, ChildStates, EventOutputMapping>;
|
|
36
|
+
/** Default child state to enter when parent state is entered */
|
|
37
|
+
defaultChildState: ChildStates;
|
|
38
|
+
/** Whether to remember the last active child state (history state) */
|
|
39
|
+
rememberHistory?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Composite state that contains a child state machine.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* A composite state is a state that contains its own internal state machine.
|
|
46
|
+
* When the composite state is active, its child state machine is also active.
|
|
47
|
+
* Events are first handled by the child state machine, and if unhandled,
|
|
48
|
+
* they bubble up to the parent state machine.
|
|
49
|
+
*
|
|
50
|
+
* @typeParam EventPayloadMapping - Event payload mapping
|
|
51
|
+
* @typeParam Context - Context type
|
|
52
|
+
* @typeParam ParentStates - Parent state names
|
|
53
|
+
* @typeParam ChildStates - Child state names
|
|
54
|
+
* @typeParam EventOutputMapping - Event output mapping
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* type ParentStates = "IDLE" | "ACTIVE";
|
|
59
|
+
* type ChildStates = "LOADING" | "READY" | "ERROR";
|
|
60
|
+
*
|
|
61
|
+
* class ActiveState extends CompositeState<Events, Context, ParentStates, ChildStates> {
|
|
62
|
+
* eventReactions = {
|
|
63
|
+
* stop: {
|
|
64
|
+
* action: () => console.log("Stopping..."),
|
|
65
|
+
* defaultTargetState: "IDLE"
|
|
66
|
+
* }
|
|
67
|
+
* };
|
|
68
|
+
*
|
|
69
|
+
* getChildStateMachine() {
|
|
70
|
+
* return {
|
|
71
|
+
* stateMachine: new TemplateStateMachine(...),
|
|
72
|
+
* defaultChildState: "LOADING",
|
|
73
|
+
* rememberHistory: true
|
|
74
|
+
* };
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare abstract class CompositeState<EventPayloadMapping = any, Context extends BaseContext = BaseContext, ParentStates extends string = string, ChildStates extends string = string, EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> extends TemplateState<EventPayloadMapping, Context, ParentStates, EventOutputMapping> {
|
|
80
|
+
protected _childStateMachineConfig: ChildStateMachineConfig<EventPayloadMapping, Context, ChildStates, EventOutputMapping> | null;
|
|
81
|
+
protected _historyState: ChildStates | null;
|
|
82
|
+
protected _context: Context | null;
|
|
83
|
+
/**
|
|
84
|
+
* Returns the configuration for the child state machine.
|
|
85
|
+
* Override this method to provide child state machine setup.
|
|
86
|
+
*/
|
|
87
|
+
protected abstract getChildStateMachine(): ChildStateMachineConfig<EventPayloadMapping, Context, ChildStates, EventOutputMapping>;
|
|
88
|
+
/**
|
|
89
|
+
* Gets the current child state, or null if no child state machine is active.
|
|
90
|
+
*/
|
|
91
|
+
getCurrentChildState(): ChildStates | null;
|
|
92
|
+
/**
|
|
93
|
+
* Gets the full hierarchical path of the current state.
|
|
94
|
+
*/
|
|
95
|
+
getStatePath(parentState: ParentStates): HierarchicalStatePath<ParentStates, ChildStates>;
|
|
96
|
+
uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, ParentStates, EventOutputMapping>, from: ParentStates | "INITIAL"): void;
|
|
97
|
+
beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, ParentStates, EventOutputMapping>, to: ParentStates | "TERMINAL"): void;
|
|
98
|
+
handles<K extends keyof EventPayloadMapping | string>(args: EventArgs<EventPayloadMapping, K>, context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, ParentStates, EventOutputMapping>): EventResult<ParentStates, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extended state machine that supports hierarchical state paths.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* This class extends TemplateStateMachine to track and expose hierarchical
|
|
105
|
+
* state paths when composite states are used.
|
|
106
|
+
*
|
|
107
|
+
* @typeParam EventPayloadMapping - Event payload mapping
|
|
108
|
+
* @typeParam Context - Context type
|
|
109
|
+
* @typeParam States - State names
|
|
110
|
+
* @typeParam EventOutputMapping - Event output mapping
|
|
111
|
+
*/
|
|
112
|
+
export declare class HierarchicalStateMachine<EventPayloadMapping = any, Context extends BaseContext = BaseContext, States extends string = string, EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> extends TemplateStateMachine<EventPayloadMapping, Context, States, EventOutputMapping> {
|
|
113
|
+
/**
|
|
114
|
+
* Gets the current hierarchical state path.
|
|
115
|
+
* Returns a simple state name for non-composite states,
|
|
116
|
+
* or a dot-notation path for composite states (e.g., "PARENT.CHILD").
|
|
117
|
+
*/
|
|
118
|
+
getCurrentStatePath(): string;
|
|
119
|
+
/**
|
|
120
|
+
* Gets all active states in the hierarchy.
|
|
121
|
+
* Returns an array where the first element is the top-level state,
|
|
122
|
+
* and subsequent elements are nested child states.
|
|
123
|
+
*/
|
|
124
|
+
getActiveStatePath(): string[];
|
|
125
|
+
/**
|
|
126
|
+
* Checks if the state machine is currently in a specific hierarchical path.
|
|
127
|
+
* Supports both simple state names and dot-notation paths.
|
|
128
|
+
*
|
|
129
|
+
* @param path - State path to check (e.g., "PARENT" or "PARENT.CHILD")
|
|
130
|
+
*/
|
|
131
|
+
isInStatePath(path: string): boolean;
|
|
132
|
+
}
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var w=()=>{};class W{_currentState;_states;_context;_statesArray;_stateChangeCallbacks;_happensCallbacks;_timeouts=void 0;_initialState;constructor(j,z,B,q=!0){if(this._states=j,this._currentState="INITIAL",this._initialState=z,this._context=B,this._statesArray=Object.keys(j),this._stateChangeCallbacks=[],this._happensCallbacks=[],q)this.start()}reset(){this.wrapup(),this.switchTo("INITIAL"),this.start()}start(){if(this.currentState!=="INITIAL")return;this._context.setup(),this.switchTo(this._initialState),this._states[this._initialState].uponEnter(this._context,this,this._initialState)}wrapup(){if(this._currentState==="TERMINAL")return;let j=this._currentState;if(j!=="INITIAL")this._states[j].beforeExit(this._context,this,"TERMINAL");this._context.cleanup(),this.switchTo("TERMINAL")}switchTo(j){this._currentState=j}happens(...j){if(this._timeouts)clearTimeout(this._timeouts);if(this._currentState==="INITIAL"||this._currentState==="TERMINAL")return{handled:!1};this._happensCallbacks.forEach((B)=>B(j,this._context));let z=this._states[this._currentState].handles(j,this._context,this);if(z.handled&&z.nextState!==void 0&&z.nextState!==this._currentState){let B=this._currentState;this._states[this._currentState].beforeExit(this._context,this,z.nextState),this.switchTo(z.nextState),this._states[this._currentState].uponEnter(this._context,this,B);for(let q of this._stateChangeCallbacks)q(B,this._currentState)}return z}onStateChange(j){this._stateChangeCallbacks.push(j)}onHappens(j){this._happensCallbacks.push(j)}get currentState(){return this._currentState}setContext(j){this._context=j}get possibleStates(){return this._statesArray}get states(){return this._states}}class H{_eventReactions={};_guards={};_eventGuards={};_delay=void 0;get handlingEvents(){return Object.keys(this._eventReactions)}get guards(){return this._guards}get eventGuards(){return this._eventGuards}get delay(){return this._delay}uponEnter(j,z,B){}beforeExit(j,z,B){}handles(j,z,B){let q=j[0],J=j[1];if(this._eventReactions[q]){let V=this._eventReactions[q].action(z,J,B),Z=this._eventReactions[q].defaultTargetState,U=this._eventGuards[q],$={handled:!0,nextState:Z},Q=V!==void 0?{...$,output:V}:$;if(U){let X=U.find((Y)=>{if(this._guards[Y.guard])return this._guards[Y.guard](z);return!1});return X?{...Q,nextState:X.target}:Q}return Q}return{handled:!1}}}function P(j){return(z)=>j.includes(z)}function C(j){return j}function I(j,z){if(!j.states.includes(j.initialState))throw Error(`Initial state "${j.initialState}" must be in the states array`);for(let q of j.stateDefinitions){if(!j.states.includes(q.name))throw Error(`State definition "${q.name}" is not in the states array`);for(let J of q.transitions){let V=j.events;if(!(String(J.event)in V))throw Error(`Event "${String(J.event)}" in state "${q.name}" is not defined in events`);if(!j.states.includes(J.targetState))throw Error(`Target state "${J.targetState}" in transition from "${q.name}" is not in the states array`);if(J.guards)for(let Z of J.guards){if(!j.states.includes(Z.targetState))throw Error(`Guard target state "${Z.targetState}" is not in the states array`);if(typeof Z.guard==="string"){if(!q.guards||!(Z.guard in q.guards))throw Error(`Guard "${Z.guard}" referenced in state "${q.name}" for event "${String(J.event)}" is not defined in the state's guards section`)}}}}let B={};for(let q of j.stateDefinitions){let J={},V={},Z={};if(q.guards)for(let[Q,X]of Object.entries(q.guards))V[Q]=X;for(let Q of q.transitions){let X=Q.event;if(J[X]={action:Q.action||w,defaultTargetState:Q.targetState},Q.guards&&Q.guards.length>0){let F=[];Q.guards.forEach((Y,G)=>{let L;if(typeof Y.guard==="string"){if(L=Y.guard,!V[L])throw Error(`Guard "${L}" referenced in state "${q.name}" for event "${String(X)}" is not defined in the state's guards section`)}else L=`guard_${q.name}_${String(X)}_${G}`,V[L]=Y.guard;F.push({guard:L,target:Y.targetState})}),Z[X]=F}}class U extends H{eventReactions=J;_guards=V;_eventGuards=Z}let $=new U;if(q.onEnter){let Q=$.uponEnter.bind($);$.uponEnter=(X,F,Y)=>{Q(X,F,Y),q.onEnter(X,Y)}}if(q.onExit){let Q=$.beforeExit.bind($);$.beforeExit=(X,F,Y)=>{Q(X,F,Y),q.onExit(X,Y)}}B[q.name]=$}for(let q of j.states)if(!B[q]){class J extends H{eventReactions={}}B[q]=new J}return new W(B,j.initialState,z,!0)}class _ extends H{_childStateMachineConfig=null;_historyState=null;_context=null;getCurrentChildState(){if(!this._childStateMachineConfig)return null;let z=this._childStateMachineConfig.stateMachine.currentState;return z==="INITIAL"||z==="TERMINAL"?null:z}getStatePath(j){let z=this.getCurrentChildState();if(z===null)return j;return`${j}.${z}`}uponEnter(j,z,B){super.uponEnter(j,z,B),this._context=j;let q=this.getChildStateMachine();this._childStateMachineConfig=q;let J=q.rememberHistory&&this._historyState?this._historyState:q.defaultChildState,V=q.stateMachine;if(V.currentState==="INITIAL")V.start();V.switchTo(J),V.states[J].uponEnter(j,V,"INITIAL")}beforeExit(j,z,B){if(this._childStateMachineConfig?.rememberHistory){let q=this.getCurrentChildState();if(q!==null)this._historyState=q}if(this._childStateMachineConfig){let q=this.getCurrentChildState();if(q!==null)this._childStateMachineConfig.stateMachine.states[q].beforeExit(j,this._childStateMachineConfig.stateMachine,"TERMINAL");this._childStateMachineConfig.stateMachine.wrapup()}super.beforeExit(j,z,B)}handles(j,z,B){if(this._childStateMachineConfig){if(this._childStateMachineConfig.stateMachine.happens(...j).handled)return{handled:!0}}return super.handles(j,z,B)}}class O extends W{getCurrentStatePath(){let j=this.currentState;if(j==="INITIAL"||j==="TERMINAL")return j;let z=this.states[j];if(z instanceof _)return z.getStatePath(j);return j}getActiveStatePath(){let j=this.currentState;if(j==="INITIAL"||j==="TERMINAL")return[j];let z=[j],B=this.states[j];if(B instanceof _){let q=B.getCurrentChildState();if(q!==null)z.push(q)}return z}isInStatePath(j){let z=this.getCurrentStatePath();return z===j||z.startsWith(j+".")}}export{C as createStateMachineSchemaWithInferredStates,I as createStateMachineFromSchema,P as createStateGuard,W as TemplateStateMachine,H as TemplateState,w as NO_OP,O as HierarchicalStateMachine,_ as CompositeState};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=A4A1425C023A074164756E2164756E21
|
package/index.js.map
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/interface.ts"],
|
|
3
|
+
"sources": ["../src/interface.ts", "../src/schema-factory.ts", "../src/hierarchical.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Base context interface for state machines.\n *\n * @remarks\n * The context is shared across all states in a state machine and can be used to store data\n * that persists between state transitions. All custom contexts must extend this interface.\n *\n * The setup and cleanup methods provide lifecycle hooks for resource management:\n * - `setup()`: Called when the context is initialized\n * - `cleanup()`: Called when the context is destroyed\n *\n * @example\n * ```typescript\n * interface MyContext extends BaseContext {\n * counter: number;\n * data: string[];\n * setup() {\n * this.counter = 0;\n * this.data = [];\n * }\n * cleanup() {\n * this.data = [];\n * }\n * }\n * ```\n *\n * @category Core\n */\nexport interface BaseContext {\n setup(): void;\n cleanup(): void;\n}\n\ntype NOOP = () => void;\n\n/**\n * Utility type to check if an object type is empty.\n * @internal\n */\ntype IsEmptyObject<T> = T extends {} ? {} extends T ? true : false : false;\n\n/**\n * Utility type to derive a string literal union from a readonly array of string literals.\n *\n * @remarks\n * This helper type extracts the element types from a readonly array to create a union type.\n * Useful for defining state machine states from an array.\n *\n * @example\n * ```typescript\n * const TEST_STATES = [\"one\", \"two\", \"three\"] as const;\n * type TestStates = CreateStateType<typeof TEST_STATES>; // \"one\" | \"two\" | \"three\"\n * ```\n *\n * @category Utilities\n */\nexport type CreateStateType<ArrayLiteral extends readonly string[]> = ArrayLiteral[number];\n\n/**\n * Type for event arguments with conditional payload requirement.\n *\n * @remarks\n * This utility type determines whether an event requires a payload argument based on the\n * event payload mapping. If the payload is an empty object, no payload is required.\n *\n * @typeParam EventPayloadMapping - Mapping of event names to their payload types\n * @typeParam K - The event key\n *\n * @category Utilities\n */\nexport type EventArgs<EventPayloadMapping, K> =\n K extends keyof EventPayloadMapping\n ? IsEmptyObject<EventPayloadMapping[K]> extends true\n ? [event: K] // No payload needed\n : [event: K, payload: EventPayloadMapping[K]] // Payload required\n : [event: K, payload?: unknown]; // Unknown events\n\n/**\n * No-operation function constant used as a placeholder for optional actions.\n *\n * @remarks\n * Use this when you need to provide a function but don't want it to do anything,\n * such as for default state transition actions that have no side effects.\n *\n * @category Core\n */\nexport const NO_OP: NOOP = ()=>{};\n\n/**\n * Result type indicating an event was not handled by the current state.\n *\n * @remarks\n * When a state doesn't have a handler defined for a particular event, it returns this type.\n * The state machine will not transition and the event is effectively ignored.\n *\n * @category Core\n */\nexport type EventNotHandled = {\n handled: false;\n}\n\n/**\n * Result type when an event is successfully handled by a state.\n *\n * @remarks\n * This type represents a successful event handling result. It can optionally include:\n * - `nextState`: The state to transition to (if different from current)\n * - `output`: A return value from the event handler\n *\n * @typeParam States - Union of all possible state names in the state machine\n * @typeParam Output - The output type for this event (defaults to void)\n *\n * @example\n * ```typescript\n * // Simple transition without output\n * const result: EventHandled<\"IDLE\" | \"ACTIVE\"> = {\n * handled: true,\n * nextState: \"ACTIVE\"\n * };\n *\n * // With output value\n * const resultWithOutput: EventHandled<\"IDLE\" | \"ACTIVE\", number> = {\n * handled: true,\n * nextState: \"IDLE\",\n * output: 42\n * };\n * ```\n *\n * @category Core\n */\nexport type EventHandled<States extends string, Output = void> = {\n handled: true;\n nextState?: States;\n output?: Output;\n}\n\n/**\n * Discriminated union representing the result of event handling.\n *\n * @remarks\n * Every event handler returns an EventResult, which is either:\n * - {@link EventHandled}: The event was processed successfully\n * - {@link EventNotHandled}: The event was not recognized/handled\n *\n * Use the `handled` discriminant to narrow the type in TypeScript.\n *\n * @typeParam States - Union of all possible state names\n * @typeParam Output - The output type for handled events\n *\n * @category Core\n */\nexport type EventResult<States extends string, Output = void> = EventNotHandled | EventHandled<States, Output>;\n\n/**\n * @description A default output mapping that maps all events to void.\n * Used as default when no output mapping is provided.\n * \n * @category Types\n */\nexport type DefaultOutputMapping<EventPayloadMapping> = {\n [K in keyof EventPayloadMapping]: void;\n};\n\n/**\n * @description This is the interface for the state machine. The interface takes in a few generic parameters.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - EventOutputMapping: A mapping of events to their output types. Defaults to void for all events.\n * \n * You can probably get by using the TemplateStateMachine class.\n * The naming is that an event would \"happen\" and the state of the state machine would \"handle\" it.\n *\n * @see {@link TemplateStateMachine}\n * @see {@link KmtInputStateMachine}\n * \n * @category Types\n */\nexport interface StateMachine<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string = 'IDLE',\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> {\n switchTo(state: States): void;\n // Overload for known events - provides IntelliSense with typed output\n happens<K extends keyof EventPayloadMapping>(\n ...args: EventArgs<EventPayloadMapping, K>\n ): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;\n // Overload for unknown events - maintains backward compatibility\n happens<K extends string>(\n ...args: EventArgs<EventPayloadMapping, K>\n ): EventResult<States, unknown>;\n setContext(context: Context): void;\n states: Record<States, State<EventPayloadMapping, Context, string extends States ? string : States, EventOutputMapping>>;\n onStateChange(callback: StateChangeCallback<States>): void;\n possibleStates: States[];\n onHappens(callback: (args: EventArgs<EventPayloadMapping, keyof EventPayloadMapping | string>, context: Context) => void): void;\n}\n\n/**\n * @description This is the type for the callback that is called when the state changes.\n *\n * @category Types\n */\nexport type StateChangeCallback<States extends string = 'IDLE'> = (currentState: States, nextState: States) => void;\n\n/**\n * @description This is the interface for the state. The interface takes in a few generic parameters:\n * You can probably get by extending the TemplateState class. \n *\n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - EventOutputMapping: A mapping of events to their output types. Defaults to void for all events.\n * \n * A state's all possible states can be only a subset of the possible states of the state machine. (a state only needs to know what states it can transition to)\n * This allows for a state to be reusable across different state machines.\n *\n * @see {@link TemplateState}\n * \n * @category Types\n */\nexport interface State<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string = 'IDLE',\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> { \n uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, from: States): void;\n beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, to: States): void;\n handles<K extends (keyof EventPayloadMapping | string)>(args: EventArgs<EventPayloadMapping, K>, context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;\n eventReactions: EventReactions<EventPayloadMapping, Context, States, EventOutputMapping>;\n guards: Guard<Context>;\n eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;\n delay: Delay<Context, EventPayloadMapping, States, EventOutputMapping> | undefined;\n}\n\n/**\n * @description This is the type for the event reactions of a state.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - EventOutputMapping: A mapping of events to their output types. Defaults to void for all events.\n * \n * The action function can now return an output value that will be included in the EventHandledResult.\n * \n * @category Types\n */\nexport type EventReactions<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> = {\n [K in keyof Partial<EventPayloadMapping>]: { \n action: (\n context: Context, \n event: EventPayloadMapping[K], \n stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>\n ) => K extends keyof EventOutputMapping ? (EventOutputMapping[K] | void) : void;\n defaultTargetState?: States;\n };\n};\n\n/**\n * @description This is the type for the guard evaluation when a state transition is happening.\n * \n * Guard evaluations are evaluated after the state has handled the event with the action.\n * Guard evaluations can be defined in an array and the first guard that evaluates to true will be used to determine the next state.\n * \n * Generic parameters:\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * \n * @category Types\n */\nexport type GuardEvaluation<Context extends BaseContext> = (context: Context) => boolean;\n\n/**\n * @description This is the type for the guard of a state.\n * \n * guard is an object that maps a key to a guard evaluation.\n * K is all the possible keys that can be used to evaluate the guard.\n * K is optional but if it is not provided, typescript won't be able to type guard in the EventGuards type.\n * \n * @category Types\n */\nexport type Guard<Context extends BaseContext, K extends string = string> = {\n [P in K]: GuardEvaluation<Context>;\n}\n\nexport type Action<\n Context extends BaseContext, \n EventPayloadMapping, \n States extends string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>,\n Output = void\n> = {\n action: (context: Context, event: EventPayloadMapping[keyof EventPayloadMapping], stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>) => Output | void;\n defaultTargetState?: States;\n}\n\nexport type Delay<\n Context extends BaseContext, \n EventPayloadMapping, \n States extends string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> = {\n time: number;\n action: Action<Context, EventPayloadMapping, States, EventOutputMapping>;\n}\n\n/**\n * @description This is a mapping of a guard to a target state.\n * \n * Generic parameters:\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - G: The guard type.\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * You probably don't need to use this type directly.\n * \n * @see {@link TemplateState['eventGuards']}\n * \n * @category Types\n */\nexport type GuardMapping<Context extends BaseContext, G, States extends string> = {\n guard: G extends Guard<Context, infer K> ? K : never;\n target: States;\n}\n\n/**\n * @description This is a mapping of an event to a guard evaluation.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - T: The guard type.\n * \n * You probably don't need to use this type directly.\n * This is a mapping of an event to a guard evaluation.\n * \n * @see {@link TemplateState['eventGuards']}\n * \n * @category Types\n */\nexport type EventGuards<EventPayloadMapping, States extends string, Context extends BaseContext, T extends Guard<Context>> = {\n [K in keyof EventPayloadMapping]: GuardMapping<Context, T, States>[];\n}\n\n/**\n * Concrete implementation of a finite state machine.\n *\n * @remarks\n * This class provides a complete, ready-to-use state machine implementation. It's generic enough\n * to handle most use cases without requiring custom extensions.\n *\n * ## Features\n *\n * - **Type-safe events**: Events and their payloads are fully typed via the EventPayloadMapping\n * - **State transitions**: Automatic state transitions based on event handlers\n * - **Event outputs**: Handlers can return values that are included in the result\n * - **Lifecycle hooks**: States can define `uponEnter` and `beforeExit` callbacks\n * - **State change listeners**: Subscribe to state transitions\n * - **Shared context**: All states access the same context object for persistent data\n *\n * ## Usage Pattern\n *\n * 1. Define your event payload mapping type\n * 2. Define your states as a string union type\n * 3. Create state classes extending {@link TemplateState}\n * 4. Instantiate TemplateStateMachine with your states and initial state\n *\n * @typeParam EventPayloadMapping - Object mapping event names to their payload types\n * @typeParam Context - Context type shared across all states\n * @typeParam States - Union of all possible state names (string literals)\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n *\n * @example\n * Basic vending machine state machine\n * ```typescript\n * type Events = {\n * insertCoin: { amount: number };\n * selectItem: { itemId: string };\n * cancel: {};\n * };\n *\n * type States = \"IDLE\" | \"PAYMENT\" | \"DISPENSING\";\n *\n * interface VendingContext extends BaseContext {\n * balance: number;\n * setup() { this.balance = 0; }\n * cleanup() {}\n * }\n *\n * const context: VendingContext = {\n * balance: 0,\n * setup() { this.balance = 0; },\n * cleanup() {}\n * };\n *\n * const machine = new TemplateStateMachine<Events, VendingContext, States>(\n * {\n * IDLE: new IdleState(),\n * PAYMENT: new PaymentState(),\n * DISPENSING: new DispensingState()\n * },\n * \"IDLE\",\n * context\n * );\n *\n * // Trigger events\n * machine.happens(\"insertCoin\", { amount: 100 });\n * machine.happens(\"selectItem\", { itemId: \"A1\" });\n * ```\n *\n * @category State Machine Core\n * @see {@link TemplateState} for creating state implementations\n * @see {@link StateMachine} for the interface definition\n */\nexport class TemplateStateMachine<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string = 'IDLE',\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> implements StateMachine<EventPayloadMapping, Context, States, EventOutputMapping> {\n\n protected _currentState: States;\n protected _states: Record<States, State<EventPayloadMapping, Context, States, EventOutputMapping>>;\n protected _context: Context;\n protected _statesArray: States[];\n protected _stateChangeCallbacks: StateChangeCallback<States>[];\n protected _happensCallbacks: ((args: EventArgs<EventPayloadMapping, keyof EventPayloadMapping | string>, context: Context) => void)[];\n protected _timeouts: ReturnType<typeof setTimeout> | undefined = undefined;\n\n constructor(states: Record<States, State<EventPayloadMapping, Context, States, EventOutputMapping>>, initialState: States, context: Context){\n this._states = states;\n this._currentState = initialState;\n this._context = context;\n this._statesArray = Object.keys(states) as States[];\n this._stateChangeCallbacks = [];\n this._happensCallbacks = [];\n this._states[initialState].uponEnter(context, this, initialState);\n }\n\n switchTo(state: States): void {\n this._currentState = state;\n }\n \n // Implementation signature - matches both overloads\n happens<K extends keyof EventPayloadMapping>(...args: EventArgs<EventPayloadMapping, K>): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;\n happens<K extends string>(...args: EventArgs<EventPayloadMapping, K>): EventResult<States, unknown>;\n happens<K extends keyof EventPayloadMapping | string>(...args: EventArgs<EventPayloadMapping, K>): EventResult<States, unknown> {\n if(this._timeouts){\n clearTimeout(this._timeouts);\n }\n this._happensCallbacks.forEach(callback => callback(args, this._context));\n const result = this._states[this._currentState].handles(args, this._context, this);\n if(result.handled && result.nextState !== undefined && result.nextState !== this._currentState){ // TODO: whether or not to transition to the same state (currently no) (uponEnter and beforeExit will still be called if the state is the same)\n const originalState = this._currentState;\n this._states[this._currentState].beforeExit(this._context, this, result.nextState);\n this.switchTo(result.nextState);\n this._states[this._currentState].uponEnter(this._context, this, originalState);\n this._stateChangeCallbacks.forEach(callback => callback(originalState, this._currentState));\n }\n return result;\n }\n\n onStateChange(callback: StateChangeCallback<States>): void {\n this._stateChangeCallbacks.push(callback);\n }\n\n onHappens(callback: (args: EventArgs<EventPayloadMapping, keyof EventPayloadMapping | string>, context: Context) => void): void {\n this._happensCallbacks.push(callback);\n }\n\n get currentState(): States {\n return this._currentState;\n }\n\n setContext(context: Context): void {\n this._context = context;\n }\n\n get possibleStates(): States[] {\n return this._statesArray;\n }\n\n get states(): Record<States, State<EventPayloadMapping, Context, States, EventOutputMapping>> {\n return this._states;\n }\n}\n/**\n * Abstract base class for state machine states.\n *\n * @remarks\n * This abstract class provides the foundation for implementing individual states in a state machine.\n * Each state defines how it responds to events through the `eventReactions` object.\n *\n * ## Key Concepts\n *\n * - **Event Reactions**: Define handlers for events this state cares about. Unhandled events are ignored.\n * - **Guards**: Conditional logic that determines which state to transition to based on context\n * - **Lifecycle Hooks**: `uponEnter` and `beforeExit` callbacks for state transition side effects\n * - **Selective Handling**: Only define reactions for events relevant to this state\n *\n * ## Implementation Pattern\n *\n * 1. Extend this class for each state in your state machine\n * 2. Implement the `eventReactions` property with handlers for relevant events\n * 3. Optionally override `uponEnter` and `beforeExit` for lifecycle logic\n * 4. Optionally define `guards` and `eventGuards` for conditional transitions\n *\n * @typeParam EventPayloadMapping - Object mapping event names to their payload types\n * @typeParam Context - Context type shared across all states\n * @typeParam States - Union of all possible state names (string literals)\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n *\n * @example\n * Simple state implementation\n * ```typescript\n * class IdleState extends TemplateState<MyEvents, MyContext, MyStates> {\n * eventReactions = {\n * start: {\n * action: (context, event) => {\n * console.log('Starting...');\n * context.startTime = Date.now();\n * },\n * defaultTargetState: \"ACTIVE\"\n * },\n * reset: {\n * action: (context, event) => {\n * context.counter = 0;\n * }\n * // No state transition - stays in IDLE\n * }\n * };\n *\n * uponEnter(context, stateMachine, fromState) {\n * console.log(`Entered IDLE from ${fromState}`);\n * }\n * }\n * ```\n *\n * @example\n * State with guards for conditional transitions\n * ```typescript\n * class PaymentState extends TemplateState<Events, VendingContext, States> {\n * guards = {\n * hasEnoughMoney: (context) => context.balance >= context.itemPrice,\n * needsChange: (context) => context.balance > context.itemPrice\n * };\n *\n * eventReactions = {\n * selectItem: {\n * action: (context, event) => {\n * context.selectedItem = event.itemId;\n * context.itemPrice = getPrice(event.itemId);\n * },\n * defaultTargetState: \"IDLE\" // Fallback if no guard matches\n * }\n * };\n *\n * eventGuards = {\n * selectItem: [\n * { guard: 'hasEnoughMoney', target: 'DISPENSING' },\n * // If hasEnoughMoney is false, uses defaultTargetState (IDLE)\n * ]\n * };\n * }\n * ```\n *\n * @category State Machine Core\n * @see {@link TemplateStateMachine} for the state machine implementation\n * @see {@link EventReactions} for defining event handlers\n */\nexport abstract class TemplateState<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string = 'IDLE',\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> implements State<EventPayloadMapping, Context, States, EventOutputMapping> {\n\n public abstract eventReactions: EventReactions<EventPayloadMapping, Context, States, EventOutputMapping>;\n protected _guards: Guard<Context> = {} as Guard<Context>;\n protected _eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>> = {} as Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;\n protected _delay: Delay<Context, EventPayloadMapping, States, EventOutputMapping> | undefined = undefined;\n\n get guards(): Guard<Context> {\n return this._guards;\n }\n\n get eventGuards(): Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>> {\n return this._eventGuards;\n }\n\n get delay(): Delay<Context, EventPayloadMapping, States, EventOutputMapping> | undefined {\n return this._delay;\n }\n\n uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, from: States): void {\n // console.log(\"enter\");\n }\n\n beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, to: States): void {\n // console.log('leave');\n }\n\n handles<K extends (keyof EventPayloadMapping | string)>(args: EventArgs<EventPayloadMapping, K>, context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>{\n const eventKey = args[0] as keyof EventPayloadMapping;\n const eventPayload = args[1] as EventPayloadMapping[keyof EventPayloadMapping];\n if (this.eventReactions[eventKey]) {\n // Capture the output from the action\n const output = this.eventReactions[eventKey].action(context, eventPayload, stateMachine);\n const targetState = this.eventReactions[eventKey].defaultTargetState;\n const guardsToEvaluate = this._eventGuards[eventKey];\n if(guardsToEvaluate){\n const target = guardsToEvaluate.find((guard)=>{\n if(this.guards[guard.guard]){\n return this.guards[guard.guard](context);\n }\n return false;\n });\n return target \n ? {handled: true, nextState: target.target, output: output as any} \n : {handled: true, nextState: targetState, output: output as any};\n }\n return {handled: true, nextState: targetState, output: output as any};\n }\n return {handled: false};\n }\n}\n\n\n/**\n * Creates a type guard function for checking if a value belongs to a specific set of states.\n *\n * @remarks\n * This utility function generates a TypeScript type guard that narrows a string type\n * to a specific union of string literals. Useful when you have multiple state types\n * and need to distinguish between them at runtime.\n *\n * @typeParam T - String literal type to guard for\n * @param set - Readonly array of string literals defining the valid states\n * @returns A type guard function that checks if a string is in the set\n *\n * @example\n * Creating state guards for hierarchical state machines\n * ```typescript\n * type MainStates = \"idle\" | \"active\" | \"paused\";\n * type SubStates = \"loading\" | \"processing\" | \"complete\";\n * type AllStates = MainStates | SubStates;\n *\n * const MAIN_STATES = [\"idle\", \"active\", \"paused\"] as const;\n * const isMainState = createStateGuard(MAIN_STATES);\n *\n * function handleState(state: AllStates) {\n * if (isMainState(state)) {\n * // TypeScript knows state is MainStates here\n * console.log('Main state:', state);\n * } else {\n * // TypeScript knows state is SubStates here\n * console.log('Sub state:', state);\n * }\n * }\n * ```\n *\n * @category Utilities\n */\nexport function createStateGuard<T extends string>(set: readonly T[]) {\n return (s: string): s is T => set.includes(s as T);\n}\n"
|
|
5
|
+
"/**\n * Base context interface for state machines.\n *\n * @remarks\n * The context is shared across all states in a state machine and can be used to store data\n * that persists between state transitions. All custom contexts must extend this interface.\n *\n * The setup and cleanup methods provide lifecycle hooks for resource management:\n * - `setup()`: Called when the context is initialized\n * - `cleanup()`: Called when the context is destroyed\n *\n * @example\n * ```typescript\n * interface MyContext extends BaseContext {\n * counter: number;\n * data: string[];\n * setup() {\n * this.counter = 0;\n * this.data = [];\n * }\n * cleanup() {\n * this.data = [];\n * }\n * }\n * ```\n *\n * @category Core\n */\nexport interface BaseContext {\n setup(): void;\n cleanup(): void;\n}\n\ntype NOOP = () => void;\n\n/**\n * Utility type to check if an object type is empty.\n * @internal\n */\ntype IsEmptyObject<T> = T extends {} ? {} extends T ? true : false : false;\n\n/**\n * Utility type to derive a string literal union from a readonly array of string literals.\n *\n * @remarks\n * This helper type extracts the element types from a readonly array to create a union type.\n * Useful for defining state machine states from an array.\n *\n * @example\n * ```typescript\n * const TEST_STATES = [\"one\", \"two\", \"three\"] as const;\n * type TestStates = CreateStateType<typeof TEST_STATES>; // \"one\" | \"two\" | \"three\"\n * ```\n *\n * @category Utilities\n */\nexport type CreateStateType<ArrayLiteral extends readonly string[]> = ArrayLiteral[number];\n\n/**\n * Type for event arguments with conditional payload requirement.\n *\n * @remarks\n * This utility type determines whether an event requires a payload argument based on the\n * event payload mapping. If the payload is an empty object, no payload is required.\n *\n * @typeParam EventPayloadMapping - Mapping of event names to their payload types\n * @typeParam K - The event key\n *\n * @category Utilities\n */\nexport type EventArgs<EventPayloadMapping, K> =\n K extends keyof EventPayloadMapping\n ? IsEmptyObject<EventPayloadMapping[K]> extends true\n ? [event: K] // No payload needed\n : [event: K, payload: EventPayloadMapping[K]] // Payload required\n : [event: K, payload?: unknown]; // Unknown events\n\n/**\n * No-operation function constant used as a placeholder for optional actions.\n *\n * @remarks\n * Use this when you need to provide a function but don't want it to do anything,\n * such as for default state transition actions that have no side effects.\n *\n * @category Core\n */\nexport const NO_OP: NOOP = ()=>{};\n\n/**\n * Result type indicating an event was not handled by the current state.\n *\n * @remarks\n * When a state doesn't have a handler defined for a particular event, it returns this type.\n * The state machine will not transition and the event is effectively ignored.\n *\n * @category Core\n */\nexport type EventNotHandled = {\n handled: false;\n}\n\n/**\n * Helper type that conditionally includes the output property.\n * @internal\n */\ntype WithOutput<Output> = Output extends void ? {} : { output?: Output };\n\n/**\n * Result type when an event is successfully handled by a state.\n *\n * @remarks\n * This type represents a successful event handling result. It can optionally include:\n * - `nextState`: The state to transition to (if different from current)\n * - `output`: A return value from the event handler (only present when Output is not void)\n *\n * @typeParam States - Union of all possible state names in the state machine\n * @typeParam Output - The output type for this event (defaults to void)\n *\n * @example\n * ```typescript\n * // Simple transition without output\n * const result: EventHandled<\"IDLE\" | \"ACTIVE\"> = {\n * handled: true,\n * nextState: \"ACTIVE\"\n * // output property does not exist when Output is void\n * };\n *\n * // With output value\n * const resultWithOutput: EventHandled<\"IDLE\" | \"ACTIVE\", number> = {\n * handled: true,\n * nextState: \"IDLE\",\n * output: 42\n * };\n * ```\n *\n * @category Core\n */\nexport type EventHandled<States extends string, Output = void> = {\n handled: true;\n nextState?: States;\n} & WithOutput<Output>;\n\n/**\n * Discriminated union representing the result of event handling.\n *\n * @remarks\n * Every event handler returns an EventResult, which is either:\n * - {@link EventHandled}: The event was processed successfully\n * - {@link EventNotHandled}: The event was not recognized/handled\n *\n * Use the `handled` discriminant to narrow the type in TypeScript.\n *\n * @typeParam States - Union of all possible state names\n * @typeParam Output - The output type for handled events\n *\n * @category Core\n */\nexport type EventResult<States extends string, Output = void> = EventNotHandled | EventHandled<States, Output>;\n\n/**\n * @description A default output mapping that maps all events to void.\n * Used as default when no output mapping is provided.\n * \n * @category Types\n */\nexport type DefaultOutputMapping<EventPayloadMapping> = {\n [K in keyof EventPayloadMapping]: void;\n};\n\n/**\n * @description This is the interface for the state machine. The interface takes in a few generic parameters.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - EventOutputMapping: A mapping of events to their output types. Defaults to void for all events.\n * \n * You can probably get by using the TemplateStateMachine class.\n * The naming is that an event would \"happen\" and the state of the state machine would \"handle\" it.\n *\n * @see {@link TemplateStateMachine}\n * @see {@link KmtInputStateMachine}\n * \n * @category Types\n */\nexport interface StateMachine<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string = 'IDLE',\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> {\n switchTo(state: States): void;\n // Overload for known events - provides IntelliSense with typed output\n happens<K extends keyof EventPayloadMapping>(\n ...args: EventArgs<EventPayloadMapping, K>\n ): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;\n // Overload for unknown events - maintains backward compatibility\n happens<K extends string>(\n ...args: EventArgs<EventPayloadMapping, K>\n ): EventResult<States, unknown>;\n setContext(context: Context): void;\n states: Record<States, State<EventPayloadMapping, Context, string extends States ? string : States, EventOutputMapping>>;\n onStateChange(callback: StateChangeCallback<States>): void;\n possibleStates: States[];\n onHappens(callback: (args: EventArgs<EventPayloadMapping, keyof EventPayloadMapping | string>, context: Context) => void): void;\n reset(): void;\n start(): void;\n wrapup(): void;\n}\n\n/**\n * @description This is the type for the callback that is called when the state changes.\n *\n * @category Types\n */\nexport type StateChangeCallback<States extends string = 'IDLE'> = (currentState: States, nextState: States) => void;\n\n/**\n * @description This is the interface for the state. The interface takes in a few generic parameters:\n * You can probably get by extending the TemplateState class. \n *\n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - EventOutputMapping: A mapping of events to their output types. Defaults to void for all events.\n * \n * A state's all possible states can be only a subset of the possible states of the state machine. (a state only needs to know what states it can transition to)\n * This allows for a state to be reusable across different state machines.\n *\n * @see {@link TemplateState}\n * \n * @category Types\n */\nexport interface State<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string = 'IDLE',\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> { \n uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, from: States | \"INITIAL\"): void;\n beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, to: States | \"TERMINAL\"): void;\n handles<K extends (keyof EventPayloadMapping | string)>(args: EventArgs<EventPayloadMapping, K>, context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;\n // eventReactions: EventReactions<EventPayloadMapping, Context, States, EventOutputMapping>;\n guards: Guard<Context>;\n eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;\n delay: Delay<Context, EventPayloadMapping, States, EventOutputMapping> | undefined;\n}\n\n/**\n * @description This is the type for the event reactions of a state.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - EventOutputMapping: A mapping of events to their output types. Defaults to void for all events.\n * \n * The action function can now return an output value that will be included in the EventHandledResult.\n * \n * @category Types\n */\nexport type EventReactions<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> = {\n [K in keyof Partial<EventPayloadMapping>]: { \n action: (\n context: Context, \n event: EventPayloadMapping[K], \n stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>\n ) => K extends keyof EventOutputMapping ? (EventOutputMapping[K] | void) : void;\n defaultTargetState?: States;\n };\n};\n\n/**\n * @description This is the type for the guard evaluation when a state transition is happening.\n * \n * Guard evaluations are evaluated after the state has handled the event with the action.\n * Guard evaluations can be defined in an array and the first guard that evaluates to true will be used to determine the next state.\n * \n * Generic parameters:\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * \n * @category Types\n */\nexport type GuardEvaluation<Context extends BaseContext> = (context: Context) => boolean;\n\n/**\n * @description This is the type for the guard of a state.\n * \n * guard is an object that maps a key to a guard evaluation.\n * K is all the possible keys that can be used to evaluate the guard.\n * K is optional but if it is not provided, typescript won't be able to type guard in the EventGuards type.\n * \n * @category Types\n */\nexport type Guard<Context extends BaseContext, K extends string = string> = {\n [P in K]: GuardEvaluation<Context>;\n}\n\nexport type Action<\n Context extends BaseContext, \n EventPayloadMapping, \n States extends string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>,\n Output = void\n> = {\n action: (context: Context, event: EventPayloadMapping[keyof EventPayloadMapping], stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>) => Output | void;\n defaultTargetState?: States;\n}\n\nexport type Delay<\n Context extends BaseContext, \n EventPayloadMapping, \n States extends string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> = {\n time: number;\n action: Action<Context, EventPayloadMapping, States, EventOutputMapping>;\n}\n\n/**\n * @description This is a mapping of a guard to a target state.\n * \n * Generic parameters:\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - G: The guard type.\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * You probably don't need to use this type directly.\n * \n * @see {@link TemplateState['eventGuards']}\n * \n * @category Types\n */\nexport type GuardMapping<Context extends BaseContext, G, States extends string> = {\n guard: G extends Guard<Context, infer K> ? K : never;\n target: States;\n}\n\n/**\n * @description This is a mapping of an event to a guard evaluation.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - T: The guard type.\n * \n * You probably don't need to use this type directly.\n * This is a mapping of an event to a guard evaluation.\n * \n * @see {@link TemplateState['eventGuards']}\n * \n * @category Types\n */\nexport type EventGuards<EventPayloadMapping, States extends string, Context extends BaseContext, T extends Guard<Context>> = {\n [K in keyof EventPayloadMapping]: GuardMapping<Context, T, States>[];\n}\n\n/**\n * Concrete implementation of a finite state machine.\n *\n * @remarks\n * This class provides a complete, ready-to-use state machine implementation. It's generic enough\n * to handle most use cases without requiring custom extensions.\n *\n * ## Features\n *\n * - **Type-safe events**: Events and their payloads are fully typed via the EventPayloadMapping\n * - **State transitions**: Automatic state transitions based on event handlers\n * - **Event outputs**: Handlers can return values that are included in the result\n * - **Lifecycle hooks**: States can define `uponEnter` and `beforeExit` callbacks\n * - **State change listeners**: Subscribe to state transitions\n * - **Shared context**: All states access the same context object for persistent data\n *\n * ## Usage Pattern\n *\n * 1. Define your event payload mapping type\n * 2. Define your states as a string union type\n * 3. Create state classes extending {@link TemplateState}\n * 4. Instantiate TemplateStateMachine with your states and initial state\n *\n * @typeParam EventPayloadMapping - Object mapping event names to their payload types\n * @typeParam Context - Context type shared across all states\n * @typeParam States - Union of all possible state names (string literals)\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n *\n * @example\n * Basic vending machine state machine\n * ```typescript\n * type Events = {\n * insertCoin: { amount: number };\n * selectItem: { itemId: string };\n * cancel: {};\n * };\n *\n * type States = \"IDLE\" | \"PAYMENT\" | \"DISPENSING\";\n *\n * interface VendingContext extends BaseContext {\n * balance: number;\n * setup() { this.balance = 0; }\n * cleanup() {}\n * }\n *\n * const context: VendingContext = {\n * balance: 0,\n * setup() { this.balance = 0; },\n * cleanup() {}\n * };\n *\n * const machine = new TemplateStateMachine<Events, VendingContext, States>(\n * {\n * IDLE: new IdleState(),\n * PAYMENT: new PaymentState(),\n * DISPENSING: new DispensingState()\n * },\n * \"IDLE\",\n * context\n * );\n *\n * // Trigger events\n * machine.happens(\"insertCoin\", { amount: 100 });\n * machine.happens(\"selectItem\", { itemId: \"A1\" });\n * ```\n *\n * @category State Machine Core\n * @see {@link TemplateState} for creating state implementations\n * @see {@link StateMachine} for the interface definition\n */\nexport class TemplateStateMachine<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string = 'IDLE',\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> implements StateMachine<EventPayloadMapping, Context, States, EventOutputMapping> {\n\n protected _currentState: States | \"INITIAL\" | \"TERMINAL\";\n protected _states: Record<States, State<EventPayloadMapping, Context, States, EventOutputMapping>>;\n protected _context: Context;\n protected _statesArray: States[];\n protected _stateChangeCallbacks: StateChangeCallback<States>[];\n protected _happensCallbacks: ((args: EventArgs<EventPayloadMapping, keyof EventPayloadMapping | string>, context: Context) => void)[];\n protected _timeouts: ReturnType<typeof setTimeout> | undefined = undefined;\n protected _initialState: States;\n\n constructor(states: Record<States, State<EventPayloadMapping, Context, States, EventOutputMapping>>, initialState: States, context: Context, autoStart: boolean = true){\n this._states = states;\n this._currentState = \"INITIAL\";\n this._initialState = initialState;\n this._context = context;\n this._statesArray = Object.keys(states) as States[];\n this._stateChangeCallbacks = [];\n this._happensCallbacks = [];\n if(autoStart){\n this.start();\n }\n }\n\n reset(): void {\n this.wrapup();\n this.switchTo(\"INITIAL\");\n this.start();\n }\n\n start(): void {\n if(this.currentState !== \"INITIAL\"){\n return;\n }\n this._context.setup();\n this.switchTo(this._initialState);\n this._states[this._initialState].uponEnter(this._context, this, this._initialState);\n }\n\n wrapup(): void {\n if(this._currentState === \"TERMINAL\") {\n return;\n }\n const originalState = this._currentState;\n if(originalState !== \"INITIAL\") {\n this._states[originalState].beforeExit(this._context, this, \"TERMINAL\");\n }\n this._context.cleanup();\n this.switchTo(\"TERMINAL\");\n }\n\n switchTo(state: States | \"INITIAL\" | \"TERMINAL\"): void {\n this._currentState = state;\n }\n \n // Implementation signature - matches both overloads\n happens<K extends keyof EventPayloadMapping>(...args: EventArgs<EventPayloadMapping, K>): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;\n happens<K extends string>(...args: EventArgs<EventPayloadMapping, K>): EventResult<States, unknown>;\n happens<K extends keyof EventPayloadMapping | string>(...args: EventArgs<EventPayloadMapping, K>): EventResult<States, unknown> {\n if(this._timeouts){\n clearTimeout(this._timeouts);\n }\n if(this._currentState === \"INITIAL\" || this._currentState === \"TERMINAL\"){\n return { handled: false };\n }\n this._happensCallbacks.forEach(callback => callback(args, this._context));\n const result = this._states[this._currentState].handles(args, this._context, this);\n if(result.handled && result.nextState !== undefined && result.nextState !== this._currentState){ // TODO: whether or not to transition to the same state (currently no, uponEnter and beforeExit will not be called if the state is the same)\n const originalState = this._currentState;\n this._states[this._currentState].beforeExit(this._context, this, result.nextState);\n this.switchTo(result.nextState);\n this._states[this._currentState].uponEnter(this._context, this, originalState);\n for(const callback of this._stateChangeCallbacks){\n callback(originalState, this._currentState);\n }\n }\n return result;\n }\n\n onStateChange(callback: StateChangeCallback<States>): void {\n this._stateChangeCallbacks.push(callback);\n }\n\n onHappens(callback: (args: EventArgs<EventPayloadMapping, keyof EventPayloadMapping | string>, context: Context) => void): void {\n this._happensCallbacks.push(callback);\n }\n\n get currentState(): States | \"INITIAL\" | \"TERMINAL\" {\n return this._currentState;\n }\n\n setContext(context: Context): void {\n this._context = context;\n }\n\n get possibleStates(): States[] {\n return this._statesArray;\n }\n\n get states(): Record<States, State<EventPayloadMapping, Context, States, EventOutputMapping>> {\n return this._states;\n }\n}\n/**\n * Abstract base class for state machine states.\n *\n * @remarks\n * This abstract class provides the foundation for implementing individual states in a state machine.\n * Each state defines how it responds to events through the `eventReactions` object.\n *\n * ## Key Concepts\n *\n * - **Event Reactions**: Define handlers for events this state cares about. Unhandled events are ignored.\n * - **Guards**: Conditional logic that determines which state to transition to based on context\n * - **Lifecycle Hooks**: `uponEnter` and `beforeExit` callbacks for state transition side effects\n * - **Selective Handling**: Only define reactions for events relevant to this state\n *\n * ## Implementation Pattern\n *\n * 1. Extend this class for each state in your state machine\n * 2. Implement the `eventReactions` property with handlers for relevant events\n * 3. Optionally override `uponEnter` and `beforeExit` for lifecycle logic\n * 4. Optionally define `guards` and `eventGuards` for conditional transitions\n *\n * @typeParam EventPayloadMapping - Object mapping event names to their payload types\n * @typeParam Context - Context type shared across all states\n * @typeParam States - Union of all possible state names (string literals)\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n *\n * @example\n * Simple state implementation\n * ```typescript\n * class IdleState extends TemplateState<MyEvents, MyContext, MyStates> {\n * eventReactions = {\n * start: {\n * action: (context, event) => {\n * console.log('Starting...');\n * context.startTime = Date.now();\n * },\n * defaultTargetState: \"ACTIVE\"\n * },\n * reset: {\n * action: (context, event) => {\n * context.counter = 0;\n * }\n * // No state transition - stays in IDLE\n * }\n * };\n *\n * uponEnter(context, stateMachine, fromState) {\n * console.log(`Entered IDLE from ${fromState}`);\n * }\n * }\n * ```\n *\n * @example\n * State with guards for conditional transitions\n * ```typescript\n * class PaymentState extends TemplateState<Events, VendingContext, States> {\n * guards = {\n * hasEnoughMoney: (context) => context.balance >= context.itemPrice,\n * needsChange: (context) => context.balance > context.itemPrice\n * };\n *\n * eventReactions = {\n * selectItem: {\n * action: (context, event) => {\n * context.selectedItem = event.itemId;\n * context.itemPrice = getPrice(event.itemId);\n * },\n * defaultTargetState: \"IDLE\" // Fallback if no guard matches\n * }\n * };\n *\n * eventGuards = {\n * selectItem: [\n * { guard: 'hasEnoughMoney', target: 'DISPENSING' },\n * // If hasEnoughMoney is false, uses defaultTargetState (IDLE)\n * ]\n * };\n * }\n * ```\n *\n * @category State Machine Core\n * @see {@link TemplateStateMachine} for the state machine implementation\n * @see {@link EventReactions} for defining event handlers\n */\nexport abstract class TemplateState<\n EventPayloadMapping, \n Context extends BaseContext, \n States extends string = 'IDLE',\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> implements State<EventPayloadMapping, Context, States, EventOutputMapping> {\n\n protected _eventReactions: EventReactions<EventPayloadMapping, Context, States, EventOutputMapping> = {} as EventReactions<EventPayloadMapping, Context, States, EventOutputMapping>;\n protected _guards: Guard<Context> = {} as Guard<Context>;\n protected _eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>> = {} as Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;\n protected _delay: Delay<Context, EventPayloadMapping, States, EventOutputMapping> | undefined = undefined;\n\n get handlingEvents(): (keyof EventPayloadMapping)[] {\n return Object.keys(this._eventReactions) as (keyof EventPayloadMapping)[];\n }\n\n get guards(): Guard<Context> {\n return this._guards;\n }\n\n get eventGuards(): Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>> {\n return this._eventGuards;\n }\n\n get delay(): Delay<Context, EventPayloadMapping, States, EventOutputMapping> | undefined {\n return this._delay;\n }\n\n uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, from: States | \"INITIAL\"): void {\n // console.log(\"enter\");\n }\n\n beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, to: States | \"TERMINAL\"): void {\n // console.log('leave');\n }\n\n handles<K extends (keyof EventPayloadMapping | string)>(args: EventArgs<EventPayloadMapping, K>, context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>{\n const eventKey = args[0] as keyof EventPayloadMapping;\n const eventPayload = args[1] as EventPayloadMapping[keyof EventPayloadMapping];\n if (this._eventReactions[eventKey]) {\n // Capture the output from the action\n const output = this._eventReactions[eventKey].action(context, eventPayload, stateMachine);\n const targetState = this._eventReactions[eventKey].defaultTargetState;\n const guardsToEvaluate = this._eventGuards[eventKey];\n const baseResult = { handled: true as const, nextState: targetState };\n const resultWithOutput = output !== undefined \n ? { ...baseResult, output } \n : baseResult;\n \n if(guardsToEvaluate){\n const target = guardsToEvaluate.find((guard)=>{\n if(this._guards[guard.guard]){\n return this._guards[guard.guard](context);\n }\n return false;\n });\n const finalResult = target \n ? { ...resultWithOutput, nextState: target.target }\n : resultWithOutput;\n return finalResult as EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;\n }\n return resultWithOutput as EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;\n }\n return {handled: false};\n }\n}\n\n\n/**\n * Creates a type guard function for checking if a value belongs to a specific set of states.\n *\n * @remarks\n * This utility function generates a TypeScript type guard that narrows a string type\n * to a specific union of string literals. Useful when you have multiple state types\n * and need to distinguish between them at runtime.\n *\n * @typeParam T - String literal type to guard for\n * @param set - Readonly array of string literals defining the valid states\n * @returns A type guard function that checks if a string is in the set\n *\n * @example\n * Creating state guards for hierarchical state machines\n * ```typescript\n * type MainStates = \"idle\" | \"active\" | \"paused\";\n * type SubStates = \"loading\" | \"processing\" | \"complete\";\n * type AllStates = MainStates | SubStates;\n *\n * const MAIN_STATES = [\"idle\", \"active\", \"paused\"] as const;\n * const isMainState = createStateGuard(MAIN_STATES);\n *\n * function handleState(state: AllStates) {\n * if (isMainState(state)) {\n * // TypeScript knows state is MainStates here\n * console.log('Main state:', state);\n * } else {\n * // TypeScript knows state is SubStates here\n * console.log('Sub state:', state);\n * }\n * }\n * ```\n *\n * @category Utilities\n */\nexport function createStateGuard<T extends string>(set: readonly T[]) {\n return (s: string): s is T => set.includes(s as T);\n}\n",
|
|
6
|
+
"/**\n * Schema-based state machine factory for runtime creation.\n *\n * @remarks\n * This module provides utilities for creating state machines from JSON-like schemas,\n * enabling dynamic state machine creation at runtime. This is useful for GUI builders\n * or configuration-driven state machines.\n *\n * @category Runtime Factory\n */\n\nimport {\n BaseContext,\n TemplateState,\n TemplateStateMachine,\n StateMachine,\n EventResult,\n NO_OP,\n DefaultOutputMapping,\n} from \"./interface\";\n\n/**\n * Payload type definition for an event in the schema.\n * Can be an empty object (no payload) or an object with typed fields.\n */\nexport type EventPayloadSchema = Record<string, string> | {};\n\n/**\n * Action function that can be executed when an event is handled.\n * Receives context, event payload, and the state machine instance.\n * Can return a value that will be included in the event result.\n * \n * @typeParam Context - The context type\n * @typeParam EventPayloadMapping - Mapping of event names to their payload types\n * @typeParam EventName - The specific event name this action handles\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n */\nexport type ActionFunction<\n Context extends BaseContext = BaseContext,\n EventPayloadMapping = any,\n EventName extends keyof EventPayloadMapping = keyof EventPayloadMapping,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> = (\n context: Context,\n payload: EventPayloadMapping[EventName],\n stateMachine: StateMachine<EventPayloadMapping, Context, any, EventOutputMapping>\n) => EventName extends keyof EventOutputMapping ? (EventOutputMapping[EventName] | void) : void | unknown;\n\n/**\n * Guard function that evaluates whether a transition should occur.\n * Returns true if the guard condition is met.\n */\nexport type GuardFunction<Context extends BaseContext = BaseContext> = (\n context: Context\n) => boolean;\n\n/**\n * Definition of a single state transition.\n * \n * @typeParam Context - The context type\n * @typeParam EventPayloadMapping - Mapping of event names to their payload types\n * @typeParam EventName - The specific event name for this transition\n * @typeParam StateNames - Union type of all valid state names\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n */\nexport interface TransitionDefinition<\n Context extends BaseContext = BaseContext,\n EventPayloadMapping = any,\n EventName extends keyof EventPayloadMapping = keyof EventPayloadMapping,\n StateNames extends string = string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> {\n /** The event that triggers this transition */\n event: EventName;\n /** The target state after this transition */\n targetState: StateNames;\n /** Optional action to execute when this transition occurs. Can return a value that will be included in the event result. */\n action?: ActionFunction<Context, EventPayloadMapping, EventName, EventOutputMapping>;\n /** \n * Optional guard conditions (evaluated in order, first true guard wins).\n * Guards can be either:\n * - A guard function defined inline\n * - A string reference to a guard defined in the state's `guards` section\n */\n guards?: Array<{\n /** Guard function to evaluate, or name of a guard defined in the state's guards section */\n guard: GuardFunction<Context> | string;\n /** Target state if this guard evaluates to true */\n targetState: StateNames;\n }>;\n}\n\n/**\n * Union type of all possible transition definitions for a given event payload mapping.\n * This ensures each transition's action payload is typed based on its specific event.\n * \n * @typeParam Context - The context type\n * @typeParam EventPayloadMapping - Mapping of event names to their payload types\n * @typeParam StateNames - Union type of all valid state names\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n */\nexport type TransitionDefinitionUnion<\n Context extends BaseContext = BaseContext,\n EventPayloadMapping = any,\n StateNames extends string = string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> = {\n [K in keyof EventPayloadMapping]: TransitionDefinition<Context, EventPayloadMapping, K, StateNames, EventOutputMapping>\n}[keyof EventPayloadMapping];\n\n/**\n * Definition of a single state in the state machine.\n * \n * @typeParam Context - The context type\n * @typeParam EventPayloadMapping - Mapping of event names to their payload types\n * @typeParam StateNames - Union type of all valid state names\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n * \n * @example\n * ```typescript\n * {\n * name: \"PAYING\",\n * guards: {\n * hasEnoughBalance: (context) => context.balance >= context.itemPrice,\n * hasInsufficientBalance: (context) => context.balance < context.itemPrice,\n * },\n * transitions: [\n * {\n * event: \"pay\",\n * targetState: \"SELECTING\",\n * guards: [\n * { guard: \"hasEnoughBalance\", targetState: \"CONFIRMED\" },\n * { guard: \"hasInsufficientBalance\", targetState: \"PAYING\" },\n * ],\n * },\n * ],\n * }\n * ```\n */\nexport interface StateDefinition<\n Context extends BaseContext = BaseContext,\n EventPayloadMapping = any,\n StateNames extends string = string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> {\n /** Name of this state */\n name: StateNames;\n /** Transitions available from this state */\n transitions: TransitionDefinitionUnion<Context, EventPayloadMapping, StateNames, EventOutputMapping>[];\n /** \n * Optional mapping of guard names to guard functions.\n * Guards defined here can be reused across multiple transitions within this state\n * by referencing them by name in the transition's guards array.\n * \n * The guard functions receive the context parameter typed with the Context type parameter,\n * not BaseContext, so you get full type safety for your context properties.\n */\n guards?: Record<string, GuardFunction<Context>>;\n /** Optional callback when entering this state */\n onEnter?: (context: Context, fromState: StateNames) => void;\n /** Optional callback when exiting this state */\n onExit?: (context: Context, toState: StateNames) => void;\n}\n\n/**\n * Helper type to extract state names from a states array.\n * If the array is a readonly tuple of string literals, it extracts the union type.\n * Otherwise, it falls back to string.\n * \n * @typeParam StatesArray - The states array type\n */\nexport type ExtractStateNames<StatesArray> = \n StatesArray extends readonly (infer S)[]\n ? S extends string\n ? S\n : string\n : StatesArray extends (infer S)[]\n ? S extends string\n ? S\n : string\n : string;\n\n/**\n * Helper type to infer StateNames from a StateMachineSchema.\n * Extracts the state names from the schema's states array.\n */\ntype InferStateNamesFromSchema<Schema> = \n Schema extends StateMachineSchema<any, any, infer SN, any>\n ? SN\n : Schema extends { states: infer S }\n ? ExtractStateNames<S>\n : string;\n\n/**\n * Helper function to create a typed state machine schema with inferred state names.\n * Use this when you have a const states array and want TypeScript to infer the state names.\n * \n * @example\n * ```typescript\n * const states = [\"IDLE\", \"RUNNING\", \"PAUSED\"] as const;\n * const schema = createStateMachineSchemaWithInferredStates<TimerContext, TimerEvents>({\n * states,\n * events: { start: {}, stop: {} },\n * initialState: \"IDLE\",\n * stateDefinitions: [\n * {\n * name: \"IDLE\", // TypeScript knows this must be one of the states\n * transitions: [\n * {\n * event: \"start\",\n * targetState: \"RUNNING\", // TypeScript knows this must be one of the states\n * }\n * ]\n * }\n * ]\n * });\n * ```\n */\nexport function createStateMachineSchemaWithInferredStates<\n Context extends BaseContext,\n EventPayloadMapping,\n States extends readonly string[],\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n>(\n schema: Omit<StateMachineSchema<Context, EventPayloadMapping, ExtractStateNames<States>, EventOutputMapping>, 'states'> & {\n states: States;\n }\n): StateMachineSchema<Context, EventPayloadMapping, ExtractStateNames<States>, EventOutputMapping> {\n return schema as unknown as StateMachineSchema<Context, EventPayloadMapping, ExtractStateNames<States>, EventOutputMapping>;\n}\n\n/**\n * Complete schema definition for a state machine.\n * \n * @typeParam Context - The context type\n * @typeParam EventPayloadMapping - Mapping of event names to their payload types\n * @typeParam StateNames - Union type of all valid state names (inferred from states array)\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n */\nexport interface StateMachineSchema<\n Context extends BaseContext = BaseContext,\n EventPayloadMapping = any,\n StateNames extends string = string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> {\n /** Array of all possible state names */\n states: readonly StateNames[] | StateNames[];\n /** Mapping of event names to their payload types */\n events: EventPayloadMapping;\n /** Array of state definitions */\n stateDefinitions: StateDefinition<Context, EventPayloadMapping, StateNames, EventOutputMapping>[];\n /** Initial state name */\n initialState: StateNames;\n}\n\n/**\n * Creates a state machine from a schema definition.\n *\n * @remarks\n * This factory function takes a schema and creates a fully functional state machine\n * at runtime. The resulting state machine uses type erasure (`any` types) but maintains\n * full runtime functionality.\n *\n * Actions can return values that will be included in the event result. To enable\n * typed outputs, provide an EventOutputMapping type parameter that maps event names\n * to their output types.\n *\n * @typeParam Context - The context type\n * @typeParam EventPayloadMapping - Mapping of event names to their payload types\n * @typeParam EventOutputMapping - Optional mapping of events to their output types\n *\n * @param schema - The schema definition for the state machine\n * @param context - The context instance to use for the state machine\n * @returns A fully configured state machine instance\n *\n * @example\n * Basic state machine without outputs\n * ```typescript\n * const schema: StateMachineSchema = {\n * states: [\"IDLE\", \"RUNNING\", \"PAUSED\"],\n * events: {\n * start: {},\n * stop: {},\n * pause: {},\n * resume: {}\n * },\n * initialState: \"IDLE\",\n * stateDefinitions: [\n * {\n * name: \"IDLE\",\n * transitions: [\n * {\n * event: \"start\",\n * targetState: \"RUNNING\",\n * action: (context) => console.log(\"Starting...\")\n * }\n * ]\n * }\n * ]\n * };\n *\n * const machine = createStateMachineFromSchema(schema, context);\n * machine.happens(\"start\");\n * ```\n *\n * @example\n * State machine with typed outputs\n * ```typescript\n * type Events = { calculate: { value: number }; getResult: {} };\n * type Outputs = { calculate: number; getResult: number };\n *\n * const schema: StateMachineSchema<MyContext, Events, Outputs> = {\n * states: [\"READY\"],\n * events: { calculate: { value: 0 }, getResult: {} },\n * initialState: \"READY\",\n * stateDefinitions: [\n * {\n * name: \"READY\",\n * transitions: [\n * {\n * event: \"calculate\",\n * targetState: \"READY\",\n * action: (context, payload) => {\n * context.total += payload.value;\n * return context.total; // Return value included in result\n * }\n * },\n * {\n * event: \"getResult\",\n * targetState: \"READY\",\n * action: (context) => context.total // Return current total\n * }\n * ]\n * }\n * ]\n * };\n *\n * const machine = createStateMachineFromSchema<MyContext, Events, Outputs>(schema, context);\n * const result = machine.happens(\"calculate\", { value: 10 });\n * if (result.handled && \"output\" in result) {\n * console.log(result.output); // Typed as number\n * }\n * ```\n *\n * @category Runtime Factory\n */\nexport function createStateMachineFromSchema<\n Schema extends StateMachineSchema<any, any, any, any>\n>(\n schema: Schema,\n context: Schema extends StateMachineSchema<infer C, any, any, any> ? C : BaseContext\n): Schema extends StateMachineSchema<infer C, infer EPM, any, infer EOM>\n ? StateMachine<EPM, C, any, EOM>\n : StateMachine<any, BaseContext, any, any> {\n // Extract types from schema for internal use\n type SchemaContext = Schema extends StateMachineSchema<infer C, any, any, any> ? C : BaseContext;\n type SchemaEventPayloadMapping = Schema extends StateMachineSchema<any, infer EPM, any, any> ? EPM : any;\n type SchemaEventOutputMapping = Schema extends StateMachineSchema<any, any, any, infer EOM> ? EOM : any;\n\n // Validate schema\n if (!schema.states.includes(schema.initialState)) {\n throw new Error(\n `Initial state \"${schema.initialState}\" must be in the states array`\n );\n }\n\n // Validate all state definitions reference valid states\n for (const stateDef of schema.stateDefinitions) {\n if (!schema.states.includes(stateDef.name)) {\n throw new Error(\n `State definition \"${stateDef.name}\" is not in the states array`\n );\n }\n\n for (const transition of stateDef.transitions) {\n const eventsRecord = schema.events as Record<string, any>;\n if (!(String(transition.event) in eventsRecord)) {\n throw new Error(\n `Event \"${String(transition.event)}\" in state \"${stateDef.name}\" is not defined in events`\n );\n }\n if (!schema.states.includes(transition.targetState)) {\n throw new Error(\n `Target state \"${transition.targetState}\" in transition from \"${stateDef.name}\" is not in the states array`\n );\n }\n\n if (transition.guards) {\n for (const guard of transition.guards) {\n if (!schema.states.includes(guard.targetState)) {\n throw new Error(\n `Guard target state \"${guard.targetState}\" is not in the states array`\n );\n }\n // Validate guard references (if guard is a string, it must exist in state's guards)\n if (typeof guard.guard === 'string') {\n if (!stateDef.guards || !(guard.guard in stateDef.guards)) {\n throw new Error(\n `Guard \"${guard.guard}\" referenced in state \"${stateDef.name}\" for event \"${String(transition.event)}\" is not defined in the state's guards section`\n );\n }\n }\n }\n }\n }\n }\n\n // Create dynamic state classes\n const stateInstances: Record<string, TemplateState<any, SchemaContext, any>> = {};\n\n for (const stateDef of schema.stateDefinitions) {\n // Build event reactions from transitions\n const eventReactions: any = {};\n const allGuards: Record<string, GuardFunction<SchemaContext>> = {};\n const eventGuards: any = {};\n\n // First, collect state-level guard definitions\n if (stateDef.guards) {\n for (const [guardName, guardFunction] of Object.entries(stateDef.guards)) {\n allGuards[guardName] = guardFunction;\n }\n }\n\n // Then, collect guards from transitions and event reactions\n for (const transition of stateDef.transitions) {\n const eventName = transition.event;\n\n // Build event reaction\n eventReactions[eventName] = {\n action: transition.action || NO_OP,\n defaultTargetState: transition.targetState,\n };\n\n // Build guards for this event if they exist\n if (transition.guards && transition.guards.length > 0) {\n const guardMappings: any[] = [];\n transition.guards.forEach((guardDef, index) => {\n let guardKey: string;\n \n // Check if guard is a string (reference to state-level guard) or a function\n if (typeof guardDef.guard === 'string') {\n // Reference to a state-level guard\n guardKey = guardDef.guard;\n // Validate that the guard exists\n if (!allGuards[guardKey]) {\n throw new Error(\n `Guard \"${guardKey}\" referenced in state \"${stateDef.name}\" for event \"${String(eventName)}\" is not defined in the state's guards section`\n );\n }\n } else {\n // Inline guard function - create a unique key\n guardKey = `guard_${stateDef.name}_${String(eventName)}_${index}`;\n allGuards[guardKey] = guardDef.guard;\n }\n \n guardMappings.push({\n guard: guardKey,\n target: guardDef.targetState,\n });\n });\n eventGuards[eventName] = guardMappings;\n }\n }\n\n // Create state instance with all guards and reactions\n class DynamicState extends TemplateState<any, SchemaContext, any> {\n eventReactions = eventReactions;\n protected _guards = allGuards as any;\n protected _eventGuards = eventGuards as any;\n }\n\n const stateInstance = new DynamicState();\n \n // Add lifecycle hooks\n if (stateDef.onEnter) {\n const originalOnEnter = stateInstance.uponEnter.bind(stateInstance);\n stateInstance.uponEnter = (ctx, sm, from) => {\n originalOnEnter(ctx, sm, from);\n stateDef.onEnter!(ctx, from);\n };\n }\n if (stateDef.onExit) {\n const originalOnExit = stateInstance.beforeExit.bind(stateInstance);\n stateInstance.beforeExit = (ctx, sm, to) => {\n originalOnExit(ctx, sm, to);\n stateDef.onExit!(ctx, to);\n };\n }\n \n stateInstances[stateDef.name] = stateInstance;\n }\n\n // Ensure all states have instances (create empty states for states without definitions)\n for (const stateName of schema.states) {\n if (!stateInstances[stateName]) {\n class EmptyState extends TemplateState<any, SchemaContext, any> {\n eventReactions = {};\n }\n stateInstances[stateName] = new EmptyState();\n }\n }\n\n // Create and return the state machine\n return new TemplateStateMachine<SchemaEventPayloadMapping, SchemaContext, any, SchemaEventOutputMapping>(\n stateInstances as any,\n schema.initialState as any,\n context,\n true\n ) as any;\n}\n",
|
|
7
|
+
"/**\n * Hierarchical State Machine POC\n *\n * @remarks\n * This module provides support for hierarchical (nested) state machines where\n * states can contain child state machines. This enables modeling complex\n * stateful behavior with parent-child relationships.\n *\n * ## Key Features\n *\n * - **Composite States**: States that contain their own internal state machine\n * - **Event Propagation**: Events bubble from child to parent if unhandled\n * - **Default Child States**: Automatic entry into default child state\n * - **History States**: Remember last active child state (optional)\n * - **State Path Tracking**: Full hierarchical path (e.g., \"PARENT.CHILD\")\n *\n * @category Hierarchical State Machines\n */\n\nimport {\n BaseContext,\n StateMachine,\n TemplateStateMachine,\n TemplateState,\n State,\n EventResult,\n DefaultOutputMapping,\n EventArgs,\n} from \"./interface\";\n\n/**\n * Represents a hierarchical state path using dot notation.\n * Example: \"PARENT.CHILD\" means we're in CHILD state within PARENT state.\n */\nexport type HierarchicalStatePath<ParentStates extends string, ChildStates extends string> =\n | ParentStates\n | `${ParentStates}.${ChildStates}`;\n\n/**\n * Configuration for a composite state's child state machine.\n *\n * @typeParam EventPayloadMapping - Event payload mapping\n * @typeParam Context - Context type\n * @typeParam ChildStates - Child state names\n * @typeParam EventOutputMapping - Event output mapping\n */\nexport interface ChildStateMachineConfig<\n EventPayloadMapping = any,\n Context extends BaseContext = BaseContext,\n ChildStates extends string = string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> {\n /** The child state machine instance */\n stateMachine: StateMachine<EventPayloadMapping, Context, ChildStates, EventOutputMapping>;\n /** Default child state to enter when parent state is entered */\n defaultChildState: ChildStates;\n /** Whether to remember the last active child state (history state) */\n rememberHistory?: boolean;\n}\n\n/**\n * Composite state that contains a child state machine.\n *\n * @remarks\n * A composite state is a state that contains its own internal state machine.\n * When the composite state is active, its child state machine is also active.\n * Events are first handled by the child state machine, and if unhandled,\n * they bubble up to the parent state machine.\n *\n * @typeParam EventPayloadMapping - Event payload mapping\n * @typeParam Context - Context type\n * @typeParam ParentStates - Parent state names\n * @typeParam ChildStates - Child state names\n * @typeParam EventOutputMapping - Event output mapping\n *\n * @example\n * ```typescript\n * type ParentStates = \"IDLE\" | \"ACTIVE\";\n * type ChildStates = \"LOADING\" | \"READY\" | \"ERROR\";\n *\n * class ActiveState extends CompositeState<Events, Context, ParentStates, ChildStates> {\n * eventReactions = {\n * stop: {\n * action: () => console.log(\"Stopping...\"),\n * defaultTargetState: \"IDLE\"\n * }\n * };\n *\n * getChildStateMachine() {\n * return {\n * stateMachine: new TemplateStateMachine(...),\n * defaultChildState: \"LOADING\",\n * rememberHistory: true\n * };\n * }\n * }\n * ```\n */\nexport abstract class CompositeState<\n EventPayloadMapping = any,\n Context extends BaseContext = BaseContext,\n ParentStates extends string = string,\n ChildStates extends string = string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> extends TemplateState<EventPayloadMapping, Context, ParentStates, EventOutputMapping> {\n protected _childStateMachineConfig: ChildStateMachineConfig<EventPayloadMapping, Context, ChildStates, EventOutputMapping> | null = null;\n protected _historyState: ChildStates | null = null;\n protected _context: Context | null = null;\n\n /**\n * Returns the configuration for the child state machine.\n * Override this method to provide child state machine setup.\n */\n protected abstract getChildStateMachine(): ChildStateMachineConfig<EventPayloadMapping, Context, ChildStates, EventOutputMapping>;\n\n /**\n * Gets the current child state, or null if no child state machine is active.\n */\n getCurrentChildState(): ChildStates | null {\n if (!this._childStateMachineConfig) {\n return null;\n }\n // Access currentState through TemplateStateMachine's getter\n const stateMachine = this._childStateMachineConfig.stateMachine as TemplateStateMachine<EventPayloadMapping, Context, ChildStates, EventOutputMapping>;\n const current = stateMachine.currentState;\n return current === \"INITIAL\" || current === \"TERMINAL\" ? null : current as ChildStates;\n }\n\n /**\n * Gets the full hierarchical path of the current state.\n */\n getStatePath(parentState: ParentStates): HierarchicalStatePath<ParentStates, ChildStates> {\n const childState = this.getCurrentChildState();\n if (childState === null) {\n return parentState;\n }\n return `${parentState}.${childState}` as HierarchicalStatePath<ParentStates, ChildStates>;\n }\n\n override uponEnter(\n context: Context,\n stateMachine: StateMachine<EventPayloadMapping, Context, ParentStates, EventOutputMapping>,\n from: ParentStates | \"INITIAL\"\n ): void {\n super.uponEnter(context, stateMachine, from);\n\n // Set context for child state machine creation\n this._context = context;\n\n // Initialize child state machine\n const config = this.getChildStateMachine();\n this._childStateMachineConfig = config;\n\n // Determine which child state to enter\n const childStateToEnter = config.rememberHistory && this._historyState\n ? this._historyState\n : config.defaultChildState;\n\n // Start child state machine and transition to the appropriate child state\n const childMachine = config.stateMachine as TemplateStateMachine<EventPayloadMapping, Context, ChildStates, EventOutputMapping>;\n if (childMachine.currentState === \"INITIAL\") {\n childMachine.start();\n }\n childMachine.switchTo(childStateToEnter);\n const childState = childMachine.states[childStateToEnter];\n childState.uponEnter(context, childMachine, \"INITIAL\");\n }\n\n override beforeExit(\n context: Context,\n stateMachine: StateMachine<EventPayloadMapping, Context, ParentStates, EventOutputMapping>,\n to: ParentStates | \"TERMINAL\"\n ): void {\n // Save current child state if history is enabled\n if (this._childStateMachineConfig?.rememberHistory) {\n const currentChild = this.getCurrentChildState();\n if (currentChild !== null) {\n this._historyState = currentChild;\n }\n }\n\n // Exit child state machine\n if (this._childStateMachineConfig) {\n const currentChild = this.getCurrentChildState();\n if (currentChild !== null) {\n const childState = this._childStateMachineConfig.stateMachine.states[currentChild];\n childState.beforeExit(context, this._childStateMachineConfig.stateMachine, \"TERMINAL\");\n }\n this._childStateMachineConfig.stateMachine.wrapup();\n }\n\n super.beforeExit(context, stateMachine, to);\n }\n\n override handles<K extends keyof EventPayloadMapping | string>(\n args: EventArgs<EventPayloadMapping, K>,\n context: Context,\n stateMachine: StateMachine<EventPayloadMapping, Context, ParentStates, EventOutputMapping>\n ): EventResult<ParentStates, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void> {\n // First, try to handle in child state machine\n if (this._childStateMachineConfig) {\n // Use type assertion to call happens with the args\n const childMachine = this._childStateMachineConfig.stateMachine;\n const childResult = (childMachine.happens as any)(...args);\n if (childResult.handled) {\n // Event was handled by child - check if child state transition occurred\n // If child transitioned, we might need to propagate that information\n // For now, we'll return that it was handled but don't change parent state\n return {\n handled: true,\n // Don't transition parent state, child handled it\n } as EventResult<ParentStates, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;\n }\n }\n\n // If child didn't handle it, try parent state\n return super.handles(args, context, stateMachine);\n }\n}\n\n/**\n * Extended state machine that supports hierarchical state paths.\n *\n * @remarks\n * This class extends TemplateStateMachine to track and expose hierarchical\n * state paths when composite states are used.\n *\n * @typeParam EventPayloadMapping - Event payload mapping\n * @typeParam Context - Context type\n * @typeParam States - State names\n * @typeParam EventOutputMapping - Event output mapping\n */\nexport class HierarchicalStateMachine<\n EventPayloadMapping = any,\n Context extends BaseContext = BaseContext,\n States extends string = string,\n EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>\n> extends TemplateStateMachine<EventPayloadMapping, Context, States, EventOutputMapping> {\n /**\n * Gets the current hierarchical state path.\n * Returns a simple state name for non-composite states,\n * or a dot-notation path for composite states (e.g., \"PARENT.CHILD\").\n */\n getCurrentStatePath(): string {\n const currentState = this.currentState;\n if (currentState === \"INITIAL\" || currentState === \"TERMINAL\") {\n return currentState;\n }\n\n const state = this.states[currentState];\n if (state instanceof CompositeState) {\n return state.getStatePath(currentState);\n }\n\n return currentState;\n }\n\n /**\n * Gets all active states in the hierarchy.\n * Returns an array where the first element is the top-level state,\n * and subsequent elements are nested child states.\n */\n getActiveStatePath(): string[] {\n const currentState = this.currentState;\n if (currentState === \"INITIAL\" || currentState === \"TERMINAL\") {\n return [currentState];\n }\n\n const path: string[] = [currentState];\n const state = this.states[currentState];\n\n if (state instanceof CompositeState) {\n const childState = state.getCurrentChildState();\n if (childState !== null) {\n path.push(childState);\n }\n }\n\n return path;\n }\n\n /**\n * Checks if the state machine is currently in a specific hierarchical path.\n * Supports both simple state names and dot-notation paths.\n *\n * @param path - State path to check (e.g., \"PARENT\" or \"PARENT.CHILD\")\n */\n isInStatePath(path: string): boolean {\n const currentPath = this.getCurrentStatePath();\n return currentPath === path || currentPath.startsWith(path + \".\");\n }\n}\n"
|
|
6
8
|
],
|
|
7
|
-
"mappings": "AAsFO,IAAM,EAAc,IAAI,
|
|
8
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "A4A1425C023A074164756E2164756E21",
|
|
9
11
|
"names": []
|
|
10
12
|
}
|
package/interface.d.ts
CHANGED
|
@@ -87,13 +87,20 @@ export declare const NO_OP: NOOP;
|
|
|
87
87
|
export type EventNotHandled = {
|
|
88
88
|
handled: false;
|
|
89
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* Helper type that conditionally includes the output property.
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
type WithOutput<Output> = Output extends void ? {} : {
|
|
95
|
+
output?: Output;
|
|
96
|
+
};
|
|
90
97
|
/**
|
|
91
98
|
* Result type when an event is successfully handled by a state.
|
|
92
99
|
*
|
|
93
100
|
* @remarks
|
|
94
101
|
* This type represents a successful event handling result. It can optionally include:
|
|
95
102
|
* - `nextState`: The state to transition to (if different from current)
|
|
96
|
-
* - `output`: A return value from the event handler
|
|
103
|
+
* - `output`: A return value from the event handler (only present when Output is not void)
|
|
97
104
|
*
|
|
98
105
|
* @typeParam States - Union of all possible state names in the state machine
|
|
99
106
|
* @typeParam Output - The output type for this event (defaults to void)
|
|
@@ -104,6 +111,7 @@ export type EventNotHandled = {
|
|
|
104
111
|
* const result: EventHandled<"IDLE" | "ACTIVE"> = {
|
|
105
112
|
* handled: true,
|
|
106
113
|
* nextState: "ACTIVE"
|
|
114
|
+
* // output property does not exist when Output is void
|
|
107
115
|
* };
|
|
108
116
|
*
|
|
109
117
|
* // With output value
|
|
@@ -119,8 +127,7 @@ export type EventNotHandled = {
|
|
|
119
127
|
export type EventHandled<States extends string, Output = void> = {
|
|
120
128
|
handled: true;
|
|
121
129
|
nextState?: States;
|
|
122
|
-
|
|
123
|
-
};
|
|
130
|
+
} & WithOutput<Output>;
|
|
124
131
|
/**
|
|
125
132
|
* Discriminated union representing the result of event handling.
|
|
126
133
|
*
|
|
@@ -172,6 +179,9 @@ export interface StateMachine<EventPayloadMapping, Context extends BaseContext,
|
|
|
172
179
|
onStateChange(callback: StateChangeCallback<States>): void;
|
|
173
180
|
possibleStates: States[];
|
|
174
181
|
onHappens(callback: (args: EventArgs<EventPayloadMapping, keyof EventPayloadMapping | string>, context: Context) => void): void;
|
|
182
|
+
reset(): void;
|
|
183
|
+
start(): void;
|
|
184
|
+
wrapup(): void;
|
|
175
185
|
}
|
|
176
186
|
/**
|
|
177
187
|
* @description This is the type for the callback that is called when the state changes.
|
|
@@ -197,10 +207,9 @@ export type StateChangeCallback<States extends string = 'IDLE'> = (currentState:
|
|
|
197
207
|
* @category Types
|
|
198
208
|
*/
|
|
199
209
|
export interface State<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE', EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> {
|
|
200
|
-
uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, from: States): void;
|
|
201
|
-
beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, to: States): void;
|
|
210
|
+
uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, from: States | "INITIAL"): void;
|
|
211
|
+
beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, to: States | "TERMINAL"): void;
|
|
202
212
|
handles<K extends (keyof EventPayloadMapping | string)>(args: EventArgs<EventPayloadMapping, K>, context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;
|
|
203
|
-
eventReactions: EventReactions<EventPayloadMapping, Context, States, EventOutputMapping>;
|
|
204
213
|
guards: Guard<Context>;
|
|
205
214
|
eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;
|
|
206
215
|
delay: Delay<Context, EventPayloadMapping, States, EventOutputMapping> | undefined;
|
|
@@ -364,20 +373,24 @@ export type EventGuards<EventPayloadMapping, States extends string, Context exte
|
|
|
364
373
|
* @see {@link StateMachine} for the interface definition
|
|
365
374
|
*/
|
|
366
375
|
export declare class TemplateStateMachine<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE', EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> implements StateMachine<EventPayloadMapping, Context, States, EventOutputMapping> {
|
|
367
|
-
protected _currentState: States;
|
|
376
|
+
protected _currentState: States | "INITIAL" | "TERMINAL";
|
|
368
377
|
protected _states: Record<States, State<EventPayloadMapping, Context, States, EventOutputMapping>>;
|
|
369
378
|
protected _context: Context;
|
|
370
379
|
protected _statesArray: States[];
|
|
371
380
|
protected _stateChangeCallbacks: StateChangeCallback<States>[];
|
|
372
381
|
protected _happensCallbacks: ((args: EventArgs<EventPayloadMapping, keyof EventPayloadMapping | string>, context: Context) => void)[];
|
|
373
382
|
protected _timeouts: ReturnType<typeof setTimeout> | undefined;
|
|
374
|
-
|
|
375
|
-
|
|
383
|
+
protected _initialState: States;
|
|
384
|
+
constructor(states: Record<States, State<EventPayloadMapping, Context, States, EventOutputMapping>>, initialState: States, context: Context, autoStart?: boolean);
|
|
385
|
+
reset(): void;
|
|
386
|
+
start(): void;
|
|
387
|
+
wrapup(): void;
|
|
388
|
+
switchTo(state: States | "INITIAL" | "TERMINAL"): void;
|
|
376
389
|
happens<K extends keyof EventPayloadMapping>(...args: EventArgs<EventPayloadMapping, K>): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;
|
|
377
390
|
happens<K extends string>(...args: EventArgs<EventPayloadMapping, K>): EventResult<States, unknown>;
|
|
378
391
|
onStateChange(callback: StateChangeCallback<States>): void;
|
|
379
392
|
onHappens(callback: (args: EventArgs<EventPayloadMapping, keyof EventPayloadMapping | string>, context: Context) => void): void;
|
|
380
|
-
get currentState(): States;
|
|
393
|
+
get currentState(): States | "INITIAL" | "TERMINAL";
|
|
381
394
|
setContext(context: Context): void;
|
|
382
395
|
get possibleStates(): States[];
|
|
383
396
|
get states(): Record<States, State<EventPayloadMapping, Context, States, EventOutputMapping>>;
|
|
@@ -467,15 +480,16 @@ export declare class TemplateStateMachine<EventPayloadMapping, Context extends B
|
|
|
467
480
|
* @see {@link EventReactions} for defining event handlers
|
|
468
481
|
*/
|
|
469
482
|
export declare abstract class TemplateState<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE', EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> implements State<EventPayloadMapping, Context, States, EventOutputMapping> {
|
|
470
|
-
|
|
483
|
+
protected _eventReactions: EventReactions<EventPayloadMapping, Context, States, EventOutputMapping>;
|
|
471
484
|
protected _guards: Guard<Context>;
|
|
472
485
|
protected _eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;
|
|
473
486
|
protected _delay: Delay<Context, EventPayloadMapping, States, EventOutputMapping> | undefined;
|
|
487
|
+
get handlingEvents(): (keyof EventPayloadMapping)[];
|
|
474
488
|
get guards(): Guard<Context>;
|
|
475
489
|
get eventGuards(): Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;
|
|
476
490
|
get delay(): Delay<Context, EventPayloadMapping, States, EventOutputMapping> | undefined;
|
|
477
|
-
uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, from: States): void;
|
|
478
|
-
beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, to: States): void;
|
|
491
|
+
uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, from: States | "INITIAL"): void;
|
|
492
|
+
beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>, to: States | "TERMINAL"): void;
|
|
479
493
|
handles<K extends (keyof EventPayloadMapping | string)>(args: EventArgs<EventPayloadMapping, K>, context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States, EventOutputMapping>): EventResult<States, K extends keyof EventOutputMapping ? EventOutputMapping[K] : void>;
|
|
480
494
|
}
|
|
481
495
|
/**
|
package/package.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example demonstrating runtime state machine creation from schema.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This example shows how to use the schema factory to create state machines
|
|
6
|
+
* dynamically at runtime, which is useful for GUI builders or configuration-driven
|
|
7
|
+
* state machines.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-based state machine factory for runtime creation.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This module provides utilities for creating state machines from JSON-like schemas,
|
|
6
|
+
* enabling dynamic state machine creation at runtime. This is useful for GUI builders
|
|
7
|
+
* or configuration-driven state machines.
|
|
8
|
+
*
|
|
9
|
+
* @category Runtime Factory
|
|
10
|
+
*/
|
|
11
|
+
import { BaseContext, StateMachine, DefaultOutputMapping } from "./interface";
|
|
12
|
+
/**
|
|
13
|
+
* Payload type definition for an event in the schema.
|
|
14
|
+
* Can be an empty object (no payload) or an object with typed fields.
|
|
15
|
+
*/
|
|
16
|
+
export type EventPayloadSchema = Record<string, string> | {};
|
|
17
|
+
/**
|
|
18
|
+
* Action function that can be executed when an event is handled.
|
|
19
|
+
* Receives context, event payload, and the state machine instance.
|
|
20
|
+
* Can return a value that will be included in the event result.
|
|
21
|
+
*
|
|
22
|
+
* @typeParam Context - The context type
|
|
23
|
+
* @typeParam EventPayloadMapping - Mapping of event names to their payload types
|
|
24
|
+
* @typeParam EventName - The specific event name this action handles
|
|
25
|
+
* @typeParam EventOutputMapping - Optional mapping of events to their output types
|
|
26
|
+
*/
|
|
27
|
+
export type ActionFunction<Context extends BaseContext = BaseContext, EventPayloadMapping = any, EventName extends keyof EventPayloadMapping = keyof EventPayloadMapping, EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> = (context: Context, payload: EventPayloadMapping[EventName], stateMachine: StateMachine<EventPayloadMapping, Context, any, EventOutputMapping>) => EventName extends keyof EventOutputMapping ? (EventOutputMapping[EventName] | void) : void | unknown;
|
|
28
|
+
/**
|
|
29
|
+
* Guard function that evaluates whether a transition should occur.
|
|
30
|
+
* Returns true if the guard condition is met.
|
|
31
|
+
*/
|
|
32
|
+
export type GuardFunction<Context extends BaseContext = BaseContext> = (context: Context) => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Definition of a single state transition.
|
|
35
|
+
*
|
|
36
|
+
* @typeParam Context - The context type
|
|
37
|
+
* @typeParam EventPayloadMapping - Mapping of event names to their payload types
|
|
38
|
+
* @typeParam EventName - The specific event name for this transition
|
|
39
|
+
* @typeParam StateNames - Union type of all valid state names
|
|
40
|
+
* @typeParam EventOutputMapping - Optional mapping of events to their output types
|
|
41
|
+
*/
|
|
42
|
+
export interface TransitionDefinition<Context extends BaseContext = BaseContext, EventPayloadMapping = any, EventName extends keyof EventPayloadMapping = keyof EventPayloadMapping, StateNames extends string = string, EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> {
|
|
43
|
+
/** The event that triggers this transition */
|
|
44
|
+
event: EventName;
|
|
45
|
+
/** The target state after this transition */
|
|
46
|
+
targetState: StateNames;
|
|
47
|
+
/** Optional action to execute when this transition occurs. Can return a value that will be included in the event result. */
|
|
48
|
+
action?: ActionFunction<Context, EventPayloadMapping, EventName, EventOutputMapping>;
|
|
49
|
+
/**
|
|
50
|
+
* Optional guard conditions (evaluated in order, first true guard wins).
|
|
51
|
+
* Guards can be either:
|
|
52
|
+
* - A guard function defined inline
|
|
53
|
+
* - A string reference to a guard defined in the state's `guards` section
|
|
54
|
+
*/
|
|
55
|
+
guards?: Array<{
|
|
56
|
+
/** Guard function to evaluate, or name of a guard defined in the state's guards section */
|
|
57
|
+
guard: GuardFunction<Context> | string;
|
|
58
|
+
/** Target state if this guard evaluates to true */
|
|
59
|
+
targetState: StateNames;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Union type of all possible transition definitions for a given event payload mapping.
|
|
64
|
+
* This ensures each transition's action payload is typed based on its specific event.
|
|
65
|
+
*
|
|
66
|
+
* @typeParam Context - The context type
|
|
67
|
+
* @typeParam EventPayloadMapping - Mapping of event names to their payload types
|
|
68
|
+
* @typeParam StateNames - Union type of all valid state names
|
|
69
|
+
* @typeParam EventOutputMapping - Optional mapping of events to their output types
|
|
70
|
+
*/
|
|
71
|
+
export type TransitionDefinitionUnion<Context extends BaseContext = BaseContext, EventPayloadMapping = any, StateNames extends string = string, EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> = {
|
|
72
|
+
[K in keyof EventPayloadMapping]: TransitionDefinition<Context, EventPayloadMapping, K, StateNames, EventOutputMapping>;
|
|
73
|
+
}[keyof EventPayloadMapping];
|
|
74
|
+
/**
|
|
75
|
+
* Definition of a single state in the state machine.
|
|
76
|
+
*
|
|
77
|
+
* @typeParam Context - The context type
|
|
78
|
+
* @typeParam EventPayloadMapping - Mapping of event names to their payload types
|
|
79
|
+
* @typeParam StateNames - Union type of all valid state names
|
|
80
|
+
* @typeParam EventOutputMapping - Optional mapping of events to their output types
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* {
|
|
85
|
+
* name: "PAYING",
|
|
86
|
+
* guards: {
|
|
87
|
+
* hasEnoughBalance: (context) => context.balance >= context.itemPrice,
|
|
88
|
+
* hasInsufficientBalance: (context) => context.balance < context.itemPrice,
|
|
89
|
+
* },
|
|
90
|
+
* transitions: [
|
|
91
|
+
* {
|
|
92
|
+
* event: "pay",
|
|
93
|
+
* targetState: "SELECTING",
|
|
94
|
+
* guards: [
|
|
95
|
+
* { guard: "hasEnoughBalance", targetState: "CONFIRMED" },
|
|
96
|
+
* { guard: "hasInsufficientBalance", targetState: "PAYING" },
|
|
97
|
+
* ],
|
|
98
|
+
* },
|
|
99
|
+
* ],
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export interface StateDefinition<Context extends BaseContext = BaseContext, EventPayloadMapping = any, StateNames extends string = string, EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> {
|
|
104
|
+
/** Name of this state */
|
|
105
|
+
name: StateNames;
|
|
106
|
+
/** Transitions available from this state */
|
|
107
|
+
transitions: TransitionDefinitionUnion<Context, EventPayloadMapping, StateNames, EventOutputMapping>[];
|
|
108
|
+
/**
|
|
109
|
+
* Optional mapping of guard names to guard functions.
|
|
110
|
+
* Guards defined here can be reused across multiple transitions within this state
|
|
111
|
+
* by referencing them by name in the transition's guards array.
|
|
112
|
+
*
|
|
113
|
+
* The guard functions receive the context parameter typed with the Context type parameter,
|
|
114
|
+
* not BaseContext, so you get full type safety for your context properties.
|
|
115
|
+
*/
|
|
116
|
+
guards?: Record<string, GuardFunction<Context>>;
|
|
117
|
+
/** Optional callback when entering this state */
|
|
118
|
+
onEnter?: (context: Context, fromState: StateNames) => void;
|
|
119
|
+
/** Optional callback when exiting this state */
|
|
120
|
+
onExit?: (context: Context, toState: StateNames) => void;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Helper type to extract state names from a states array.
|
|
124
|
+
* If the array is a readonly tuple of string literals, it extracts the union type.
|
|
125
|
+
* Otherwise, it falls back to string.
|
|
126
|
+
*
|
|
127
|
+
* @typeParam StatesArray - The states array type
|
|
128
|
+
*/
|
|
129
|
+
export type ExtractStateNames<StatesArray> = StatesArray extends readonly (infer S)[] ? S extends string ? S : string : StatesArray extends (infer S)[] ? S extends string ? S : string : string;
|
|
130
|
+
/**
|
|
131
|
+
* Helper function to create a typed state machine schema with inferred state names.
|
|
132
|
+
* Use this when you have a const states array and want TypeScript to infer the state names.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const states = ["IDLE", "RUNNING", "PAUSED"] as const;
|
|
137
|
+
* const schema = createStateMachineSchemaWithInferredStates<TimerContext, TimerEvents>({
|
|
138
|
+
* states,
|
|
139
|
+
* events: { start: {}, stop: {} },
|
|
140
|
+
* initialState: "IDLE",
|
|
141
|
+
* stateDefinitions: [
|
|
142
|
+
* {
|
|
143
|
+
* name: "IDLE", // TypeScript knows this must be one of the states
|
|
144
|
+
* transitions: [
|
|
145
|
+
* {
|
|
146
|
+
* event: "start",
|
|
147
|
+
* targetState: "RUNNING", // TypeScript knows this must be one of the states
|
|
148
|
+
* }
|
|
149
|
+
* ]
|
|
150
|
+
* }
|
|
151
|
+
* ]
|
|
152
|
+
* });
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export declare function createStateMachineSchemaWithInferredStates<Context extends BaseContext, EventPayloadMapping, States extends readonly string[], EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>>(schema: Omit<StateMachineSchema<Context, EventPayloadMapping, ExtractStateNames<States>, EventOutputMapping>, 'states'> & {
|
|
156
|
+
states: States;
|
|
157
|
+
}): StateMachineSchema<Context, EventPayloadMapping, ExtractStateNames<States>, EventOutputMapping>;
|
|
158
|
+
/**
|
|
159
|
+
* Complete schema definition for a state machine.
|
|
160
|
+
*
|
|
161
|
+
* @typeParam Context - The context type
|
|
162
|
+
* @typeParam EventPayloadMapping - Mapping of event names to their payload types
|
|
163
|
+
* @typeParam StateNames - Union type of all valid state names (inferred from states array)
|
|
164
|
+
* @typeParam EventOutputMapping - Optional mapping of events to their output types
|
|
165
|
+
*/
|
|
166
|
+
export interface StateMachineSchema<Context extends BaseContext = BaseContext, EventPayloadMapping = any, StateNames extends string = string, EventOutputMapping extends Partial<Record<keyof EventPayloadMapping, unknown>> = DefaultOutputMapping<EventPayloadMapping>> {
|
|
167
|
+
/** Array of all possible state names */
|
|
168
|
+
states: readonly StateNames[] | StateNames[];
|
|
169
|
+
/** Mapping of event names to their payload types */
|
|
170
|
+
events: EventPayloadMapping;
|
|
171
|
+
/** Array of state definitions */
|
|
172
|
+
stateDefinitions: StateDefinition<Context, EventPayloadMapping, StateNames, EventOutputMapping>[];
|
|
173
|
+
/** Initial state name */
|
|
174
|
+
initialState: StateNames;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Creates a state machine from a schema definition.
|
|
178
|
+
*
|
|
179
|
+
* @remarks
|
|
180
|
+
* This factory function takes a schema and creates a fully functional state machine
|
|
181
|
+
* at runtime. The resulting state machine uses type erasure (`any` types) but maintains
|
|
182
|
+
* full runtime functionality.
|
|
183
|
+
*
|
|
184
|
+
* Actions can return values that will be included in the event result. To enable
|
|
185
|
+
* typed outputs, provide an EventOutputMapping type parameter that maps event names
|
|
186
|
+
* to their output types.
|
|
187
|
+
*
|
|
188
|
+
* @typeParam Context - The context type
|
|
189
|
+
* @typeParam EventPayloadMapping - Mapping of event names to their payload types
|
|
190
|
+
* @typeParam EventOutputMapping - Optional mapping of events to their output types
|
|
191
|
+
*
|
|
192
|
+
* @param schema - The schema definition for the state machine
|
|
193
|
+
* @param context - The context instance to use for the state machine
|
|
194
|
+
* @returns A fully configured state machine instance
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* Basic state machine without outputs
|
|
198
|
+
* ```typescript
|
|
199
|
+
* const schema: StateMachineSchema = {
|
|
200
|
+
* states: ["IDLE", "RUNNING", "PAUSED"],
|
|
201
|
+
* events: {
|
|
202
|
+
* start: {},
|
|
203
|
+
* stop: {},
|
|
204
|
+
* pause: {},
|
|
205
|
+
* resume: {}
|
|
206
|
+
* },
|
|
207
|
+
* initialState: "IDLE",
|
|
208
|
+
* stateDefinitions: [
|
|
209
|
+
* {
|
|
210
|
+
* name: "IDLE",
|
|
211
|
+
* transitions: [
|
|
212
|
+
* {
|
|
213
|
+
* event: "start",
|
|
214
|
+
* targetState: "RUNNING",
|
|
215
|
+
* action: (context) => console.log("Starting...")
|
|
216
|
+
* }
|
|
217
|
+
* ]
|
|
218
|
+
* }
|
|
219
|
+
* ]
|
|
220
|
+
* };
|
|
221
|
+
*
|
|
222
|
+
* const machine = createStateMachineFromSchema(schema, context);
|
|
223
|
+
* machine.happens("start");
|
|
224
|
+
* ```
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* State machine with typed outputs
|
|
228
|
+
* ```typescript
|
|
229
|
+
* type Events = { calculate: { value: number }; getResult: {} };
|
|
230
|
+
* type Outputs = { calculate: number; getResult: number };
|
|
231
|
+
*
|
|
232
|
+
* const schema: StateMachineSchema<MyContext, Events, Outputs> = {
|
|
233
|
+
* states: ["READY"],
|
|
234
|
+
* events: { calculate: { value: 0 }, getResult: {} },
|
|
235
|
+
* initialState: "READY",
|
|
236
|
+
* stateDefinitions: [
|
|
237
|
+
* {
|
|
238
|
+
* name: "READY",
|
|
239
|
+
* transitions: [
|
|
240
|
+
* {
|
|
241
|
+
* event: "calculate",
|
|
242
|
+
* targetState: "READY",
|
|
243
|
+
* action: (context, payload) => {
|
|
244
|
+
* context.total += payload.value;
|
|
245
|
+
* return context.total; // Return value included in result
|
|
246
|
+
* }
|
|
247
|
+
* },
|
|
248
|
+
* {
|
|
249
|
+
* event: "getResult",
|
|
250
|
+
* targetState: "READY",
|
|
251
|
+
* action: (context) => context.total // Return current total
|
|
252
|
+
* }
|
|
253
|
+
* ]
|
|
254
|
+
* }
|
|
255
|
+
* ]
|
|
256
|
+
* };
|
|
257
|
+
*
|
|
258
|
+
* const machine = createStateMachineFromSchema<MyContext, Events, Outputs>(schema, context);
|
|
259
|
+
* const result = machine.happens("calculate", { value: 10 });
|
|
260
|
+
* if (result.handled && "output" in result) {
|
|
261
|
+
* console.log(result.output); // Typed as number
|
|
262
|
+
* }
|
|
263
|
+
* ```
|
|
264
|
+
*
|
|
265
|
+
* @category Runtime Factory
|
|
266
|
+
*/
|
|
267
|
+
export declare function createStateMachineFromSchema<Schema extends StateMachineSchema<any, any, any, any>>(schema: Schema, context: Schema extends StateMachineSchema<infer C, any, any, any> ? C : BaseContext): Schema extends StateMachineSchema<infer C, infer EPM, any, infer EOM> ? StateMachine<EPM, C, any, EOM> : StateMachine<any, BaseContext, any, any>;
|