@statewalker/fsm 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -0
- package/dist/index.d.ts +297 -279
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +543 -761
- package/dist/index.js.map +1 -1
- package/package.json +10 -20
- package/src/core/fsm-base-class.ts +26 -26
- package/src/core/fsm-process.ts +69 -11
- package/src/core/fsm-state-config.ts +27 -0
- package/src/core/fsm-state-descriptor.ts +21 -0
- package/src/core/fsm-state.ts +22 -21
- package/src/core/fsm-transitions.ts +72 -0
- package/src/core/index.ts +2 -1
- package/src/index.ts +2 -5
- package/src/start-process.ts +185 -0
- package/src/trace/index.ts +2 -0
- package/src/trace/printer.ts +62 -0
- package/src/{utils → trace}/tracer.ts +12 -1
- package/CHANGELOG.md +0 -59
- package/bin/cli.js +0 -5
- package/src/core/new-fsm-process.ts +0 -83
- package/src/orchestrator/constants.ts +0 -130
- package/src/orchestrator/index.ts +0 -9
- package/src/orchestrator/is-generator.ts +0 -12
- package/src/orchestrator/launcher.ts +0 -172
- package/src/orchestrator/process-config-manager.ts +0 -70
- package/src/orchestrator/resolve-module-refs.ts +0 -52
- package/src/orchestrator/start-node-processes.ts +0 -19
- package/src/orchestrator/start-process.ts +0 -73
- package/src/orchestrator/start-processes.ts +0 -32
- package/src/orchestrator/types.ts +0 -34
- package/src/utils/handlers.ts +0 -35
- package/src/utils/index.ts +0 -4
- package/src/utils/printer.ts +0 -48
- package/src/utils/process.ts +0 -26
- package/src/utils/transitions.ts +0 -56
package/dist/index.js
CHANGED
|
@@ -1,786 +1,568 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/core/fsm-base-class.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared handler-registry substrate under both `FsmProcess` and `FsmState`.
|
|
4
|
+
*
|
|
5
|
+
* Both the process and every state need the same primitive: keep named lists of
|
|
6
|
+
* callbacks (`onEnter`, `onExit`, `dump`, …) and run them with consistent ordering
|
|
7
|
+
* and error routing. Centralising it here keeps the two subclasses thin and their
|
|
8
|
+
* hook methods one-liners. `handlers` maps a hook name to its ordered callback list;
|
|
9
|
+
* the protected `_addHandler` / `_removeHandler` / `_runHandler` manage and invoke
|
|
10
|
+
* them. Handlers run **sequentially** (awaited in turn); a throwing handler is routed
|
|
11
|
+
* to `_handleError` rather than aborting the batch. Subclasses expose typed sugar
|
|
12
|
+
* (e.g. `state.onEnter(fn)` calls `_addHandler("onEnter", fn)`) and override
|
|
13
|
+
* `_handleError` to forward errors.
|
|
14
|
+
*/
|
|
2
15
|
var FsmBaseClass = class {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
await Promise.all(
|
|
39
|
-
list.map(async (handler) => {
|
|
40
|
-
try {
|
|
41
|
-
await handler(...args);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
this._handleError(error);
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
async _runHandler(type, ...args) {
|
|
49
|
-
const list = this.handlers[type] || [];
|
|
50
|
-
for (const handler of list) {
|
|
51
|
-
try {
|
|
52
|
-
await handler(...args);
|
|
53
|
-
} catch (error) {
|
|
54
|
-
await this._handleError(error);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async _handleError(error) {
|
|
59
|
-
console.error(error);
|
|
60
|
-
}
|
|
16
|
+
constructor() {
|
|
17
|
+
this.handlers = {};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Register `handler` under `type`; returns a disposer that removes it.
|
|
21
|
+
* `direct=false` prepends instead of appends — used so `onExit` handlers run in
|
|
22
|
+
* reverse (inner-to-outer) unwinding order.
|
|
23
|
+
*/
|
|
24
|
+
_addHandler(type, handler, direct = true) {
|
|
25
|
+
let list = this.handlers[type];
|
|
26
|
+
if (!list) list = this.handlers[type] = [];
|
|
27
|
+
const h = handler;
|
|
28
|
+
direct ? list.push(h) : list.unshift(h);
|
|
29
|
+
return () => this._removeHandler(type, h);
|
|
30
|
+
}
|
|
31
|
+
_removeHandler(type, handler) {
|
|
32
|
+
let list = this.handlers[type];
|
|
33
|
+
if (!list) return;
|
|
34
|
+
list = list.filter((h) => h !== handler);
|
|
35
|
+
if (list.length > 0) this.handlers[type] = list;
|
|
36
|
+
else delete this.handlers[type];
|
|
37
|
+
}
|
|
38
|
+
/** Run every handler of `type` in order, awaiting each; route throws to `_handleError`. */
|
|
39
|
+
async _runHandler(type, ...args) {
|
|
40
|
+
const list = this.handlers[type] || [];
|
|
41
|
+
for (const handler of list) try {
|
|
42
|
+
await handler(...args);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
await this._handleError(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Default error sink — logs. `FsmState`/`FsmProcess` override it to fire `onStateError`. */
|
|
48
|
+
async _handleError(error) {
|
|
49
|
+
console.error(error);
|
|
50
|
+
}
|
|
61
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Bind the named methods of `obj` to `obj` in place, so they can be passed as bare
|
|
54
|
+
* callbacks (e.g. `ctx[KEY_DISPATCH] = process.dispatch`) without losing `this`.
|
|
55
|
+
*/
|
|
62
56
|
function bindMethods(obj, ...methods) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
const o = obj;
|
|
58
|
+
for (const methodName of methods) {
|
|
59
|
+
const method = o[methodName];
|
|
60
|
+
if (typeof method !== "function") continue;
|
|
61
|
+
o[methodName] = method.bind(o);
|
|
62
|
+
}
|
|
63
|
+
return obj;
|
|
70
64
|
}
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/core/fsm-state.ts
|
|
67
|
+
/**
|
|
68
|
+
* One live node in a running machine's active-state stack.
|
|
69
|
+
*
|
|
70
|
+
* A state needs a place to attach behaviour and record data while it is active.
|
|
71
|
+
* `FsmState` is that handle — created by the engine each time a state is entered,
|
|
72
|
+
* discarded when it exits — so handlers can capture per-activation closures instead
|
|
73
|
+
* of sharing mutable machine-wide state. It offers lifecycle hooks `onEnter` /
|
|
74
|
+
* `onExit` / `onStateError`, serialization hooks `dump` / `restore`, and the tree
|
|
75
|
+
* links `key` / `parent` / `descriptor`; every hook method returns a disposer.
|
|
76
|
+
* Attach hooks from within `FsmProcess.onStateCreate((state) => …)`, or let
|
|
77
|
+
* `startProcess` install them for you from a `load` callback.
|
|
78
|
+
*/
|
|
73
79
|
var FsmState = class extends FsmBaseClass {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return this.data[key] ?? (recursive ? this.parent?.getData(key, recursive) : void 0);
|
|
107
|
-
}
|
|
108
|
-
useData(key) {
|
|
109
|
-
return [
|
|
110
|
-
(recursive = true) => this.getData(key, recursive),
|
|
111
|
-
(value) => this.setData(key, value)
|
|
112
|
-
];
|
|
113
|
-
}
|
|
114
|
-
async _handleError(error) {
|
|
115
|
-
await this._runHandler("onStateError", this, error);
|
|
116
|
-
await this.process._handleStateError(this, error);
|
|
117
|
-
}
|
|
80
|
+
constructor(process, parent, key, descriptor) {
|
|
81
|
+
super();
|
|
82
|
+
this.process = process;
|
|
83
|
+
this.key = key;
|
|
84
|
+
this.parent = parent;
|
|
85
|
+
this.descriptor = descriptor;
|
|
86
|
+
bindMethods(this, "onEnter", "onExit", "dump", "restore", "onStateError");
|
|
87
|
+
}
|
|
88
|
+
/** Run when this state is entered. */
|
|
89
|
+
onEnter(handler) {
|
|
90
|
+
return this._addHandler("onEnter", handler, true);
|
|
91
|
+
}
|
|
92
|
+
/** Run when this state exits — registered inner-first so unwinding is inner-to-outer. */
|
|
93
|
+
onExit(handler) {
|
|
94
|
+
return this._addHandler("onExit", handler, false);
|
|
95
|
+
}
|
|
96
|
+
/** Handle an error thrown by another handler on this state (also bubbles to the process). */
|
|
97
|
+
onStateError(handler) {
|
|
98
|
+
return this._addHandler("onStateError", handler);
|
|
99
|
+
}
|
|
100
|
+
/** Contribute to this state's snapshot: fill `dump.data` when the process is dumped. */
|
|
101
|
+
dump(handler) {
|
|
102
|
+
return this._addHandler("dump", handler, true);
|
|
103
|
+
}
|
|
104
|
+
/** Rehydrate from this state's snapshot: read `dump.data` when the process is restored. */
|
|
105
|
+
restore(handler) {
|
|
106
|
+
return this._addHandler("restore", handler, true);
|
|
107
|
+
}
|
|
108
|
+
async _handleError(error) {
|
|
109
|
+
await this._runHandler("onStateError", this, error);
|
|
110
|
+
await this.process._handleStateError(this, error);
|
|
111
|
+
}
|
|
118
112
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/core/fsm-state-config.ts
|
|
115
|
+
/** Wildcard `from`-state: a transition that applies in *any* state. */
|
|
116
|
+
const STATE_ANY = "*";
|
|
117
|
+
/** Empty `from`-state: the *initial* pseudo-state a parent enters into. */
|
|
118
|
+
const STATE_INITIAL = "";
|
|
119
|
+
/** Empty `to`-state: the *final* pseudo-state; entering it exits the parent. */
|
|
120
|
+
const STATE_FINAL = "";
|
|
121
|
+
/** Wildcard `event`: a transition triggered by *any* event. */
|
|
122
|
+
const EVENT_ANY = "*";
|
|
123
|
+
/** Empty event: the eventless/automatic transition (fires with no named event). */
|
|
124
|
+
const EVENT_EMPTY = "";
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/core/fsm-state-descriptor.ts
|
|
127
|
+
/**
|
|
128
|
+
* The *compiled* form of an `FsmStateConfig` subtree.
|
|
129
|
+
*
|
|
130
|
+
* Resolving a transition happens on every event, so the flat `[from, event, to]`
|
|
131
|
+
* tuple list is pre-indexed once into nested maps for O(1) lookup, and the
|
|
132
|
+
* wildcard-fallback order is applied here rather than re-derived on each dispatch.
|
|
133
|
+
* The shape is `transitions[from][event] = to` plus a `states` map of child
|
|
134
|
+
* descriptors. `FsmProcess` builds one from your config in its constructor; you
|
|
135
|
+
* rarely construct a descriptor directly.
|
|
136
|
+
*/
|
|
137
|
+
var FsmStateDescriptor = class FsmStateDescriptor {
|
|
138
|
+
constructor() {
|
|
139
|
+
this.transitions = {};
|
|
140
|
+
this.states = {};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Recursively compile a config (and its nested `states`) into descriptors,
|
|
144
|
+
* turning the tuple list into the indexed `transitions` map.
|
|
145
|
+
*/
|
|
146
|
+
static build(config) {
|
|
147
|
+
const descriptor = new FsmStateDescriptor();
|
|
148
|
+
for (const [from, event, to] of config.transitions || []) {
|
|
149
|
+
let index = descriptor.transitions[from];
|
|
150
|
+
if (!index) index = descriptor.transitions[from] = {};
|
|
151
|
+
index[event] = to;
|
|
152
|
+
}
|
|
153
|
+
if (config.states) for (const substateConfig of config.states) descriptor.states[substateConfig.key] = FsmStateDescriptor.build(substateConfig);
|
|
154
|
+
return descriptor;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the target state for a `(stateKey, eventKey)` pair. Tries the most
|
|
158
|
+
* specific match first, then falls back through the wildcards —
|
|
159
|
+
* `(state, event)` → `(*, event)` → `(state, *)` → `(*, *)` — and returns
|
|
160
|
+
* `STATE_FINAL` if nothing matches, so an unmatched event exits the state rather
|
|
161
|
+
* than silently doing nothing.
|
|
162
|
+
*/
|
|
163
|
+
getTargetStateKey(stateKey, eventKey) {
|
|
164
|
+
const pairs = [
|
|
165
|
+
[stateKey, eventKey],
|
|
166
|
+
["*", eventKey],
|
|
167
|
+
[stateKey, "*"],
|
|
168
|
+
["*", "*"]
|
|
169
|
+
];
|
|
170
|
+
let targetKey;
|
|
171
|
+
for (let i = 0, len = pairs.length; targetKey === void 0 && i < len; i++) {
|
|
172
|
+
const [stateKey, eventKey] = pairs[i];
|
|
173
|
+
const stateTransitions = this.transitions[stateKey];
|
|
174
|
+
if (!stateTransitions) continue;
|
|
175
|
+
targetKey = stateTransitions[eventKey];
|
|
176
|
+
}
|
|
177
|
+
return targetKey !== void 0 ? targetKey : "";
|
|
178
|
+
}
|
|
165
179
|
};
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/core/fsm-process.ts
|
|
182
|
+
/** Not started / no current transition. */
|
|
183
|
+
const STATUS_NONE = 0;
|
|
184
|
+
/** Entering: descended into a parent's first (initial) child. */
|
|
185
|
+
const STATUS_FIRST = 1;
|
|
186
|
+
/** Entering: advanced to a resolved target (sibling) state. */
|
|
187
|
+
const STATUS_NEXT = 2;
|
|
188
|
+
/** Rested on a leaf — dispatch returns control here (the default `mask`). */
|
|
189
|
+
const STATUS_LEAF = 4;
|
|
190
|
+
/** Exiting: popped back up to the parent state. */
|
|
191
|
+
const STATUS_LAST = 8;
|
|
192
|
+
/** Terminated: the machine has exited its root and cannot advance further. */
|
|
193
|
+
const STATUS_FINISHED = 16;
|
|
194
|
+
/** Composite: any "entering" phase. */
|
|
195
|
+
const STATUS_ENTER = 3;
|
|
196
|
+
/** Composite: any "exiting" phase. */
|
|
197
|
+
const STATUS_EXIT = 12;
|
|
198
|
+
/**
|
|
199
|
+
* The running state machine — owner of the active-state stack and the traversal.
|
|
200
|
+
*
|
|
201
|
+
* This is the engine: given a compiled config it drives the enter/exit walk in
|
|
202
|
+
* response to events, so callers reason in terms of states and events, not manual
|
|
203
|
+
* stack bookkeeping. `dispatch(event)` advances the machine to the next resting leaf;
|
|
204
|
+
* `shutdown(event?)` unwinds every active state; `state` is the current leaf;
|
|
205
|
+
* `onStateCreate(handler)` is the primary extension point (fires once per created
|
|
206
|
+
* state — attach that state's hooks there); and `dump()` / `restore(dump)` snapshot
|
|
207
|
+
* and rehydrate the whole stack. Typical use: `new FsmProcess(config)`, register
|
|
208
|
+
* `onStateCreate`, then `dispatch("")` to enter the initial state and
|
|
209
|
+
* `dispatch(event)` for each subsequent event.
|
|
210
|
+
*/
|
|
176
211
|
var FsmProcess = class extends FsmBaseClass {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
212
|
+
constructor(config) {
|
|
213
|
+
super();
|
|
214
|
+
this.running = false;
|
|
215
|
+
this.mask = 4;
|
|
216
|
+
this.status = 0;
|
|
217
|
+
this.rootDescriptor = FsmStateDescriptor.build(config);
|
|
218
|
+
this.config = config;
|
|
219
|
+
bindMethods(this, "dispatch", "dump", "restore");
|
|
220
|
+
}
|
|
221
|
+
/** Force-exit the whole stack (root last), running each state's `onExit`; ends the machine. */
|
|
222
|
+
async shutdown(event) {
|
|
223
|
+
while (this.state) {
|
|
224
|
+
this.event = event;
|
|
225
|
+
this.status = 16;
|
|
226
|
+
await this.state?._runHandler("onExit", this.state);
|
|
227
|
+
this.state = this.state.parent;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Feed an event to the machine and run the enter/exit cycle until it rests on a
|
|
232
|
+
* leaf (`status & mask`) or finishes.
|
|
233
|
+
*
|
|
234
|
+
* If a dispatch is already running (e.g. an `onEnter` handler dispatches
|
|
235
|
+
* synchronously), the event is queued in `nextEvent` and applied when the current
|
|
236
|
+
* run settles — runs never nest. Returns `false` once the machine has finished,
|
|
237
|
+
* `true` otherwise.
|
|
238
|
+
*/
|
|
239
|
+
async dispatch(event) {
|
|
240
|
+
this.nextEvent = event;
|
|
241
|
+
if (!this.running && !(this.status & 16)) {
|
|
242
|
+
this.running = true;
|
|
243
|
+
try {
|
|
244
|
+
this.event = this.nextEvent;
|
|
245
|
+
this.nextEvent = void 0;
|
|
246
|
+
while (true) {
|
|
247
|
+
if (this.status & 12) await this.state?._runHandler("onExit", this.state);
|
|
248
|
+
if (!await this._update()) break;
|
|
249
|
+
if (this.status & 3) await this.state?._runHandler("onEnter", this.state);
|
|
250
|
+
if (this.status & this.mask) break;
|
|
251
|
+
}
|
|
252
|
+
} finally {
|
|
253
|
+
this.running = false;
|
|
254
|
+
}
|
|
255
|
+
const nextEvent = this.nextEvent;
|
|
256
|
+
if (nextEvent !== void 0) return Promise.resolve().then(() => this.dispatch(nextEvent));
|
|
257
|
+
}
|
|
258
|
+
return !(this.status & 16);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Snapshot the machine to a plain object: `{ status, event, stack }` where the
|
|
262
|
+
* stack is root→leaf, each entry carrying whatever its `dump` hooks recorded.
|
|
263
|
+
*/
|
|
264
|
+
async dump(...args) {
|
|
265
|
+
const dumpState = async (state) => {
|
|
266
|
+
const stateDump = {
|
|
267
|
+
key: state.key,
|
|
268
|
+
data: {}
|
|
269
|
+
};
|
|
270
|
+
await state._runHandler("dump", state, stateDump.data, ...args);
|
|
271
|
+
return stateDump;
|
|
272
|
+
};
|
|
273
|
+
const dumpStates = async (state, stack = []) => {
|
|
274
|
+
if (!state) return stack;
|
|
275
|
+
state.parent && await dumpStates(state.parent, stack);
|
|
276
|
+
stack.push(await dumpState(state));
|
|
277
|
+
return stack;
|
|
278
|
+
};
|
|
279
|
+
return {
|
|
280
|
+
status: this.status,
|
|
281
|
+
event: this.event,
|
|
282
|
+
stack: await dumpStates(this.state)
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Rebuild the machine from a `dump`: recreate each state in the stack (firing
|
|
287
|
+
* `onStateCreate`) and replay its `restore` hooks, leaving the process resumable
|
|
288
|
+
* from where it was snapshotted.
|
|
289
|
+
*/
|
|
290
|
+
async restore(dump, ...args) {
|
|
291
|
+
this.status = dump.status || 0;
|
|
292
|
+
this.event = dump.event;
|
|
293
|
+
this.state = void 0;
|
|
294
|
+
for (let i = 0; i < dump.stack.length; i++) {
|
|
295
|
+
const stateDump = dump.stack[i];
|
|
296
|
+
this.state = this.state ? await this._newSubstate(this.state, stateDump.key) : await this._newState(void 0, stateDump.key, this.rootDescriptor);
|
|
297
|
+
await this.state._runHandler("restore", this.state, stateDump.data, ...args);
|
|
298
|
+
}
|
|
299
|
+
return this;
|
|
300
|
+
}
|
|
301
|
+
/** Primary extension point: fires once for every state the machine creates. */
|
|
302
|
+
onStateCreate(handler) {
|
|
303
|
+
return this._addHandler("onStateCreate", handler, true);
|
|
304
|
+
}
|
|
305
|
+
onStateError(handler) {
|
|
306
|
+
return this._addHandler("onStateError", handler);
|
|
307
|
+
}
|
|
308
|
+
async _handleStateError(state, error) {
|
|
309
|
+
await this._runHandler("onStateError", state, error);
|
|
310
|
+
this._handleError(error);
|
|
311
|
+
}
|
|
312
|
+
async _newState(parent, key, descriptor) {
|
|
313
|
+
const state = new FsmState(this, parent, key, descriptor);
|
|
314
|
+
await this._runHandler("onStateCreate", state);
|
|
315
|
+
return state;
|
|
316
|
+
}
|
|
317
|
+
async _getSubstate(parent, prevStateKey) {
|
|
318
|
+
if (!parent) return;
|
|
319
|
+
const toState = parent.descriptor?.getTargetStateKey(prevStateKey || "", this.event || "") || "";
|
|
320
|
+
if (!toState) return;
|
|
321
|
+
return this._newSubstate(parent, toState);
|
|
322
|
+
}
|
|
323
|
+
async _newSubstate(parent, toState) {
|
|
324
|
+
let descriptor;
|
|
325
|
+
for (let state = parent; !descriptor && state; state = state.parent) descriptor = state.descriptor?.states[toState];
|
|
326
|
+
return this._newState(parent, toState, descriptor);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* One step of the traversal: compute the next state and update `status`.
|
|
330
|
+
*
|
|
331
|
+
* Either descends to a child / advances to a resolved target (setting an ENTER
|
|
332
|
+
* status), or — when no target resolves — settles on the current leaf
|
|
333
|
+
* (`STATUS_LEAF`) or pops to the parent (`STATUS_LAST`), reaching `STATUS_FINISHED`
|
|
334
|
+
* once the root is exited. Returns `false` when finished.
|
|
335
|
+
*/
|
|
336
|
+
async _update() {
|
|
337
|
+
if (this.status & 16) return false;
|
|
338
|
+
const nextState = this.status !== 0 ? this.status & 3 ? await this._getSubstate(this.state, "") : await this._getSubstate(this.state?.parent, this.state?.key) : await this._newState(void 0, this.config.key, this.rootDescriptor);
|
|
339
|
+
if (nextState !== void 0) {
|
|
340
|
+
this.state = nextState;
|
|
341
|
+
this.status = this.status & 12 ? 2 : 1;
|
|
342
|
+
} else {
|
|
343
|
+
if (this.status & 12) {
|
|
344
|
+
this.state = this.state?.parent;
|
|
345
|
+
this.status = 8;
|
|
346
|
+
} else this.status = 4;
|
|
347
|
+
if (!this.state) this.status = 16;
|
|
348
|
+
}
|
|
349
|
+
return !(this.status & 16);
|
|
350
|
+
}
|
|
307
351
|
};
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return active;
|
|
320
|
-
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/core/fsm-transitions.ts
|
|
354
|
+
/**
|
|
355
|
+
* List the transitions currently reachable from `state` — for a UI or viewer that
|
|
356
|
+
* needs to know which events can fire right now (e.g. to render only the enabled
|
|
357
|
+
* buttons). Collects `[from, event, to]` tuples by walking up the parent chain; the
|
|
358
|
+
* nearest state's rule for an event wins, so an outer fallback is masked by an inner
|
|
359
|
+
* override. The returned tuples are ordered outer→inner (root first).
|
|
360
|
+
*/
|
|
321
361
|
function getStateTransitions(state) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
return result.reverse();
|
|
362
|
+
const result = [];
|
|
363
|
+
const index = {};
|
|
364
|
+
if (state) {
|
|
365
|
+
let prevStateKey = state.key;
|
|
366
|
+
for (let parent = state.parent; parent; parent = parent.parent) {
|
|
367
|
+
if (!parent.descriptor) continue;
|
|
368
|
+
result.push(...getTransitionsFromDescriptor(parent.descriptor, prevStateKey, index));
|
|
369
|
+
prevStateKey = parent.key;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return result.reverse();
|
|
335
373
|
}
|
|
336
|
-
function getTransitionsFromDescriptor(descriptor, prevStateKey, index
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
374
|
+
function getTransitionsFromDescriptor(descriptor, prevStateKey, index) {
|
|
375
|
+
const result = [];
|
|
376
|
+
const prevStateKeys = [prevStateKey, "*"];
|
|
377
|
+
for (const prevKey of prevStateKeys) {
|
|
378
|
+
const targets = descriptor.transitions[prevKey];
|
|
379
|
+
if (targets) for (const [event, target] of Object.entries(targets)) {
|
|
380
|
+
if (index[event]) continue;
|
|
381
|
+
index[event] = true;
|
|
382
|
+
result.push([
|
|
383
|
+
prevStateKey,
|
|
384
|
+
event,
|
|
385
|
+
target
|
|
386
|
+
]);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
352
390
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
started = true;
|
|
364
|
-
state.onEnter(async () => {
|
|
365
|
-
const modules = await load(state.key, process2.event) ?? [];
|
|
366
|
-
for (const module of Array.isArray(modules) ? modules : [modules]) {
|
|
367
|
-
const result = await module?.(context);
|
|
368
|
-
if (isGenerator2(result)) {
|
|
369
|
-
state.onExit(() => {
|
|
370
|
-
result.return?.(void 0);
|
|
371
|
-
});
|
|
372
|
-
(async () => {
|
|
373
|
-
for await (const event of result) {
|
|
374
|
-
if (terminated) {
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
await dispatch(event);
|
|
378
|
-
if (terminated) {
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
})();
|
|
383
|
-
} else if (typeof result === "function") {
|
|
384
|
-
state.onExit(result);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
async function shutdown() {
|
|
390
|
-
await process2.shutdown();
|
|
391
|
-
terminated = true;
|
|
392
|
-
}
|
|
393
|
-
return [dispatch, shutdown];
|
|
394
|
-
function isGenerator2(value) {
|
|
395
|
-
return typeof value === "object" && value !== null && "next" in value && typeof value.next === "function";
|
|
396
|
-
}
|
|
391
|
+
/**
|
|
392
|
+
* Guard: would `event` trigger any transition in the process's current state? Used
|
|
393
|
+
* to avoid dispatching dead events — the runner calls it before `dispatch`, and
|
|
394
|
+
* consumers use it to enable/disable controls. Returns `true` iff `event` appears
|
|
395
|
+
* among `getStateTransitions(process.state)`.
|
|
396
|
+
*/
|
|
397
|
+
function isStateTransitionEnabled(process, event) {
|
|
398
|
+
const transitions = getStateTransitions(process.state);
|
|
399
|
+
for (const [, ev] of transitions) if (ev === event) return true;
|
|
400
|
+
return false;
|
|
397
401
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return visit(value);
|
|
411
|
-
function visit(val) {
|
|
412
|
-
if (!val) {
|
|
413
|
-
return [];
|
|
414
|
-
}
|
|
415
|
-
if (typeof val === "function") {
|
|
416
|
-
return [val];
|
|
417
|
-
}
|
|
418
|
-
if (typeof val !== "object") {
|
|
419
|
-
return [];
|
|
420
|
-
}
|
|
421
|
-
if (Array.isArray(val)) {
|
|
422
|
-
return val.flatMap(visit);
|
|
423
|
-
}
|
|
424
|
-
const modulesIndex = val;
|
|
425
|
-
return Object.entries(modulesIndex).flatMap(([key, value2]) => {
|
|
426
|
-
return accept(key, value2) ? visit(value2) : [];
|
|
427
|
-
});
|
|
428
|
-
}
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/start-process.ts
|
|
404
|
+
/** Context key: `(event) => Promise<void>` that dispatches an event (guarded). */
|
|
405
|
+
const KEY_DISPATCH = "fsm:dispatch";
|
|
406
|
+
/** Context key: `() => Promise<void>` that shuts the machine down. */
|
|
407
|
+
const KEY_TERMINATE = "fsm:terminate";
|
|
408
|
+
/** Context key: the current state-key stack (root→leaf), refreshed on every transition. */
|
|
409
|
+
const KEY_STATES = "fsm:states";
|
|
410
|
+
/** Context key: the last dispatched event. */
|
|
411
|
+
const KEY_EVENT = "fsm:event";
|
|
412
|
+
function isGenerator(value) {
|
|
413
|
+
return typeof value === "object" && value !== null && "next" in value && typeof value.next === "function";
|
|
429
414
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
415
|
+
/**
|
|
416
|
+
* Ergonomic runner: build a machine, attach behaviour via one `load` callback, and
|
|
417
|
+
* bind the machine into `context`.
|
|
418
|
+
*
|
|
419
|
+
* Doing this by hand (`new FsmProcess` + `onStateCreate` + `onEnter` + a loader +
|
|
420
|
+
* context wiring) is boilerplate every consumer repeats, so `startProcess` does it
|
|
421
|
+
* once — most callers use this instead of the raw engine. It creates the process; on
|
|
422
|
+
* each state entry calls `load(stateKey, event)` and installs the returned
|
|
423
|
+
* `StageHandler`s (their return values become `onExit` cleanups or event-yielding
|
|
424
|
+
* generators — see `StageHandler`); binds `KEY_DISPATCH` / `KEY_TERMINATE` /
|
|
425
|
+
* `KEY_STATES` / `KEY_EVENT` into `context`; dispatches `startEvent` to enter the
|
|
426
|
+
* initial state; and returns a `ProcessHandle`.
|
|
427
|
+
*
|
|
428
|
+
* @param context shared object handlers read/write and the machine is bound into
|
|
429
|
+
* @param config the declarative machine definition
|
|
430
|
+
* @param load returns the handler(s) to run for a given `(stateKey, event)`
|
|
431
|
+
* @param startEvent initial event (default `""`, the eventless start)
|
|
432
|
+
*/
|
|
433
|
+
async function startProcess(context, config, load, startEvent = "") {
|
|
434
|
+
let terminated = false;
|
|
435
|
+
const ctx = context;
|
|
436
|
+
const process = new FsmProcess(config);
|
|
437
|
+
const statesStack = [];
|
|
438
|
+
process.onStateCreate((state) => {
|
|
439
|
+
state.onEnter(async () => {
|
|
440
|
+
statesStack.push(state.key);
|
|
441
|
+
ctx[KEY_STATES] = [...statesStack];
|
|
442
|
+
ctx[KEY_EVENT] = process.event;
|
|
443
|
+
const modules = await load(state.key, process.event) ?? [];
|
|
444
|
+
for (const module of Array.isArray(modules) ? modules : [modules]) {
|
|
445
|
+
const result = await module?.(context);
|
|
446
|
+
if (isGenerator(result)) {
|
|
447
|
+
let stateExited = false;
|
|
448
|
+
state.onExit(() => {
|
|
449
|
+
stateExited = true;
|
|
450
|
+
result.return?.(void 0);
|
|
451
|
+
});
|
|
452
|
+
(async () => {
|
|
453
|
+
try {
|
|
454
|
+
for await (const event of result) {
|
|
455
|
+
if (stateExited || terminated) break;
|
|
456
|
+
await dispatch(event);
|
|
457
|
+
if (stateExited || terminated) break;
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (!stateExited && !terminated) await state._handleError(error);
|
|
461
|
+
}
|
|
462
|
+
})();
|
|
463
|
+
} else if (typeof result === "function") state.onExit(result);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
state.onExit(() => {
|
|
467
|
+
statesStack.pop();
|
|
468
|
+
ctx[KEY_STATES] = [...statesStack];
|
|
469
|
+
ctx[KEY_EVENT] = process.event;
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
async function dispatch(event) {
|
|
473
|
+
if (event !== void 0 && isStateTransitionEnabled(process, event)) await process.dispatch(event);
|
|
474
|
+
}
|
|
475
|
+
async function terminate() {
|
|
476
|
+
terminated = true;
|
|
477
|
+
await process.shutdown();
|
|
478
|
+
}
|
|
479
|
+
async function dumpProcess(...args) {
|
|
480
|
+
return process.dump(...args);
|
|
481
|
+
}
|
|
482
|
+
async function restoreProcess(dumpData, ...args) {
|
|
483
|
+
statesStack.length = 0;
|
|
484
|
+
terminated = false;
|
|
485
|
+
await process.restore(dumpData, ...args);
|
|
486
|
+
for (let state = process.state; state; state = state.parent) statesStack.unshift(state.key);
|
|
487
|
+
ctx[KEY_STATES] = [...statesStack];
|
|
488
|
+
ctx[KEY_EVENT] = process.event;
|
|
489
|
+
}
|
|
490
|
+
ctx[KEY_DISPATCH] = dispatch;
|
|
491
|
+
ctx[KEY_TERMINATE] = terminate;
|
|
492
|
+
await process.dispatch(startEvent);
|
|
493
|
+
return {
|
|
494
|
+
shutdown: terminate,
|
|
495
|
+
dump: dumpProcess,
|
|
496
|
+
restore: restoreProcess
|
|
497
|
+
};
|
|
502
498
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
499
|
+
/** Permanent equal alias of {@link startProcess} — the name existing consumers import. */
|
|
500
|
+
const startFsmProcess = startProcess;
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region src/trace/printer.ts
|
|
503
|
+
const printerStore = /* @__PURE__ */ new WeakMap();
|
|
504
|
+
/**
|
|
505
|
+
* Build a `Printer` bound to `process` that indents each line by the current state
|
|
506
|
+
* nesting depth (two spaces per level), so log output visually mirrors the state
|
|
507
|
+
* tree — hierarchical indentation makes enter/exit traces readable at a glance.
|
|
508
|
+
*/
|
|
509
|
+
function preparePrinter(process, { prefix = "", print = console.log, lineNumbers = false }) {
|
|
510
|
+
let lineCounter = 0;
|
|
511
|
+
const shift = () => {
|
|
512
|
+
let prefix = "";
|
|
513
|
+
for (let s = process.state?.parent; s; s = s.parent) prefix += " ";
|
|
514
|
+
return prefix;
|
|
515
|
+
};
|
|
516
|
+
const getPrefix = lineNumbers ? () => `[${++lineCounter}]${shift()}` : shift;
|
|
517
|
+
const printer = (...args) => print(prefix, getPrefix(), ...args);
|
|
518
|
+
return printer;
|
|
506
519
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
520
|
+
/**
|
|
521
|
+
* Attach a printer to `process` (stored in a weak map so it is GC'd with the
|
|
522
|
+
* process). Handlers/tracers then reach it via {@link getProcessPrinter} /
|
|
523
|
+
* {@link getPrinter} rather than threading a logger through every call.
|
|
524
|
+
*/
|
|
525
|
+
function setProcessPrinter(process, config = {}) {
|
|
526
|
+
const printer = preparePrinter(process, config);
|
|
527
|
+
printerStore.set(process, printer);
|
|
510
528
|
}
|
|
511
|
-
|
|
512
|
-
|
|
529
|
+
/** The printer attached to `process`, or `console.log` if none was set. */
|
|
530
|
+
function getProcessPrinter(process) {
|
|
531
|
+
return printerStore.get(process) || console.log;
|
|
513
532
|
}
|
|
533
|
+
/** The printer for a state — its own if set, else its process's (see {@link getProcessPrinter}). */
|
|
514
534
|
function getPrinter(state) {
|
|
515
|
-
|
|
535
|
+
return printerStore.get(state) || getProcessPrinter(state.process);
|
|
516
536
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/trace/tracer.ts
|
|
539
|
+
/**
|
|
540
|
+
* Trace every state of `process`: as each state is created, attach a state tracer —
|
|
541
|
+
* so you can watch the machine move as a readable stream of enter/exit markers
|
|
542
|
+
* without editing any handler. Hooks `onStateCreate` and delegates to
|
|
543
|
+
* {@link setStateTracer}; pass a `print` sink to override the process printer.
|
|
544
|
+
*/
|
|
545
|
+
function setProcessTracer(process, print) {
|
|
546
|
+
return process.onStateCreate((state) => {
|
|
547
|
+
setStateTracer(state, print);
|
|
548
|
+
});
|
|
523
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Trace one state's lifecycle: emit `<key event="…">` on enter and `</key>` on exit
|
|
552
|
+
* (XML-like, so nested states read as nested tags). Falls back to the state's
|
|
553
|
+
* printer ({@link getPrinter}) when no `print` sink is given.
|
|
554
|
+
*/
|
|
524
555
|
function setStateTracer(state, print) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// src/utils/process.ts
|
|
538
|
-
function newProcess(config, {
|
|
539
|
-
prefix,
|
|
540
|
-
print,
|
|
541
|
-
lineNumbers = true
|
|
542
|
-
}) {
|
|
543
|
-
const process2 = new FsmProcess(config);
|
|
544
|
-
setProcessPrinter(process2, {
|
|
545
|
-
prefix,
|
|
546
|
-
print,
|
|
547
|
-
lineNumbers
|
|
548
|
-
});
|
|
549
|
-
setProcessTracer(process2);
|
|
550
|
-
return process2;
|
|
556
|
+
state.onEnter(() => {
|
|
557
|
+
(print || getPrinter(state))(`<${state?.key} event="${state.process.event}">`);
|
|
558
|
+
});
|
|
559
|
+
state.onExit(async () => {
|
|
560
|
+
await Promise.resolve().then(async () => {
|
|
561
|
+
(print || getPrinter(state))(`</${state.key}> <!-- event="${state.process.event}" -->`);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
551
564
|
}
|
|
565
|
+
//#endregion
|
|
566
|
+
export { EVENT_ANY, EVENT_EMPTY, FsmBaseClass, FsmProcess, FsmState, FsmStateDescriptor, KEY_DISPATCH, KEY_EVENT, KEY_STATES, KEY_TERMINATE, STATE_ANY, STATE_FINAL, STATE_INITIAL, STATUS_ENTER, STATUS_EXIT, STATUS_FINISHED, STATUS_FIRST, STATUS_LAST, STATUS_LEAF, STATUS_NEXT, STATUS_NONE, bindMethods, getPrinter, getProcessPrinter, getStateTransitions, isStateTransitionEnabled, preparePrinter, setProcessPrinter, setProcessTracer, setStateTracer, startFsmProcess, startProcess };
|
|
552
567
|
|
|
553
|
-
// src/orchestrator/is-generator.ts
|
|
554
|
-
function isGenerator(value) {
|
|
555
|
-
return typeof value === "object" && value !== null && "next" in value && typeof value.next === "function";
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// src/orchestrator/start-process.ts
|
|
559
|
-
async function startFsmProcess(context, config, load, startEvent = "") {
|
|
560
|
-
let terminated = false;
|
|
561
|
-
const ctx = context;
|
|
562
|
-
const process2 = new FsmProcess(config);
|
|
563
|
-
const statesStack = [];
|
|
564
|
-
process2.onStateCreate((state) => {
|
|
565
|
-
state.onEnter(async () => {
|
|
566
|
-
statesStack.push(state.key);
|
|
567
|
-
ctx[KEY_STATES] = [...statesStack];
|
|
568
|
-
ctx[KEY_EVENT] = process2.event;
|
|
569
|
-
const modules = await load(state.key, process2.event) ?? [];
|
|
570
|
-
for (const module of Array.isArray(modules) ? modules : [modules]) {
|
|
571
|
-
const result = await module?.(context);
|
|
572
|
-
if (isGenerator(result)) {
|
|
573
|
-
let stateExited = false;
|
|
574
|
-
state.onExit(() => {
|
|
575
|
-
stateExited = true;
|
|
576
|
-
result.return?.(void 0);
|
|
577
|
-
});
|
|
578
|
-
(async () => {
|
|
579
|
-
for await (const event of result) {
|
|
580
|
-
if (stateExited || terminated) {
|
|
581
|
-
break;
|
|
582
|
-
}
|
|
583
|
-
await dispatch(event);
|
|
584
|
-
}
|
|
585
|
-
})();
|
|
586
|
-
} else if (typeof result === "function") {
|
|
587
|
-
state.onExit(result);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
state.onExit(() => {
|
|
592
|
-
statesStack.pop();
|
|
593
|
-
ctx[KEY_STATES] = [...statesStack];
|
|
594
|
-
ctx[KEY_EVENT] = process2.event;
|
|
595
|
-
});
|
|
596
|
-
});
|
|
597
|
-
async function terminate() {
|
|
598
|
-
terminated = true;
|
|
599
|
-
await process2.shutdown();
|
|
600
|
-
}
|
|
601
|
-
async function dispatch(event) {
|
|
602
|
-
if (event !== void 0 && isStateTransitionEnabled(process2, event)) {
|
|
603
|
-
await process2.dispatch(event);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
ctx[KEY_DISPATCH] = dispatch;
|
|
607
|
-
ctx[KEY_TERMINATE] = terminate;
|
|
608
|
-
await process2.dispatch(startEvent);
|
|
609
|
-
return terminate;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// src/orchestrator/launcher.ts
|
|
613
|
-
function asArray(value, filter = (v) => v) {
|
|
614
|
-
if (value === void 0) return [];
|
|
615
|
-
const array = Array.isArray(value) ? value : [value];
|
|
616
|
-
return array.filter(Boolean).map(filter).filter(Boolean);
|
|
617
|
-
}
|
|
618
|
-
async function launcher(options) {
|
|
619
|
-
const configsManager = new ProcessConfigManager();
|
|
620
|
-
async function launch(processName, context, startEvent = "start") {
|
|
621
|
-
const config2 = configsManager.getProcessConfig(processName);
|
|
622
|
-
const load = configsManager.getHandlersLoader(processName);
|
|
623
|
-
return startFsmProcess(context, config2, load, startEvent);
|
|
624
|
-
}
|
|
625
|
-
function registerProcess(configsManager2, process2) {
|
|
626
|
-
if (!process2 || typeof process2 !== "object") {
|
|
627
|
-
throw new Error("Invalid process configuration: not an object");
|
|
628
|
-
}
|
|
629
|
-
const processName = process2.name;
|
|
630
|
-
if (!processName || typeof processName !== "string") {
|
|
631
|
-
throw new Error("Invalid process configuration: missing or invalid name");
|
|
632
|
-
}
|
|
633
|
-
const processConfig = process2.config;
|
|
634
|
-
if (processConfig && typeof processConfig !== "object") {
|
|
635
|
-
throw new Error(
|
|
636
|
-
`Invalid process configuration: config is not an object in process ${process2.name}`
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
if (processConfig) {
|
|
640
|
-
configsManager2.registerConfig(processName, processConfig);
|
|
641
|
-
}
|
|
642
|
-
const processHandlers = asArray(
|
|
643
|
-
process2.handlers ?? process2.handler ?? process2.default,
|
|
644
|
-
(v, idx) => {
|
|
645
|
-
if (typeof v === "function") return v;
|
|
646
|
-
if (typeof v === "object" && v !== null) return v;
|
|
647
|
-
throw new Error(
|
|
648
|
-
[
|
|
649
|
-
`Invalid process configuration:`,
|
|
650
|
-
`a process handler is not object nor function in process ${processName} at index ${idx}`,
|
|
651
|
-
`Recieved: ${v === null ? "null" : typeof v}`
|
|
652
|
-
].join("\n")
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
);
|
|
656
|
-
if (processHandlers.length > 0) {
|
|
657
|
-
configsManager2.registerHandlers(processName, ...processHandlers);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
const rootContext = { "fsm:launch": launch };
|
|
661
|
-
const init = typeof options === "function" ? options : async () => options;
|
|
662
|
-
const config = await init(rootContext);
|
|
663
|
-
if (typeof config !== "object" || !config) {
|
|
664
|
-
throw new Error("Invalid launcher configuration");
|
|
665
|
-
}
|
|
666
|
-
const processes = asArray(
|
|
667
|
-
config.processes ?? config.process ?? config.default,
|
|
668
|
-
(v) => {
|
|
669
|
-
if (typeof v === "function") {
|
|
670
|
-
return {
|
|
671
|
-
name: v.name || "",
|
|
672
|
-
handler: v
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
if (typeof v !== "object") {
|
|
676
|
-
throw new Error(
|
|
677
|
-
"Invalid launcher configuration: process is not an object"
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
return v;
|
|
681
|
-
}
|
|
682
|
-
);
|
|
683
|
-
const processesToStart = {};
|
|
684
|
-
for (const process2 of processes) {
|
|
685
|
-
const processName = process2.name;
|
|
686
|
-
const start = process2.start;
|
|
687
|
-
if (start === void 0) {
|
|
688
|
-
if (!(processName in processesToStart)) {
|
|
689
|
-
processesToStart[processName] = void 0;
|
|
690
|
-
}
|
|
691
|
-
} else {
|
|
692
|
-
processesToStart[processName] = start;
|
|
693
|
-
}
|
|
694
|
-
registerProcess(configsManager, process2);
|
|
695
|
-
}
|
|
696
|
-
const shutdowns = [];
|
|
697
|
-
let initContext = (context) => context;
|
|
698
|
-
const configContext = config.context ?? config.init;
|
|
699
|
-
if (configContext) {
|
|
700
|
-
if (typeof configContext === "function") {
|
|
701
|
-
initContext = configContext;
|
|
702
|
-
} else if (typeof configContext === "object") {
|
|
703
|
-
initContext = () => ({ parent: configContext });
|
|
704
|
-
} else {
|
|
705
|
-
throw new Error("Invalid launcher configuration: context");
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
for (const [processName, start] of Object.entries(processesToStart)) {
|
|
709
|
-
if (start === false) continue;
|
|
710
|
-
let context = {
|
|
711
|
-
parent: rootContext,
|
|
712
|
-
"fsm:name": processName
|
|
713
|
-
};
|
|
714
|
-
context = initContext(context) ?? context;
|
|
715
|
-
if (typeof context !== "object") {
|
|
716
|
-
throw new Error(
|
|
717
|
-
`Invalid launcher context for process "${processName}". Expected an object, but recieved ${typeof context}.`
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
const shutdown = await launch(processName, context);
|
|
721
|
-
shutdowns.push(shutdown);
|
|
722
|
-
}
|
|
723
|
-
return async () => {
|
|
724
|
-
for (const shutdown of shutdowns) {
|
|
725
|
-
await shutdown();
|
|
726
|
-
}
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// src/orchestrator/resolve-module-refs.ts
|
|
731
|
-
function resolveModulesUrls(baseUrl, ...refs) {
|
|
732
|
-
return refs.map((ref) => new URL(ref, baseUrl).href);
|
|
733
|
-
}
|
|
734
|
-
function resolveModulesPaths(baseUrl, ...refs) {
|
|
735
|
-
return refs.map((ref) => new URL(ref, baseUrl).pathname);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// src/orchestrator/start-processes.ts
|
|
739
|
-
async function startProcesses({
|
|
740
|
-
onExit,
|
|
741
|
-
modules,
|
|
742
|
-
init
|
|
743
|
-
}) {
|
|
744
|
-
const modulesList = [];
|
|
745
|
-
for (const module of modules) {
|
|
746
|
-
modulesList.push(
|
|
747
|
-
typeof module === "string" || module instanceof URL ? await import(String(module)) : module
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
return await launcher({
|
|
751
|
-
init,
|
|
752
|
-
processes: [
|
|
753
|
-
...modulesList,
|
|
754
|
-
// This function is called on exit at the end of the process chain
|
|
755
|
-
function Exit() {
|
|
756
|
-
return onExit;
|
|
757
|
-
}
|
|
758
|
-
]
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// src/orchestrator/start-node-processes.ts
|
|
763
|
-
async function startNodeProcesses(modules) {
|
|
764
|
-
try {
|
|
765
|
-
if (!modules) {
|
|
766
|
-
modules = process.argv.slice(2);
|
|
767
|
-
}
|
|
768
|
-
const terminate = await startProcesses({
|
|
769
|
-
modules: resolveModulesPaths(`file://${process.cwd()}/`, ...modules),
|
|
770
|
-
onExit: () => process.exit(0)
|
|
771
|
-
});
|
|
772
|
-
process.on("SIGINT", terminate);
|
|
773
|
-
process.on("SIGTERM", terminate);
|
|
774
|
-
process.stdin.resume();
|
|
775
|
-
} catch (err) {
|
|
776
|
-
console.error("Fatal error:", err);
|
|
777
|
-
process.exit(1);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// src/index.ts
|
|
782
|
-
var index_default = startNodeProcesses;
|
|
783
|
-
|
|
784
|
-
export { EVENT_ANY, EVENT_EMPTY, FsmProcess, FsmState, FsmStateDescriptor, KEY_DISPATCH, KEY_EVENT, KEY_LAUNCH_PROCESS, KEY_PRINTER, KEY_REGISTER_CONFIG, KEY_REGISTER_HANDLERS, KEY_STATES, KEY_TERMINATE, ProcessConfigManager, STATE_ANY, STATE_FINAL, STATE_INITIAL, STATUS_ENTER, STATUS_EXIT, STATUS_FINISHED, STATUS_FIRST, STATUS_LAST, STATUS_LEAF, STATUS_NEXT, STATUS_NONE, index_default as default, getPrinter, getProcessPrinter, getStateTransitions, isStateTransitionEnabled, launcher, newFsmProcess, newProcess, preparePrinter, resolveModulesPaths, resolveModulesUrls, setPrinter, setProcessPrinter, setProcessTracer, setStateTracer, startFsmProcess, startNodeProcesses, startProcesses, toStageHandlers };
|
|
785
|
-
//# sourceMappingURL=index.js.map
|
|
786
568
|
//# sourceMappingURL=index.js.map
|