@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
|
@@ -5,10 +5,24 @@ import {
|
|
|
5
5
|
STATE_FINAL,
|
|
6
6
|
} from "./fsm-state-config.ts";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The *compiled* form of an `FsmStateConfig` subtree.
|
|
10
|
+
*
|
|
11
|
+
* Resolving a transition happens on every event, so the flat `[from, event, to]`
|
|
12
|
+
* tuple list is pre-indexed once into nested maps for O(1) lookup, and the
|
|
13
|
+
* wildcard-fallback order is applied here rather than re-derived on each dispatch.
|
|
14
|
+
* The shape is `transitions[from][event] = to` plus a `states` map of child
|
|
15
|
+
* descriptors. `FsmProcess` builds one from your config in its constructor; you
|
|
16
|
+
* rarely construct a descriptor directly.
|
|
17
|
+
*/
|
|
8
18
|
export class FsmStateDescriptor {
|
|
9
19
|
transitions: Record<string, Record<string, string>> = {};
|
|
10
20
|
states: Record<string, FsmStateDescriptor> = {};
|
|
11
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Recursively compile a config (and its nested `states`) into descriptors,
|
|
24
|
+
* turning the tuple list into the indexed `transitions` map.
|
|
25
|
+
*/
|
|
12
26
|
static build(config: FsmStateConfig) {
|
|
13
27
|
const descriptor = new FsmStateDescriptor();
|
|
14
28
|
for (const [from, event, to] of config.transitions || []) {
|
|
@@ -27,6 +41,13 @@ export class FsmStateDescriptor {
|
|
|
27
41
|
return descriptor;
|
|
28
42
|
}
|
|
29
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the target state for a `(stateKey, eventKey)` pair. Tries the most
|
|
46
|
+
* specific match first, then falls back through the wildcards —
|
|
47
|
+
* `(state, event)` → `(*, event)` → `(state, *)` → `(*, *)` — and returns
|
|
48
|
+
* `STATE_FINAL` if nothing matches, so an unmatched event exits the state rather
|
|
49
|
+
* than silently doing nothing.
|
|
50
|
+
*/
|
|
30
51
|
getTargetStateKey(stateKey: string, eventKey: string) {
|
|
31
52
|
const pairs = [
|
|
32
53
|
[stateKey, eventKey],
|
package/src/core/fsm-state.ts
CHANGED
|
@@ -2,25 +2,41 @@ import { bindMethods, FsmBaseClass } from "./fsm-base-class.ts";
|
|
|
2
2
|
import type { FsmProcess } from "./fsm-process.ts";
|
|
3
3
|
import type { FsmStateDescriptor } from "./fsm-state-descriptor.ts";
|
|
4
4
|
|
|
5
|
+
/** Serialized form of a single state: its `key` plus the `data` bag its `dump` hooks filled. */
|
|
5
6
|
export type FsmStateDump = Record<string, unknown> & {
|
|
6
7
|
key: string;
|
|
7
8
|
data: Record<string, unknown>;
|
|
8
9
|
};
|
|
10
|
+
/** An `onEnter` / `onExit` callback; receives the state it fired on. */
|
|
9
11
|
export type FsmStateHandler = (
|
|
10
12
|
state: FsmState,
|
|
11
13
|
...args: unknown[]
|
|
12
14
|
) => void | Promise<void>;
|
|
13
15
|
|
|
16
|
+
/** A `dump` / `restore` callback; reads or fills the mutable per-state `data` bag. */
|
|
14
17
|
export type FsmStateDumpHandler = (
|
|
15
18
|
state: FsmState,
|
|
16
19
|
dump: FsmStateDump,
|
|
17
20
|
) => void | Promise<void>;
|
|
18
21
|
|
|
22
|
+
/** An `onStateError` callback; receives the error thrown by another handler on this state. */
|
|
19
23
|
export type FsmStateErrorHandler = (
|
|
20
24
|
state: FsmState,
|
|
21
25
|
error: unknown,
|
|
22
26
|
) => void | Promise<void>;
|
|
23
27
|
|
|
28
|
+
/**
|
|
29
|
+
* One live node in a running machine's active-state stack.
|
|
30
|
+
*
|
|
31
|
+
* A state needs a place to attach behaviour and record data while it is active.
|
|
32
|
+
* `FsmState` is that handle — created by the engine each time a state is entered,
|
|
33
|
+
* discarded when it exits — so handlers can capture per-activation closures instead
|
|
34
|
+
* of sharing mutable machine-wide state. It offers lifecycle hooks `onEnter` /
|
|
35
|
+
* `onExit` / `onStateError`, serialization hooks `dump` / `restore`, and the tree
|
|
36
|
+
* links `key` / `parent` / `descriptor`; every hook method returns a disposer.
|
|
37
|
+
* Attach hooks from within `FsmProcess.onStateCreate((state) => …)`, or let
|
|
38
|
+
* `startProcess` install them for you from a `load` callback.
|
|
39
|
+
*/
|
|
24
40
|
export class FsmState extends FsmBaseClass {
|
|
25
41
|
process: FsmProcess;
|
|
26
42
|
key: string;
|
|
@@ -38,44 +54,29 @@ export class FsmState extends FsmBaseClass {
|
|
|
38
54
|
this.key = key;
|
|
39
55
|
this.parent = parent;
|
|
40
56
|
this.descriptor = descriptor;
|
|
41
|
-
bindMethods(
|
|
42
|
-
this,
|
|
43
|
-
"onEnter",
|
|
44
|
-
"onExit",
|
|
45
|
-
"dump",
|
|
46
|
-
"restore",
|
|
47
|
-
"useData",
|
|
48
|
-
"onStateError",
|
|
49
|
-
);
|
|
57
|
+
bindMethods(this, "onEnter", "onExit", "dump", "restore", "onStateError");
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
/** Run when this state is entered. */
|
|
52
61
|
onEnter(handler: FsmStateHandler) {
|
|
53
62
|
return this._addHandler("onEnter", handler, true);
|
|
54
63
|
}
|
|
64
|
+
/** Run when this state exits — registered inner-first so unwinding is inner-to-outer. */
|
|
55
65
|
onExit(handler: FsmStateHandler) {
|
|
56
66
|
return this._addHandler("onExit", handler, false);
|
|
57
67
|
}
|
|
68
|
+
/** Handle an error thrown by another handler on this state (also bubbles to the process). */
|
|
58
69
|
onStateError(handler: FsmStateErrorHandler) {
|
|
59
70
|
return this._addHandler("onStateError", handler);
|
|
60
71
|
}
|
|
72
|
+
/** Contribute to this state's snapshot: fill `dump.data` when the process is dumped. */
|
|
61
73
|
dump(handler: FsmStateDumpHandler) {
|
|
62
74
|
return this._addHandler("dump", handler, true);
|
|
63
75
|
}
|
|
76
|
+
/** Rehydrate from this state's snapshot: read `dump.data` when the process is restored. */
|
|
64
77
|
restore(handler: FsmStateDumpHandler) {
|
|
65
78
|
return this._addHandler("restore", handler, true);
|
|
66
79
|
}
|
|
67
|
-
getData<T>(key: string, recursive: boolean = true): T | undefined {
|
|
68
|
-
return (
|
|
69
|
-
(this.data[key] as T) ??
|
|
70
|
-
(recursive ? this.parent?.getData<T>(key, recursive) : undefined)
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
useData<T>(key: string) {
|
|
74
|
-
return [
|
|
75
|
-
(recursive: boolean = true) => this.getData<T>(key, recursive),
|
|
76
|
-
(value: T) => this.setData(key, value),
|
|
77
|
-
];
|
|
78
|
-
}
|
|
79
80
|
|
|
80
81
|
async _handleError(error: Error | unknown) {
|
|
81
82
|
await this._runHandler("onStateError", this, error);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { FsmProcess } from "./fsm-process.ts";
|
|
2
|
+
import type { FsmState } from "./fsm-state.ts";
|
|
3
|
+
import type { FsmStateDescriptor } from "./fsm-state-descriptor.ts";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Transition introspection — read-only queries over the state/transition graph
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List the transitions currently reachable from `state` — for a UI or viewer that
|
|
11
|
+
* needs to know which events can fire right now (e.g. to render only the enabled
|
|
12
|
+
* buttons). Collects `[from, event, to]` tuples by walking up the parent chain; the
|
|
13
|
+
* nearest state's rule for an event wins, so an outer fallback is masked by an inner
|
|
14
|
+
* override. The returned tuples are ordered outer→inner (root first).
|
|
15
|
+
*/
|
|
16
|
+
export function getStateTransitions(
|
|
17
|
+
state?: FsmState,
|
|
18
|
+
): [from: string, event: string, to: string][] {
|
|
19
|
+
const result: [from: string, event: string, to: string][] = [];
|
|
20
|
+
const index: Record<string, boolean> = {};
|
|
21
|
+
if (state) {
|
|
22
|
+
let prevStateKey = state.key;
|
|
23
|
+
for (let parent = state.parent; parent; parent = parent.parent) {
|
|
24
|
+
if (!parent.descriptor) continue;
|
|
25
|
+
result.push(
|
|
26
|
+
...getTransitionsFromDescriptor(parent.descriptor, prevStateKey, index),
|
|
27
|
+
);
|
|
28
|
+
prevStateKey = parent.key;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result.reverse();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getTransitionsFromDescriptor(
|
|
35
|
+
descriptor: FsmStateDescriptor,
|
|
36
|
+
prevStateKey: string,
|
|
37
|
+
index: Record<string, boolean>,
|
|
38
|
+
): [from: string, event: string, to: string][] {
|
|
39
|
+
const result: [from: string, event: string, to: string][] = [];
|
|
40
|
+
const prevStateKeys = [prevStateKey, "*"];
|
|
41
|
+
for (const prevKey of prevStateKeys) {
|
|
42
|
+
const targets = descriptor.transitions[prevKey];
|
|
43
|
+
if (targets) {
|
|
44
|
+
for (const [event, target] of Object.entries(targets)) {
|
|
45
|
+
if (index[event]) continue;
|
|
46
|
+
// Mark the event seen regardless of target — an inner exit-to-final
|
|
47
|
+
// (`to === ""`, a falsy target) must still mask an outer rule for the
|
|
48
|
+
// same event, matching what `dispatch` actually resolves.
|
|
49
|
+
index[event] = true;
|
|
50
|
+
result.push([prevStateKey, event, target]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Guard: would `event` trigger any transition in the process's current state? Used
|
|
59
|
+
* to avoid dispatching dead events — the runner calls it before `dispatch`, and
|
|
60
|
+
* consumers use it to enable/disable controls. Returns `true` iff `event` appears
|
|
61
|
+
* among `getStateTransitions(process.state)`.
|
|
62
|
+
*/
|
|
63
|
+
export function isStateTransitionEnabled(
|
|
64
|
+
process: FsmProcess,
|
|
65
|
+
event: string,
|
|
66
|
+
): boolean {
|
|
67
|
+
const transitions = getStateTransitions(process.state);
|
|
68
|
+
for (const [, ev] of transitions) {
|
|
69
|
+
if (ev === event) return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export * from "./fsm-base-class.ts";
|
|
1
2
|
export * from "./fsm-process.ts";
|
|
2
3
|
export * from "./fsm-state.ts";
|
|
3
4
|
export * from "./fsm-state-config.ts";
|
|
4
5
|
export * from "./fsm-state-descriptor.ts";
|
|
5
|
-
export * from "./
|
|
6
|
+
export * from "./fsm-transitions.ts";
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
export * from "./core/index.ts";
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
4
|
-
|
|
5
|
-
import { startNodeProcesses } from "./orchestrator/start-node-processes.ts";
|
|
6
|
-
export default startNodeProcesses;
|
|
2
|
+
export * from "./start-process.ts";
|
|
3
|
+
export * from "./trace/index.ts";
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { FsmProcess, type FsmProcessDump } from "./core/fsm-process.ts";
|
|
2
|
+
import type { FsmState } from "./core/fsm-state.ts";
|
|
3
|
+
import type { FsmStateConfig } from "./core/fsm-state-config.ts";
|
|
4
|
+
import { isStateTransitionEnabled } from "./core/fsm-transitions.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-state behaviour contract used by `startProcess`. One function shape expresses
|
|
8
|
+
* what to do while in a state, and its *return value* wires up the rest, so a state's
|
|
9
|
+
* setup and teardown live together. The handler runs on entry with the shared
|
|
10
|
+
* `context` and may return:
|
|
11
|
+
* - nothing — no teardown;
|
|
12
|
+
* - a cleanup `function` — registered as this state's `onExit`;
|
|
13
|
+
* - an async/sync generator — run concurrently, each yielded string dispatched back
|
|
14
|
+
* into the machine (self-driving/reactive states), auto-`return()`ed on exit.
|
|
15
|
+
*/
|
|
16
|
+
export type StageHandler<C = Record<string, unknown>> = (
|
|
17
|
+
context: C,
|
|
18
|
+
) =>
|
|
19
|
+
| void
|
|
20
|
+
| (() => void | Promise<void>)
|
|
21
|
+
| Promise<void | (() => void | Promise<void>)>
|
|
22
|
+
| AsyncGenerator<string, void, unknown>
|
|
23
|
+
| Generator<string, void, unknown>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The caller's remote control returned by `startProcess`: stop the machine and
|
|
27
|
+
* snapshot/rehydrate it without holding a reference to the underlying `FsmProcess`.
|
|
28
|
+
*/
|
|
29
|
+
export interface ProcessHandle {
|
|
30
|
+
shutdown(): Promise<void>;
|
|
31
|
+
dump(...args: unknown[]): Promise<FsmProcessDump>;
|
|
32
|
+
restore(dump: FsmProcessDump, ...args: unknown[]): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Context keys under which `startProcess` binds the machine into the shared context
|
|
36
|
+
// object. Why via context (not closures): handlers reach the running machine
|
|
37
|
+
// uniformly — `(ctx[KEY_DISPATCH])(event)` — regardless of where they are defined.
|
|
38
|
+
/** Context key: `(event) => Promise<void>` that dispatches an event (guarded). */
|
|
39
|
+
export const KEY_DISPATCH = "fsm:dispatch";
|
|
40
|
+
/** Context key: `() => Promise<void>` that shuts the machine down. */
|
|
41
|
+
export const KEY_TERMINATE = "fsm:terminate";
|
|
42
|
+
/** Context key: the current state-key stack (root→leaf), refreshed on every transition. */
|
|
43
|
+
export const KEY_STATES = "fsm:states";
|
|
44
|
+
/** Context key: the last dispatched event. */
|
|
45
|
+
export const KEY_EVENT = "fsm:event";
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Generator detection
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
function isGenerator(
|
|
52
|
+
value: unknown,
|
|
53
|
+
): value is
|
|
54
|
+
| Generator<string, void, unknown>
|
|
55
|
+
| AsyncGenerator<string, void, unknown> {
|
|
56
|
+
return (
|
|
57
|
+
typeof value === "object" &&
|
|
58
|
+
value !== null &&
|
|
59
|
+
"next" in value &&
|
|
60
|
+
typeof (value as { next: unknown }).next === "function"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// startProcess (also exported as startFsmProcess for backward compat)
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ergonomic runner: build a machine, attach behaviour via one `load` callback, and
|
|
70
|
+
* bind the machine into `context`.
|
|
71
|
+
*
|
|
72
|
+
* Doing this by hand (`new FsmProcess` + `onStateCreate` + `onEnter` + a loader +
|
|
73
|
+
* context wiring) is boilerplate every consumer repeats, so `startProcess` does it
|
|
74
|
+
* once — most callers use this instead of the raw engine. It creates the process; on
|
|
75
|
+
* each state entry calls `load(stateKey, event)` and installs the returned
|
|
76
|
+
* `StageHandler`s (their return values become `onExit` cleanups or event-yielding
|
|
77
|
+
* generators — see `StageHandler`); binds `KEY_DISPATCH` / `KEY_TERMINATE` /
|
|
78
|
+
* `KEY_STATES` / `KEY_EVENT` into `context`; dispatches `startEvent` to enter the
|
|
79
|
+
* initial state; and returns a `ProcessHandle`.
|
|
80
|
+
*
|
|
81
|
+
* @param context shared object handlers read/write and the machine is bound into
|
|
82
|
+
* @param config the declarative machine definition
|
|
83
|
+
* @param load returns the handler(s) to run for a given `(stateKey, event)`
|
|
84
|
+
* @param startEvent initial event (default `""`, the eventless start)
|
|
85
|
+
*/
|
|
86
|
+
export async function startProcess<C = unknown>(
|
|
87
|
+
context: C,
|
|
88
|
+
config: FsmStateConfig,
|
|
89
|
+
load: (
|
|
90
|
+
state: string,
|
|
91
|
+
event: string | undefined,
|
|
92
|
+
) => StageHandler<C>[] | Promise<StageHandler<C>[]>,
|
|
93
|
+
startEvent = "",
|
|
94
|
+
): Promise<ProcessHandle> {
|
|
95
|
+
let terminated = false;
|
|
96
|
+
const ctx = context as Record<string, unknown>;
|
|
97
|
+
const process = new FsmProcess(config);
|
|
98
|
+
const statesStack: string[] = [];
|
|
99
|
+
|
|
100
|
+
process.onStateCreate((state: FsmState) => {
|
|
101
|
+
state.onEnter(async () => {
|
|
102
|
+
statesStack.push(state.key);
|
|
103
|
+
ctx[KEY_STATES] = [...statesStack];
|
|
104
|
+
ctx[KEY_EVENT] = process.event;
|
|
105
|
+
|
|
106
|
+
const modules = (await load(state.key, process.event)) ?? [];
|
|
107
|
+
for (const module of Array.isArray(modules) ? modules : [modules]) {
|
|
108
|
+
const result = await module?.(context);
|
|
109
|
+
if (isGenerator(result)) {
|
|
110
|
+
let stateExited = false;
|
|
111
|
+
state.onExit(() => {
|
|
112
|
+
stateExited = true;
|
|
113
|
+
result.return?.(undefined as never);
|
|
114
|
+
});
|
|
115
|
+
(async () => {
|
|
116
|
+
try {
|
|
117
|
+
for await (const event of result) {
|
|
118
|
+
if (stateExited || terminated) break;
|
|
119
|
+
await dispatch(event);
|
|
120
|
+
if (stateExited || terminated) break;
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// A generator `return()` during state exit / termination throws an
|
|
124
|
+
// abort we intentionally swallow; a genuine error thrown by the
|
|
125
|
+
// generator body must surface through the state's error handling
|
|
126
|
+
// rather than vanish.
|
|
127
|
+
if (!stateExited && !terminated) await state._handleError(error);
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
} else if (typeof result === "function") {
|
|
131
|
+
state.onExit(result as () => void | Promise<void>);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
state.onExit(() => {
|
|
136
|
+
statesStack.pop();
|
|
137
|
+
ctx[KEY_STATES] = [...statesStack];
|
|
138
|
+
ctx[KEY_EVENT] = process.event;
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
async function dispatch(event: string): Promise<void> {
|
|
143
|
+
if (event !== undefined && isStateTransitionEnabled(process, event)) {
|
|
144
|
+
await process.dispatch(event);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function terminate(): Promise<void> {
|
|
148
|
+
terminated = true;
|
|
149
|
+
await process.shutdown();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function dumpProcess(...args: unknown[]): Promise<FsmProcessDump> {
|
|
153
|
+
return process.dump(...args);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function restoreProcess(
|
|
157
|
+
dumpData: FsmProcessDump,
|
|
158
|
+
...args: unknown[]
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
statesStack.length = 0;
|
|
161
|
+
terminated = false;
|
|
162
|
+
await process.restore(dumpData, ...args);
|
|
163
|
+
for (
|
|
164
|
+
let state: FsmState | undefined = process.state;
|
|
165
|
+
state;
|
|
166
|
+
state = state.parent
|
|
167
|
+
) {
|
|
168
|
+
statesStack.unshift(state.key);
|
|
169
|
+
}
|
|
170
|
+
ctx[KEY_STATES] = [...statesStack];
|
|
171
|
+
ctx[KEY_EVENT] = process.event;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ctx[KEY_DISPATCH] = dispatch;
|
|
175
|
+
ctx[KEY_TERMINATE] = terminate;
|
|
176
|
+
await process.dispatch(startEvent);
|
|
177
|
+
return {
|
|
178
|
+
shutdown: terminate,
|
|
179
|
+
dump: dumpProcess,
|
|
180
|
+
restore: restoreProcess,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Permanent equal alias of {@link startProcess} — the name existing consumers import. */
|
|
185
|
+
export const startFsmProcess = startProcess;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { FsmProcess } from "../core/fsm-process.ts";
|
|
2
|
+
|
|
3
|
+
/** A sink for trace output — anything shaped like `console.log`. */
|
|
4
|
+
export type Printer = (...args: unknown[]) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Tuning for a `Printer`.
|
|
7
|
+
* - `prefix` — string prepended to every line (e.g. a process tag).
|
|
8
|
+
* - `print` — the underlying sink (defaults to `console.log`).
|
|
9
|
+
* - `lineNumbers` — prepend an incrementing `[n]` counter.
|
|
10
|
+
*/
|
|
11
|
+
export type PrinterConfig = {
|
|
12
|
+
prefix?: string;
|
|
13
|
+
print?: (...args: unknown[]) => void;
|
|
14
|
+
lineNumbers?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const printerStore = new WeakMap<object, Printer>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build a `Printer` bound to `process` that indents each line by the current state
|
|
21
|
+
* nesting depth (two spaces per level), so log output visually mirrors the state
|
|
22
|
+
* tree — hierarchical indentation makes enter/exit traces readable at a glance.
|
|
23
|
+
*/
|
|
24
|
+
export function preparePrinter(
|
|
25
|
+
process: FsmProcess,
|
|
26
|
+
{ prefix = "", print = console.log, lineNumbers = false }: PrinterConfig,
|
|
27
|
+
): Printer {
|
|
28
|
+
let lineCounter = 0;
|
|
29
|
+
const shift = () => {
|
|
30
|
+
let prefix = "";
|
|
31
|
+
for (let s = process.state?.parent; s; s = s.parent) {
|
|
32
|
+
prefix += " ";
|
|
33
|
+
}
|
|
34
|
+
return prefix;
|
|
35
|
+
};
|
|
36
|
+
const getPrefix = lineNumbers ? () => `[${++lineCounter}]${shift()}` : shift;
|
|
37
|
+
const printer = (...args: unknown[]) => print(prefix, getPrefix(), ...args);
|
|
38
|
+
return printer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attach a printer to `process` (stored in a weak map so it is GC'd with the
|
|
43
|
+
* process). Handlers/tracers then reach it via {@link getProcessPrinter} /
|
|
44
|
+
* {@link getPrinter} rather than threading a logger through every call.
|
|
45
|
+
*/
|
|
46
|
+
export function setProcessPrinter(
|
|
47
|
+
process: FsmProcess,
|
|
48
|
+
config: PrinterConfig = {},
|
|
49
|
+
) {
|
|
50
|
+
const printer = preparePrinter(process, config);
|
|
51
|
+
printerStore.set(process, printer);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The printer attached to `process`, or `console.log` if none was set. */
|
|
55
|
+
export function getProcessPrinter(process: FsmProcess): Printer {
|
|
56
|
+
return printerStore.get(process) || console.log;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The printer for a state — its own if set, else its process's (see {@link getProcessPrinter}). */
|
|
60
|
+
export function getPrinter(state: { process: FsmProcess }): Printer {
|
|
61
|
+
return printerStore.get(state) || getProcessPrinter(state.process);
|
|
62
|
+
}
|
|
@@ -2,12 +2,23 @@ import type { FsmProcess } from "../core/fsm-process.ts";
|
|
|
2
2
|
import type { FsmState } from "../core/fsm-state.ts";
|
|
3
3
|
import { getPrinter, type Printer } from "./printer.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Trace every state of `process`: as each state is created, attach a state tracer —
|
|
7
|
+
* so you can watch the machine move as a readable stream of enter/exit markers
|
|
8
|
+
* without editing any handler. Hooks `onStateCreate` and delegates to
|
|
9
|
+
* {@link setStateTracer}; pass a `print` sink to override the process printer.
|
|
10
|
+
*/
|
|
5
11
|
export function setProcessTracer(process: FsmProcess, print?: Printer) {
|
|
6
|
-
return process.onStateCreate((state) => {
|
|
12
|
+
return process.onStateCreate((state: FsmState) => {
|
|
7
13
|
setStateTracer(state, print);
|
|
8
14
|
});
|
|
9
15
|
}
|
|
10
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Trace one state's lifecycle: emit `<key event="…">` on enter and `</key>` on exit
|
|
19
|
+
* (XML-like, so nested states read as nested tags). Falls back to the state's
|
|
20
|
+
* printer ({@link getPrinter}) when no `print` sink is given.
|
|
21
|
+
*/
|
|
11
22
|
export function setStateTracer(state: FsmState, print?: Printer) {
|
|
12
23
|
state.onEnter(() => {
|
|
13
24
|
const printLine = print || getPrinter(state);
|
package/CHANGELOG.md
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# @statewalker/fsm
|
|
2
|
-
|
|
3
|
-
## 0.16.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- Fix logical bug in transitions loading
|
|
8
|
-
|
|
9
|
-
## 0.15.2
|
|
10
|
-
|
|
11
|
-
### Patch Changes
|
|
12
|
-
|
|
13
|
-
- Update rollup and dependencies
|
|
14
|
-
- Updated dependencies
|
|
15
|
-
- @statewalker/tree@0.10.2
|
|
16
|
-
|
|
17
|
-
## 0.15.1
|
|
18
|
-
|
|
19
|
-
### Patch Changes
|
|
20
|
-
|
|
21
|
-
- Normalize package.json files
|
|
22
|
-
- Updated dependencies
|
|
23
|
-
- @statewalker/tree@0.10.1
|
|
24
|
-
|
|
25
|
-
## 0.15.0
|
|
26
|
-
|
|
27
|
-
### Minor Changes
|
|
28
|
-
|
|
29
|
-
- Migrage the "initAsyncProcess" method from the @statewalker/fsm to @statewalker/fsm-process package and add process utility methods
|
|
30
|
-
|
|
31
|
-
## 0.14.0
|
|
32
|
-
|
|
33
|
-
### Minor Changes
|
|
34
|
-
|
|
35
|
-
- Update exports
|
|
36
|
-
|
|
37
|
-
## 0.13.0
|
|
38
|
-
|
|
39
|
-
### Minor Changes
|
|
40
|
-
|
|
41
|
-
- Fix exports
|
|
42
|
-
|
|
43
|
-
## 0.12.0
|
|
44
|
-
|
|
45
|
-
### Minor Changes
|
|
46
|
-
|
|
47
|
-
- Update version
|
|
48
|
-
|
|
49
|
-
## 0.11.0
|
|
50
|
-
|
|
51
|
-
### Minor Changes
|
|
52
|
-
|
|
53
|
-
- Add initialization function for async processes
|
|
54
|
-
|
|
55
|
-
## 0.10.1
|
|
56
|
-
|
|
57
|
-
### Patch Changes
|
|
58
|
-
|
|
59
|
-
- Migrate the @statewalker/fsm project from the @statewalker/statewalker monorepo
|
package/bin/cli.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { isStateTransitionEnabled } from "../utils/transitions.ts";
|
|
2
|
-
import { FsmProcess } from "./fsm-process.ts";
|
|
3
|
-
import type { FsmStateConfig } from "./fsm-state-config.ts";
|
|
4
|
-
|
|
5
|
-
export type Module<C = unknown> = (context: C) =>
|
|
6
|
-
| void
|
|
7
|
-
| (() => void | Promise<void>)
|
|
8
|
-
| Promise<void | (() => void | Promise<void>)>
|
|
9
|
-
// Event emitters / triggers
|
|
10
|
-
| Generator<string>
|
|
11
|
-
| AsyncGenerator<string>;
|
|
12
|
-
|
|
13
|
-
export function newFsmProcess<C>(
|
|
14
|
-
context: C,
|
|
15
|
-
config: FsmStateConfig,
|
|
16
|
-
load: (
|
|
17
|
-
state: string,
|
|
18
|
-
event: undefined | string,
|
|
19
|
-
) =>
|
|
20
|
-
| undefined
|
|
21
|
-
| Module<C>
|
|
22
|
-
| Module<C>[]
|
|
23
|
-
| Promise<undefined | Module<C> | Module<C>[]>,
|
|
24
|
-
): [
|
|
25
|
-
dispatch: (event: string) => Promise<boolean>,
|
|
26
|
-
shutdown: () => Promise<void>,
|
|
27
|
-
] {
|
|
28
|
-
let started = false;
|
|
29
|
-
let terminated = false;
|
|
30
|
-
const process = new FsmProcess(config);
|
|
31
|
-
async function dispatch(event: string): Promise<boolean> {
|
|
32
|
-
return terminated ||
|
|
33
|
-
(event !== undefined &&
|
|
34
|
-
(!started || isStateTransitionEnabled(process, event)))
|
|
35
|
-
? process.dispatch(event)
|
|
36
|
-
: false;
|
|
37
|
-
}
|
|
38
|
-
process.onStateCreate((state) => {
|
|
39
|
-
started = true;
|
|
40
|
-
state.onEnter(async () => {
|
|
41
|
-
const modules = (await load(state.key, process.event)) ?? [];
|
|
42
|
-
for (const module of Array.isArray(modules) ? modules : [modules]) {
|
|
43
|
-
const result = await module?.(context);
|
|
44
|
-
if (isGenerator(result)) {
|
|
45
|
-
state.onExit(() => {
|
|
46
|
-
result.return?.(void 0);
|
|
47
|
-
});
|
|
48
|
-
(async () => {
|
|
49
|
-
for await (const event of result) {
|
|
50
|
-
if (terminated) {
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
await dispatch(event);
|
|
54
|
-
if (terminated) {
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
})();
|
|
59
|
-
} else if (typeof result === "function") {
|
|
60
|
-
state.onExit(result);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
async function shutdown(): Promise<void> {
|
|
66
|
-
await process.shutdown();
|
|
67
|
-
terminated = true;
|
|
68
|
-
}
|
|
69
|
-
return [dispatch, shutdown] as const;
|
|
70
|
-
|
|
71
|
-
function isGenerator(
|
|
72
|
-
value: unknown,
|
|
73
|
-
): value is
|
|
74
|
-
| Generator<string, void, unknown>
|
|
75
|
-
| AsyncGenerator<string, void, unknown> {
|
|
76
|
-
return (
|
|
77
|
-
typeof value === "object" &&
|
|
78
|
-
value !== null &&
|
|
79
|
-
"next" in value &&
|
|
80
|
-
typeof (value as Generator<string, void, unknown>).next === "function"
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
}
|