@doeixd/machine 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/dist/cjs/development/index.js +275 -5
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +5 -5
- package/dist/esm/development/index.js +275 -5
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +5 -5
- package/dist/types/extract.d.ts +40 -4
- package/dist/types/extract.d.ts.map +1 -1
- package/dist/types/generators.d.ts +40 -9
- package/dist/types/generators.d.ts.map +1 -1
- package/dist/types/higher-order.d.ts +221 -0
- package/dist/types/higher-order.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/utils.d.ts +208 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/extract.ts +180 -8
- package/src/generators.ts +25 -25
- package/src/higher-order.ts +364 -0
- package/src/index.ts +18 -1
- package/src/utils.ts +171 -5
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A collection of high-level, type-safe utility functions for @doeixd/machine.
|
|
3
|
+
* @description These helpers provide ergonomic improvements for common patterns like
|
|
4
|
+
* state checking, event creation, debugging, and composing transitions.
|
|
5
|
+
*/
|
|
6
|
+
import { Machine, AsyncMachine, MaybePromise, Context, Event, Transitions, TransitionArgs } from './index';
|
|
7
|
+
/**
|
|
8
|
+
* A type representing a Class Constructor, used for type guards.
|
|
9
|
+
*/
|
|
10
|
+
type ClassConstructor = new (...args: any[]) => any;
|
|
11
|
+
/**
|
|
12
|
+
* A type-safe way to check if a machine is in a specific state, acting as a Type Guard.
|
|
13
|
+
* This is the preferred way to do state checking when using class-based machines.
|
|
14
|
+
*
|
|
15
|
+
* @template T - The class constructor type to check against.
|
|
16
|
+
* @param machine - The machine instance to check.
|
|
17
|
+
* @param machineClass - The class constructor representing the state.
|
|
18
|
+
* @returns {boolean} `true` if the machine is an instance of the class, narrowing its type.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* declare const machine: LoggedInMachine | LoggedOutMachine;
|
|
22
|
+
*
|
|
23
|
+
* if (isState(machine, LoggedInMachine)) {
|
|
24
|
+
* // `machine` is now correctly typed as LoggedInMachine
|
|
25
|
+
* machine.logout();
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
export declare function isState<T extends ClassConstructor>(machine: any, machineClass: T): machine is InstanceType<T>;
|
|
29
|
+
/**
|
|
30
|
+
* A type-safe factory function for creating event objects for `runMachine`.
|
|
31
|
+
* This provides full autocompletion and type checking for event names and their arguments.
|
|
32
|
+
*
|
|
33
|
+
* @template M - The machine type the event belongs to.
|
|
34
|
+
* @template K - The specific event name (transition method name).
|
|
35
|
+
* @param type - The name of the event (e.g., "increment").
|
|
36
|
+
* @param args - The arguments for that event, correctly typed.
|
|
37
|
+
* @returns A type-safe event object ready to be passed to `dispatch`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Given: type MyMachine = Machine<{...}> & { add: (n: number) => any }
|
|
41
|
+
* const event = createEvent<MyMachine, 'add'>('add', 5);
|
|
42
|
+
* // `event` is correctly typed as { type: "add"; args: [number] }
|
|
43
|
+
*
|
|
44
|
+
* await runner.dispatch(event);
|
|
45
|
+
*/
|
|
46
|
+
export declare function createEvent<M extends Machine<any>, K extends keyof Transitions<M> & string>(type: K, ...args: TransitionArgs<M, K>): Event<M>;
|
|
47
|
+
/**
|
|
48
|
+
* Creates a new machine instance by shallowly merging a partial context into the
|
|
49
|
+
* current context, preserving all original transitions.
|
|
50
|
+
*
|
|
51
|
+
* @template M - The machine type.
|
|
52
|
+
* @param machine - The original machine instance.
|
|
53
|
+
* @param partialContext - An object with a subset of context properties to update.
|
|
54
|
+
* @returns A new machine instance of the same type with the merged context.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const user = new User({ name: 'Alex', age: 30, status: 'active' });
|
|
58
|
+
* const updatedUser = mergeContext(user, { status: 'inactive' });
|
|
59
|
+
* // updatedUser.context is { name: 'Alex', age: 30, status: 'inactive' }
|
|
60
|
+
*/
|
|
61
|
+
export declare function mergeContext<M extends Machine<any>>(machine: M, partialContext: Partial<Context<M>>): M;
|
|
62
|
+
/**
|
|
63
|
+
* Sequentially applies a series of transitions to a machine.
|
|
64
|
+
* This function correctly handles both synchronous and asynchronous transitions,
|
|
65
|
+
* always returning a Promise with the final machine state.
|
|
66
|
+
*
|
|
67
|
+
* @template M - The machine type, must be compatible with AsyncMachine.
|
|
68
|
+
* @param initialMachine - The starting machine state.
|
|
69
|
+
* @param transitions - An array of functions, each taking a machine and returning the next.
|
|
70
|
+
* @returns A `Promise` that resolves to the final machine state after all transitions complete.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const finalState = await pipeTransitions(
|
|
74
|
+
* new Counter({ count: 0 }),
|
|
75
|
+
* (m) => m.increment(), // sync
|
|
76
|
+
* (m) => m.addAsync(5), // async
|
|
77
|
+
* (m) => m.increment() // sync
|
|
78
|
+
* );
|
|
79
|
+
* // finalState.context.count will be 6
|
|
80
|
+
*/
|
|
81
|
+
export declare function pipeTransitions<M extends AsyncMachine<any>>(initialMachine: M, ...transitions: ((m: M) => MaybePromise<M>)[]): Promise<M>;
|
|
82
|
+
/**
|
|
83
|
+
* A "tap" utility for logging a machine's context without interrupting a chain of operations.
|
|
84
|
+
* It prints the context to the console and returns the machine instance unchanged.
|
|
85
|
+
*
|
|
86
|
+
* @template M - The machine type.
|
|
87
|
+
* @param machine - The machine instance to log.
|
|
88
|
+
* @param label - An optional label to print before the context object.
|
|
89
|
+
* @returns The original, unmodified machine instance.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* import { logState as tap } from './utils';
|
|
93
|
+
*
|
|
94
|
+
* await pipeTransitions(
|
|
95
|
+
* new Counter({ count: 0 }),
|
|
96
|
+
* tap, // Logs: { count: 0 }
|
|
97
|
+
* (m) => m.increment(),
|
|
98
|
+
* (m) => tap(m, 'After increment:') // Logs: After increment: { count: 1 }
|
|
99
|
+
* );
|
|
100
|
+
*/
|
|
101
|
+
export declare function logState<M extends Machine<any>>(machine: M, label?: string): M;
|
|
102
|
+
/**
|
|
103
|
+
* Calls a transition function with an explicit `this` context.
|
|
104
|
+
* Useful for invoking transition methods with proper context binding.
|
|
105
|
+
*
|
|
106
|
+
* @template C - The context type that the function expects as `this`.
|
|
107
|
+
* @template F - The function type with a `this` parameter.
|
|
108
|
+
* @template A - The argument types for the function.
|
|
109
|
+
* @param fn - The transition function to call.
|
|
110
|
+
* @param context - The context object to bind as `this`.
|
|
111
|
+
* @param args - Arguments to pass to the function.
|
|
112
|
+
* @returns The result of calling the function with the given context and arguments.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* type MyContext = { count: number };
|
|
116
|
+
* const increment = function(this: MyContext) { return this.count + 1; };
|
|
117
|
+
* const result = call(increment, { count: 5 }); // Returns 6
|
|
118
|
+
*
|
|
119
|
+
* // Particularly useful with machine transitions:
|
|
120
|
+
* import { call } from '@doeixd/machine/utils';
|
|
121
|
+
* const nextMachine = yield* step(call(m.increment, m.context));
|
|
122
|
+
*/
|
|
123
|
+
export declare function call<C, F extends (this: C, ...args: any[]) => any>(fn: F, context: C, ...args: Parameters<F> extends [any, ...infer Rest] ? Rest : never): ReturnType<F>;
|
|
124
|
+
/**
|
|
125
|
+
* Binds all transition methods of a machine to its context automatically.
|
|
126
|
+
* Returns a Proxy that intercepts method calls and binds them to `machine.context`.
|
|
127
|
+
* This eliminates the need to use `.call(m.context, ...)` for every transition.
|
|
128
|
+
*
|
|
129
|
+
* Automatically recursively wraps returned machines, enabling seamless chaining
|
|
130
|
+
* in generator-based flows.
|
|
131
|
+
*
|
|
132
|
+
* @template M - The machine type with a `context` property and transition methods.
|
|
133
|
+
* @param machine - The machine instance to wrap.
|
|
134
|
+
* @returns A Proxy of the machine where all callable properties (transitions) are automatically bound to the machine's context.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* type CounterContext = { count: number };
|
|
138
|
+
* const counter = bindTransitions(createMachine({ count: 0 }, {
|
|
139
|
+
* increment(this: CounterContext) { return createCounter(this.count + 1); }
|
|
140
|
+
* }));
|
|
141
|
+
*
|
|
142
|
+
* // Now you can call transitions directly without .call():
|
|
143
|
+
* const next = counter.increment(); // Works! This is automatically bound.
|
|
144
|
+
*
|
|
145
|
+
* // Particularly useful with generators:
|
|
146
|
+
* const result = run(function* (m) {
|
|
147
|
+
* m = yield* step(m.increment()); // Clean syntax
|
|
148
|
+
* m = yield* step(m.add(5)); // No .call() needed
|
|
149
|
+
* return m;
|
|
150
|
+
* }, bindTransitions(counter));
|
|
151
|
+
*
|
|
152
|
+
* @remarks
|
|
153
|
+
* The Proxy preserves all original properties and methods. Non-callable properties
|
|
154
|
+
* are accessed directly from the machine. Callable properties are wrapped to bind
|
|
155
|
+
* them to `machine.context` before invocation. Returned machines are automatically
|
|
156
|
+
* re-wrapped to maintain binding across transition chains.
|
|
157
|
+
*/
|
|
158
|
+
export declare function bindTransitions<M extends {
|
|
159
|
+
context: any;
|
|
160
|
+
}>(machine: M): M;
|
|
161
|
+
/**
|
|
162
|
+
* A strongly-typed wrapper class for binding transitions to machine context.
|
|
163
|
+
* Unlike the Proxy-based `bindTransitions`, this class preserves full type safety
|
|
164
|
+
* and provides better IDE support through explicit property forwarding.
|
|
165
|
+
*
|
|
166
|
+
* @template M - The machine type with a `context` property and transition methods.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* type CounterContext = { count: number };
|
|
170
|
+
* const counter = createMachine({ count: 0 }, {
|
|
171
|
+
* increment(this: CounterContext) { return createCounter(this.count + 1); }
|
|
172
|
+
* });
|
|
173
|
+
*
|
|
174
|
+
* const bound = new BoundMachine(counter);
|
|
175
|
+
*
|
|
176
|
+
* // All transitions are automatically bound to context
|
|
177
|
+
* const result = run(function* (m) {
|
|
178
|
+
* m = yield* step(m.increment());
|
|
179
|
+
* m = yield* step(m.add(5));
|
|
180
|
+
* return m.context.count;
|
|
181
|
+
* }, bound);
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* Advantages over Proxy-based `bindTransitions`:
|
|
185
|
+
* - Full type safety with TypeScript's type system
|
|
186
|
+
* - Returned machines are automatically re-wrapped
|
|
187
|
+
* - Better IDE autocompletion and hover information
|
|
188
|
+
* - No type casting needed
|
|
189
|
+
*
|
|
190
|
+
* Disadvantages:
|
|
191
|
+
* - Requires explicit instance creation: `new BoundMachine(m)` vs `bindTransitions(m)`
|
|
192
|
+
* - Not a transparent drop-in replacement for the original machine
|
|
193
|
+
*/
|
|
194
|
+
export declare class BoundMachine<M extends {
|
|
195
|
+
context: any;
|
|
196
|
+
}> {
|
|
197
|
+
private readonly wrappedMachine;
|
|
198
|
+
[key: string | symbol]: any;
|
|
199
|
+
constructor(machine: M);
|
|
200
|
+
/**
|
|
201
|
+
* Access the underlying machine's context directly.
|
|
202
|
+
*/
|
|
203
|
+
get context(): M extends {
|
|
204
|
+
context: infer C;
|
|
205
|
+
} ? C : never;
|
|
206
|
+
}
|
|
207
|
+
export {};
|
|
208
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,OAAO,EACP,KAAK,EACL,WAAW,EACX,cAAc,EAEf,MAAM,SAAS,CAAC;AAMjB;;GAEG;AACH,KAAK,gBAAgB,GAAG,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC;AAEpD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,gBAAgB,EAChD,OAAO,EAAE,GAAG,EACZ,YAAY,EAAE,CAAC,GACd,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,CAE5B;AAOD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CACzB,CAAC,SAAS,OAAO,CAAC,GAAG,CAAC,EACtB,CAAC,SAAS,MAAM,WAAW,CAAC,CAAC,CAAC,GAAG,MAAM,EACvC,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAElD;AAOD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,OAAO,CAAC,GAAG,CAAC,EACjD,OAAO,EAAE,CAAC,EACV,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAClC,CAAC,CAEH;AAOD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,eAAe,CAAC,CAAC,SAAS,YAAY,CAAC,GAAG,CAAC,EAC/D,cAAc,EAAE,CAAC,EACjB,GAAG,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,GAC5C,OAAO,CAAC,CAAC,CAAC,CAMZ;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAO9E;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAChE,EAAE,EAAE,CAAC,EACL,OAAO,EAAE,CAAC,EACV,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,GACjE,UAAU,CAAC,CAAC,CAAC,CAEf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAqBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,YAAY,CAAC,CAAC,SAAS;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE;IAClD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAI;IACnC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,GAAG,CAAC;gBAEhB,OAAO,EAAE,CAAC;IA+BtB;;OAEG;IACH,IAAI,OAAO,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,CAAC,GAAG,KAAK,CAExD;CACF"}
|
package/package.json
CHANGED
package/src/extract.ts
CHANGED
|
@@ -20,22 +20,57 @@ import { Project, Type, Node } from 'ts-morph';
|
|
|
20
20
|
// SECTION: CONFIGURATION TYPES
|
|
21
21
|
// =============================================================================
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for a parallel region
|
|
25
|
+
*/
|
|
26
|
+
export interface ParallelRegionConfig {
|
|
27
|
+
/** A unique name for this region (e.g., 'fontStyle') */
|
|
28
|
+
name: string;
|
|
29
|
+
/** The initial state class for this region */
|
|
30
|
+
initialState: string;
|
|
31
|
+
/** All reachable state classes within this region */
|
|
32
|
+
classes: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Configuration for child states in a hierarchical machine
|
|
37
|
+
*/
|
|
38
|
+
export interface ChildStatesConfig {
|
|
39
|
+
/** The property in the parent's context that holds the child machine */
|
|
40
|
+
contextProperty: string;
|
|
41
|
+
/** An array of all possible child state class names */
|
|
42
|
+
classes: string[];
|
|
43
|
+
/** The initial child state */
|
|
44
|
+
initialState: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
/**
|
|
24
48
|
* Configuration for a single machine to extract
|
|
25
49
|
*/
|
|
26
50
|
export interface MachineConfig {
|
|
27
51
|
/** Path to the source file containing the machine */
|
|
28
52
|
input: string;
|
|
29
|
-
/** Array of class names that represent states */
|
|
30
|
-
classes: string[];
|
|
31
53
|
/** Output file path (optional, defaults to stdout) */
|
|
32
54
|
output?: string;
|
|
33
55
|
/** Top-level ID for the statechart */
|
|
34
56
|
id: string;
|
|
35
|
-
/** Name of the class that represents the initial state */
|
|
36
|
-
initialState: string;
|
|
37
57
|
/** Optional description of the machine */
|
|
38
58
|
description?: string;
|
|
59
|
+
|
|
60
|
+
// EITHER `initialState` and `classes` for an FSM...
|
|
61
|
+
/** Array of class names that represent states (for simple FSM) */
|
|
62
|
+
classes?: string[];
|
|
63
|
+
/** Name of the class that represents the initial state (for simple FSM) */
|
|
64
|
+
initialState?: string;
|
|
65
|
+
|
|
66
|
+
// OR `parallel` for a parallel machine.
|
|
67
|
+
/** Configuration for parallel regions (mutually exclusive with initialState/classes) */
|
|
68
|
+
parallel?: {
|
|
69
|
+
regions: ParallelRegionConfig[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Configuration for hierarchical/nested states */
|
|
73
|
+
children?: ChildStatesConfig;
|
|
39
74
|
}
|
|
40
75
|
|
|
41
76
|
/**
|
|
@@ -63,11 +98,16 @@ export interface ExtractionConfig {
|
|
|
63
98
|
* plain JSON-compatible value. It's smart enough to resolve class constructor
|
|
64
99
|
* types into their string names.
|
|
65
100
|
*
|
|
101
|
+
* Note: This function is kept for future extensibility but is not currently used
|
|
102
|
+
* as the AST-based extraction approach (via extractFromCallExpression) is preferred.
|
|
103
|
+
*
|
|
66
104
|
* @param type - The `ts-morph` Type object to serialize.
|
|
67
105
|
* @param verbose - Enable debug logging
|
|
68
106
|
* @returns A JSON-compatible value (string, number, object, array).
|
|
107
|
+
* @internal
|
|
69
108
|
*/
|
|
70
|
-
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
110
|
+
function _typeToJson(type: Type, verbose = false): any {
|
|
71
111
|
// --- Terminal Types ---
|
|
72
112
|
const symbol = type.getSymbol();
|
|
73
113
|
if (symbol && symbol.getDeclarations().some(Node.isClassDeclaration)) {
|
|
@@ -83,7 +123,7 @@ function typeToJson(type: Type, verbose = false): any {
|
|
|
83
123
|
// --- Recursive Types ---
|
|
84
124
|
if (type.isArray()) {
|
|
85
125
|
const elementType = type.getArrayElementTypeOrThrow();
|
|
86
|
-
return [
|
|
126
|
+
return [_typeToJson(elementType, verbose)];
|
|
87
127
|
}
|
|
88
128
|
|
|
89
129
|
// --- Object Types ---
|
|
@@ -102,7 +142,7 @@ function typeToJson(type: Type, verbose = false): any {
|
|
|
102
142
|
if (!declaration) continue;
|
|
103
143
|
|
|
104
144
|
try {
|
|
105
|
-
obj[propName] =
|
|
145
|
+
obj[propName] = _typeToJson(declaration.getType(), verbose);
|
|
106
146
|
} catch (e) {
|
|
107
147
|
if (verbose) console.error(` Warning: Failed to serialize property ${propName}:`, e);
|
|
108
148
|
obj[propName] = 'unknown';
|
|
@@ -438,6 +478,41 @@ function analyzeStateNode(classSymbol: any, verbose = false): object {
|
|
|
438
478
|
// SECTION: MAIN ORCHESTRATOR
|
|
439
479
|
// =============================================================================
|
|
440
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Helper function to analyze a state node with optional nesting support
|
|
483
|
+
*/
|
|
484
|
+
function analyzeStateNodeWithNesting(
|
|
485
|
+
className: string,
|
|
486
|
+
classSymbol: any,
|
|
487
|
+
sourceFile: any,
|
|
488
|
+
childConfig: ChildStatesConfig | undefined,
|
|
489
|
+
verbose = false
|
|
490
|
+
): any {
|
|
491
|
+
const stateNode = analyzeStateNode(classSymbol, verbose) as any;
|
|
492
|
+
|
|
493
|
+
// If this state has children, analyze them recursively
|
|
494
|
+
if (childConfig) {
|
|
495
|
+
if (verbose) {
|
|
496
|
+
console.error(` 👪 Analyzing children for state: ${className}`);
|
|
497
|
+
}
|
|
498
|
+
stateNode.initial = childConfig.initialState;
|
|
499
|
+
stateNode.states = {};
|
|
500
|
+
|
|
501
|
+
// Recursively analyze each child state
|
|
502
|
+
for (const childClassName of childConfig.classes) {
|
|
503
|
+
const childClassDeclaration = sourceFile.getClass(childClassName);
|
|
504
|
+
if (childClassDeclaration) {
|
|
505
|
+
const childSymbol = childClassDeclaration.getSymbolOrThrow();
|
|
506
|
+
stateNode.states[childClassName] = analyzeStateNode(childSymbol, verbose);
|
|
507
|
+
} else {
|
|
508
|
+
console.warn(`⚠️ Warning: Child class '${childClassName}' not found.`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return stateNode;
|
|
514
|
+
}
|
|
515
|
+
|
|
441
516
|
/**
|
|
442
517
|
* Extracts a single machine configuration to a statechart
|
|
443
518
|
*
|
|
@@ -461,6 +536,56 @@ export function extractMachine(
|
|
|
461
536
|
throw new Error(`Source file not found: ${config.input}`);
|
|
462
537
|
}
|
|
463
538
|
|
|
539
|
+
// Handle parallel machine configuration
|
|
540
|
+
if (config.parallel) {
|
|
541
|
+
if (verbose) {
|
|
542
|
+
console.error(` ⏹️ Parallel machine detected. Analyzing regions.`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const parallelChart: any = {
|
|
546
|
+
id: config.id,
|
|
547
|
+
type: 'parallel',
|
|
548
|
+
states: {},
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
if (config.description) {
|
|
552
|
+
parallelChart.description = config.description;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
for (const region of config.parallel.regions) {
|
|
556
|
+
if (verbose) {
|
|
557
|
+
console.error(` 📍 Analyzing region: ${region.name}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const regionStates: any = {};
|
|
561
|
+
for (const className of region.classes) {
|
|
562
|
+
const classDeclaration = sourceFile.getClass(className);
|
|
563
|
+
if (classDeclaration) {
|
|
564
|
+
const classSymbol = classDeclaration.getSymbolOrThrow();
|
|
565
|
+
regionStates[className] = analyzeStateNode(classSymbol, verbose);
|
|
566
|
+
} else {
|
|
567
|
+
console.warn(`⚠️ Warning: Class '${className}' not found for region '${region.name}'.`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
parallelChart.states[region.name] = {
|
|
572
|
+
initial: region.initialState,
|
|
573
|
+
states: regionStates,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (verbose) {
|
|
578
|
+
console.error(` ✅ Extracted ${config.parallel.regions.length} parallel regions`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return parallelChart;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Handle standard FSM configuration
|
|
585
|
+
if (!config.initialState || !config.classes) {
|
|
586
|
+
throw new Error(`Machine config for '${config.id}' must have either 'parallel' or 'initialState'/'classes'.`);
|
|
587
|
+
}
|
|
588
|
+
|
|
464
589
|
const fullChart: any = {
|
|
465
590
|
id: config.id,
|
|
466
591
|
initial: config.initialState,
|
|
@@ -478,7 +603,17 @@ export function extractMachine(
|
|
|
478
603
|
continue;
|
|
479
604
|
}
|
|
480
605
|
const classSymbol = classDeclaration.getSymbolOrThrow();
|
|
481
|
-
|
|
606
|
+
|
|
607
|
+
// Check if this is the initial state and has children configuration
|
|
608
|
+
const hasChildren = className === config.initialState && config.children;
|
|
609
|
+
const stateNode = analyzeStateNodeWithNesting(
|
|
610
|
+
className,
|
|
611
|
+
classSymbol,
|
|
612
|
+
sourceFile,
|
|
613
|
+
hasChildren ? config.children : undefined,
|
|
614
|
+
verbose
|
|
615
|
+
);
|
|
616
|
+
|
|
482
617
|
fullChart.states[className] = stateNode;
|
|
483
618
|
}
|
|
484
619
|
|
|
@@ -569,6 +704,43 @@ export function generateChart() {
|
|
|
569
704
|
}
|
|
570
705
|
}
|
|
571
706
|
|
|
707
|
+
/**
|
|
708
|
+
* Example configuration demonstrating hierarchical and parallel machines.
|
|
709
|
+
* This is not used by default but serves as documentation.
|
|
710
|
+
*/
|
|
711
|
+
export const ADVANCED_CONFIG_EXAMPLES = {
|
|
712
|
+
hierarchical: {
|
|
713
|
+
input: 'examples/dashboardMachine.ts',
|
|
714
|
+
id: 'dashboard',
|
|
715
|
+
classes: ['DashboardMachine', 'LoggedOutMachine'],
|
|
716
|
+
initialState: 'DashboardMachine',
|
|
717
|
+
children: {
|
|
718
|
+
contextProperty: 'child',
|
|
719
|
+
initialState: 'ViewingChildMachine',
|
|
720
|
+
classes: ['ViewingChildMachine', 'EditingChildMachine'],
|
|
721
|
+
},
|
|
722
|
+
} as MachineConfig,
|
|
723
|
+
|
|
724
|
+
parallel: {
|
|
725
|
+
input: 'examples/editorMachine.ts',
|
|
726
|
+
id: 'editor',
|
|
727
|
+
parallel: {
|
|
728
|
+
regions: [
|
|
729
|
+
{
|
|
730
|
+
name: 'fontWeight',
|
|
731
|
+
initialState: 'NormalWeight',
|
|
732
|
+
classes: ['NormalWeight', 'BoldWeight'],
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: 'textDecoration',
|
|
736
|
+
initialState: 'NoDecoration',
|
|
737
|
+
classes: ['NoDecoration', 'UnderlineState'],
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
},
|
|
741
|
+
} as MachineConfig,
|
|
742
|
+
};
|
|
743
|
+
|
|
572
744
|
// This allows the script to be executed directly from the command line.
|
|
573
745
|
if (require.main === module) {
|
|
574
746
|
generateChart();
|
package/src/generators.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Runs a generator-based state machine flow to completion.
|
|
@@ -117,9 +117,9 @@ import { Machine } from './index';
|
|
|
117
117
|
* }, machine);
|
|
118
118
|
* ```
|
|
119
119
|
*/
|
|
120
|
-
export function run<C extends
|
|
121
|
-
flow: (m:
|
|
122
|
-
initial:
|
|
120
|
+
export function run<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }, T = any>(
|
|
121
|
+
flow: (m: M) => Generator<M, T, M>,
|
|
122
|
+
initial: M
|
|
123
123
|
): T {
|
|
124
124
|
// Create the generator by calling the flow function with the initial machine
|
|
125
125
|
const generator = flow(initial);
|
|
@@ -197,9 +197,9 @@ export function run<C extends object, T>(
|
|
|
197
197
|
* }, machine);
|
|
198
198
|
* ```
|
|
199
199
|
*/
|
|
200
|
-
export function step<C extends
|
|
201
|
-
m:
|
|
202
|
-
): Generator<
|
|
200
|
+
export function step<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(
|
|
201
|
+
m: M
|
|
202
|
+
): Generator<M, M, M> {
|
|
203
203
|
// Create an immediately-invoked generator that:
|
|
204
204
|
// 1. Yields the provided machine
|
|
205
205
|
// 2. Receives a value back (the next state)
|
|
@@ -229,7 +229,7 @@ export function step<C extends object>(
|
|
|
229
229
|
* }, counter);
|
|
230
230
|
* ```
|
|
231
231
|
*/
|
|
232
|
-
export function yieldMachine<C extends
|
|
232
|
+
export function yieldMachine<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(m: M): M {
|
|
233
233
|
return m;
|
|
234
234
|
}
|
|
235
235
|
|
|
@@ -259,10 +259,10 @@ export function yieldMachine<C extends object>(m: Machine<C>): Machine<C> {
|
|
|
259
259
|
* console.log(result.context.count); // 6
|
|
260
260
|
* ```
|
|
261
261
|
*/
|
|
262
|
-
export function runSequence<C extends
|
|
263
|
-
initial:
|
|
264
|
-
flows: Array<(m:
|
|
265
|
-
):
|
|
262
|
+
export function runSequence<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(
|
|
263
|
+
initial: M,
|
|
264
|
+
flows: Array<(m: M) => Generator<M, M, M>>
|
|
265
|
+
): M {
|
|
266
266
|
return flows.reduce((machine, flow) => {
|
|
267
267
|
return run(flow, machine);
|
|
268
268
|
}, initial);
|
|
@@ -295,9 +295,9 @@ export function runSequence<C extends object>(
|
|
|
295
295
|
* }, counter);
|
|
296
296
|
* ```
|
|
297
297
|
*/
|
|
298
|
-
export function createFlow<C extends
|
|
299
|
-
flow: (m:
|
|
300
|
-
): (m:
|
|
298
|
+
export function createFlow<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(
|
|
299
|
+
flow: (m: M) => Generator<M, M, M>
|
|
300
|
+
): (m: M) => Generator<M, M, M> {
|
|
301
301
|
return flow;
|
|
302
302
|
}
|
|
303
303
|
|
|
@@ -328,10 +328,10 @@ export function createFlow<C extends object>(
|
|
|
328
328
|
* // Final: 6
|
|
329
329
|
* ```
|
|
330
330
|
*/
|
|
331
|
-
export function runWithDebug<C extends
|
|
332
|
-
flow: (m:
|
|
333
|
-
initial:
|
|
334
|
-
logger: (step: number, machine:
|
|
331
|
+
export function runWithDebug<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }, T = any>(
|
|
332
|
+
flow: (m: M) => Generator<M, T, M>,
|
|
333
|
+
initial: M,
|
|
334
|
+
logger: (step: number, machine: M) => void = (step, m) => {
|
|
335
335
|
console.log(`Step ${step}:`, m.context);
|
|
336
336
|
}
|
|
337
337
|
): T {
|
|
@@ -380,9 +380,9 @@ export function runWithDebug<C extends object, T>(
|
|
|
380
380
|
* }, asyncMachine);
|
|
381
381
|
* ```
|
|
382
382
|
*/
|
|
383
|
-
export async function runAsync<C extends
|
|
384
|
-
flow: (m:
|
|
385
|
-
initial:
|
|
383
|
+
export async function runAsync<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }, T = any>(
|
|
384
|
+
flow: (m: M) => AsyncGenerator<M, T, M>,
|
|
385
|
+
initial: M
|
|
386
386
|
): Promise<T> {
|
|
387
387
|
const generator = flow(initial);
|
|
388
388
|
let current = initial;
|
|
@@ -413,9 +413,9 @@ export async function runAsync<C extends object, T>(
|
|
|
413
413
|
* }, machine);
|
|
414
414
|
* ```
|
|
415
415
|
*/
|
|
416
|
-
export async function* stepAsync<C extends
|
|
417
|
-
m:
|
|
418
|
-
): AsyncGenerator<
|
|
416
|
+
export async function* stepAsync<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(
|
|
417
|
+
m: M
|
|
418
|
+
): AsyncGenerator<M, M, M> {
|
|
419
419
|
const received = yield m;
|
|
420
420
|
return received;
|
|
421
421
|
}
|