@statewalker/fsm 0.35.0 → 0.37.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.
Files changed (41) hide show
  1. package/README.md +112 -0
  2. package/bin/cli.js +1 -1
  3. package/dist/bin/node-runner-UJp8T_oP.d.ts +16 -0
  4. package/dist/bin/node-runner-UJp8T_oP.d.ts.map +1 -0
  5. package/dist/bin/node-runner.js +40 -0
  6. package/dist/bin/node-runner.js.map +1 -0
  7. package/dist/index-vTjnkbGW.d.ts +129 -0
  8. package/dist/index-vTjnkbGW.d.ts.map +1 -0
  9. package/dist/index.js +43 -719
  10. package/dist/index.js.map +1 -1
  11. package/dist/launcher-6VUDviTj.d.ts +48 -0
  12. package/dist/launcher-6VUDviTj.d.ts.map +1 -0
  13. package/dist/launcher-CASrMAOa.js +443 -0
  14. package/dist/launcher-CASrMAOa.js.map +1 -0
  15. package/package.json +8 -8
  16. package/src/bin/node-runner.ts +57 -0
  17. package/src/core/fsm-base-class.ts +1 -26
  18. package/src/core/fsm-process.ts +14 -11
  19. package/src/core/fsm-state.ts +1 -21
  20. package/src/core/index.ts +1 -1
  21. package/src/index.ts +1 -5
  22. package/src/orchestrator/handler-registry.ts +108 -0
  23. package/src/orchestrator/index.ts +15 -9
  24. package/src/orchestrator/launcher.ts +42 -141
  25. package/src/orchestrator/start-process.ts +141 -26
  26. package/src/utils/index.ts +0 -2
  27. package/src/utils/printer.ts +5 -11
  28. package/src/utils/tracer.ts +1 -1
  29. package/CHANGELOG.md +0 -59
  30. package/dist/index.d.ts +0 -310
  31. package/src/core/new-fsm-process.ts +0 -83
  32. package/src/orchestrator/constants.ts +0 -130
  33. package/src/orchestrator/is-generator.ts +0 -12
  34. package/src/orchestrator/process-config-manager.ts +0 -70
  35. package/src/orchestrator/resolve-module-refs.ts +0 -52
  36. package/src/orchestrator/start-node-processes.ts +0 -19
  37. package/src/orchestrator/start-processes.ts +0 -32
  38. package/src/orchestrator/types.ts +0 -34
  39. package/src/utils/handlers.ts +0 -35
  40. package/src/utils/process.ts +0 -26
  41. package/src/utils/transitions.ts +0 -56
@@ -1,167 +1,68 @@
1
1
  import type { FsmStateConfig } from "../core/fsm-state-config.ts";
2
- import { ProcessConfigManager } from "./process-config-manager.ts";
3
- import { startFsmProcess } from "./start-process.ts";
4
- import type { StageHandler } from "./types.ts";
2
+ import type { StageHandler } from "./handler-registry.ts";
3
+ import { createHandlerRegistry } from "./handler-registry.ts";
4
+ import { startProcess } from "./start-process.ts";
5
5
 
6
- export type ProcessConfig = {
7
- name: string;
8
- config?: FsmStateConfig;
9
- } & (
10
- | {
11
- handler?: StageHandler;
12
- }
13
- | {
14
- handlers?:
15
- | StageHandler
16
- | (
17
- | StageHandler
18
- | {
19
- [state: string]: StageHandler | StageHandler[];
20
- }
21
- )[];
22
- }
23
- | {
24
- default?:
25
- | StageHandler
26
- | (
27
- | StageHandler
28
- | {
29
- [state: string]: StageHandler | StageHandler[];
30
- }
31
- )[];
32
- }
33
- );
6
+ export const KEY_LAUNCH_PROCESS = "fsm:launch";
34
7
 
35
- function asArray<T>(
36
- value: T | T[] | undefined,
37
- filter: (v: T, idx: number) => T | undefined = (v) => v,
38
- ): T[] {
39
- if (value === undefined) return [];
40
- const array = Array.isArray(value) ? value : [value];
41
- return array.filter(Boolean).map(filter).filter(Boolean) as T[];
8
+ export interface LauncherConfig {
9
+ processes: ProcessDef[];
10
+ start?: string[];
11
+ context?: (parent: Record<string, unknown>) => Record<string, unknown>;
42
12
  }
43
13
 
44
- export async function launcher(options: unknown) {
45
- const configsManager = new ProcessConfigManager();
14
+ export interface ProcessDef {
15
+ name: string;
16
+ config: FsmStateConfig;
17
+ handlers?: (StageHandler | Record<string, StageHandler | StageHandler[]>)[];
18
+ start?: boolean;
19
+ }
20
+
21
+ export async function launcher(
22
+ config: LauncherConfig,
23
+ ): Promise<() => Promise<void>> {
24
+ const registry = createHandlerRegistry();
46
25
 
47
26
  async function launch(
48
27
  processName: string,
49
28
  context: Record<string, unknown>,
50
- startEvent: string = "start",
29
+ startEvent = "start",
51
30
  ) {
52
- const config = configsManager.getProcessConfig(processName);
53
- const load = configsManager.getHandlersLoader(processName);
54
- return startFsmProcess(context, config, load, startEvent);
31
+ const cfg = registry.getConfig(processName);
32
+ const load = registry.getLoader(processName);
33
+ return startProcess(context, cfg, load, startEvent);
55
34
  }
56
35
 
57
- function registerProcess(
58
- configsManager: ProcessConfigManager,
59
- process: Record<string, unknown>,
60
- ) {
61
- if (!process || typeof process !== "object") {
62
- throw new Error("Invalid process configuration: not an object");
63
- }
64
- const processName = process.name;
65
- if (!processName || typeof processName !== "string") {
66
- throw new Error("Invalid process configuration: missing or invalid name");
67
- }
36
+ const rootContext: Record<string, unknown> = {
37
+ [KEY_LAUNCH_PROCESS]: launch,
38
+ };
39
+ const initContext = config.context ?? ((ctx: Record<string, unknown>) => ctx);
68
40
 
69
- const processConfig = process.config as FsmStateConfig | undefined;
70
- if (processConfig && typeof processConfig !== "object") {
71
- throw new Error(
72
- `Invalid process configuration: config is not an object in process ${process.name}`,
73
- );
74
- }
75
- if (processConfig) {
76
- configsManager.registerConfig(processName, processConfig);
77
- }
41
+ const startSet = config.start ? new Set(config.start) : undefined;
78
42
 
79
- const processHandlers = asArray(
80
- process.handlers ?? process.handler ?? process.default,
81
- (v, idx) => {
82
- if (typeof v === "function") return v;
83
- if (typeof v === "object" && v !== null) return v;
84
- throw new Error(
85
- [
86
- `Invalid process configuration:`,
87
- `a process handler is not object nor function in process ${processName} at index ${idx}`,
88
- `Recieved: ${v === null ? "null" : typeof v}`,
89
- ].join("\n"),
90
- );
91
- },
92
- );
93
- if (processHandlers.length > 0) {
94
- configsManager.registerHandlers(processName, ...processHandlers);
43
+ for (const def of config.processes) {
44
+ registry.register(def.name, def.config);
45
+ if (def.handlers?.length) {
46
+ registry.registerHandlers(def.name, ...def.handlers);
95
47
  }
96
48
  }
97
49
 
98
- const rootContext = { "fsm:launch": launch };
99
- const init = typeof options === "function" ? options : async () => options;
100
- const config = await init(rootContext);
101
- if (typeof config !== "object" || !config) {
102
- throw new Error("Invalid launcher configuration");
103
- }
50
+ const shutdowns: (() => Promise<void>)[] = [];
51
+ const seen = new Set<string>();
52
+ for (const def of config.processes) {
53
+ if (seen.has(def.name)) continue;
54
+ seen.add(def.name);
104
55
 
105
- const processes = asArray(
106
- config.processes ?? config.process ?? config.default,
107
- (v) => {
108
- if (typeof v === "function") {
109
- return {
110
- name: v.name || "",
111
- handler: v,
112
- };
113
- }
114
- if (typeof v !== "object") {
115
- throw new Error(
116
- "Invalid launcher configuration: process is not an object",
117
- );
118
- }
119
- return v;
120
- },
121
- );
56
+ if (def.start === false) continue;
57
+ if (startSet && !startSet.has(def.name)) continue;
122
58
 
123
- const processesToStart = {} as Record<string, undefined | boolean>;
124
- for (const process of processes) {
125
- const processName = process.name;
126
- const start = process.start;
127
- if (start === undefined) {
128
- if (!(processName in processesToStart)) {
129
- processesToStart[processName] = undefined;
130
- }
131
- } else {
132
- processesToStart[processName] = start;
133
- }
134
- registerProcess(configsManager, process);
135
- }
136
-
137
- const shutdowns: (() => Promise<void>)[] = [];
138
- let initContext: (
139
- parent: Record<string, unknown>,
140
- ) => Record<string, unknown> = (context) => context;
141
- const configContext = config.context ?? config.init;
142
- if (configContext) {
143
- if (typeof configContext === "function") {
144
- initContext = configContext;
145
- } else if (typeof configContext === "object") {
146
- initContext = () => ({ parent: configContext });
147
- } else {
148
- throw new Error("Invalid launcher configuration: context");
149
- }
150
- }
151
- for (const [processName, start] of Object.entries(processesToStart)) {
152
- if (start === false) continue;
153
59
  let context: Record<string, unknown> = {
154
60
  parent: rootContext,
155
- "fsm:name": processName,
61
+ "fsm:name": def.name,
156
62
  };
157
63
  context = initContext(context) ?? context;
158
- if (typeof context !== "object") {
159
- throw new Error(
160
- `Invalid launcher context for process "${processName}". Expected an object, but recieved ${typeof context}.`,
161
- );
162
- }
163
- const shutdown = await launch(processName, context);
164
- shutdowns.push(shutdown);
64
+ const handle = await launch(def.name, context);
65
+ shutdowns.push(handle.shutdown);
165
66
  }
166
67
 
167
68
  return async () => {
@@ -1,34 +1,115 @@
1
- import { FsmProcess } from "../core/fsm-process.ts";
1
+ import { FsmProcess, type FsmProcessDump } from "../core/fsm-process.ts";
2
+ import type { FsmState } from "../core/fsm-state.ts";
2
3
  import type { FsmStateConfig } from "../core/fsm-state-config.ts";
3
- import { isStateTransitionEnabled } from "../utils/index.ts";
4
- import {
5
- KEY_DISPATCH,
6
- KEY_EVENT,
7
- KEY_STATES,
8
- KEY_TERMINATE,
9
- } from "./constants.ts";
10
- import { isGenerator } from "./is-generator.ts";
11
- import type { StageHandler } from "./types.ts";
12
-
13
- export async function startFsmProcess<C = unknown>(
4
+ import type { FsmStateDescriptor } from "../core/fsm-state-descriptor.ts";
5
+ import type { StageHandler } from "./handler-registry.ts";
6
+
7
+ export interface ProcessHandle {
8
+ shutdown(): Promise<void>;
9
+ dump(...args: unknown[]): Promise<FsmProcessDump>;
10
+ restore(dump: FsmProcessDump, ...args: unknown[]): Promise<void>;
11
+ }
12
+
13
+ // Context keys for binding FSM functions into the shared context object.
14
+ export const KEY_DISPATCH = "fsm:dispatch";
15
+ export const KEY_TERMINATE = "fsm:terminate";
16
+ export const KEY_STATES = "fsm:states";
17
+ export const KEY_EVENT = "fsm:event";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Transition introspection
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export function getStateTransitions(
24
+ state?: FsmState,
25
+ ): [from: string, event: string, to: string][] {
26
+ const result: [from: string, event: string, to: string][] = [];
27
+ const index: Record<string, boolean> = {};
28
+ if (state) {
29
+ let prevStateKey = state.key;
30
+ for (let parent = state.parent; parent; parent = parent.parent) {
31
+ if (!parent.descriptor) continue;
32
+ result.push(
33
+ ...getTransitionsFromDescriptor(parent.descriptor, prevStateKey, index),
34
+ );
35
+ prevStateKey = parent.key;
36
+ }
37
+ }
38
+ return result.reverse();
39
+ }
40
+
41
+ function getTransitionsFromDescriptor(
42
+ descriptor: FsmStateDescriptor,
43
+ prevStateKey: string,
44
+ index: Record<string, boolean>,
45
+ ): [from: string, event: string, to: string][] {
46
+ const result: [from: string, event: string, to: string][] = [];
47
+ const prevStateKeys = [prevStateKey, "*"];
48
+ for (const prevKey of prevStateKeys) {
49
+ const targets = descriptor.transitions[prevKey];
50
+ if (targets) {
51
+ for (const [event, target] of Object.entries(targets)) {
52
+ if (index[event]) continue;
53
+ if (target) index[event] = true;
54
+ result.push([prevStateKey, event, target]);
55
+ }
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+
61
+ export function isStateTransitionEnabled(
62
+ process: FsmProcess,
63
+ event: string,
64
+ ): boolean {
65
+ const transitions = getStateTransitions(process.state);
66
+ for (const [, ev] of transitions) {
67
+ if (ev === event) return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Generator detection
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function isGenerator(
77
+ value: unknown,
78
+ ): value is
79
+ | Generator<string, void, unknown>
80
+ | AsyncGenerator<string, void, unknown> {
81
+ return (
82
+ typeof value === "object" &&
83
+ value !== null &&
84
+ "next" in value &&
85
+ typeof (value as { next: unknown }).next === "function"
86
+ );
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // startProcess (also exported as startFsmProcess for backward compat)
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export async function startProcess<C = unknown>(
14
94
  context: C,
15
95
  config: FsmStateConfig,
16
96
  load: (
17
97
  state: string,
18
- event: undefined | string,
98
+ event: string | undefined,
19
99
  ) => StageHandler<C>[] | Promise<StageHandler<C>[]>,
20
100
  startEvent = "",
21
- ): Promise<() => Promise<void>> {
101
+ ): Promise<ProcessHandle> {
22
102
  let terminated = false;
23
103
  const ctx = context as Record<string, unknown>;
24
104
  const process = new FsmProcess(config);
25
105
  const statesStack: string[] = [];
26
- // ctx[KEY_PROCESS] = process;
27
- process.onStateCreate((state) => {
106
+
107
+ process.onStateCreate((state: FsmState) => {
28
108
  state.onEnter(async () => {
29
109
  statesStack.push(state.key);
30
110
  ctx[KEY_STATES] = [...statesStack];
31
111
  ctx[KEY_EVENT] = process.event;
112
+
32
113
  const modules = (await load(state.key, process.event)) ?? [];
33
114
  for (const module of Array.isArray(modules) ? modules : [modules]) {
34
115
  const result = await module?.(context);
@@ -36,18 +117,21 @@ export async function startFsmProcess<C = unknown>(
36
117
  let stateExited = false;
37
118
  state.onExit(() => {
38
119
  stateExited = true;
39
- result.return?.(void 0);
120
+ result.return?.(undefined as never);
40
121
  });
41
122
  (async () => {
42
- for await (const event of result) {
43
- if (stateExited || terminated) {
44
- break;
123
+ try {
124
+ for await (const event of result) {
125
+ if (stateExited || terminated) break;
126
+ await dispatch(event);
127
+ if (stateExited || terminated) break;
45
128
  }
46
- await dispatch(event);
129
+ } catch {
130
+ // Swallow generator errors after return()
47
131
  }
48
132
  })();
49
133
  } else if (typeof result === "function") {
50
- state.onExit(result);
134
+ state.onExit(result as () => void | Promise<void>);
51
135
  }
52
136
  }
53
137
  });
@@ -57,17 +141,48 @@ export async function startFsmProcess<C = unknown>(
57
141
  ctx[KEY_EVENT] = process.event;
58
142
  });
59
143
  });
144
+
145
+ async function dispatch(event: string): Promise<void> {
146
+ if (event !== undefined && isStateTransitionEnabled(process, event)) {
147
+ await process.dispatch(event);
148
+ }
149
+ }
60
150
  async function terminate(): Promise<void> {
61
151
  terminated = true;
62
152
  await process.shutdown();
63
153
  }
64
- async function dispatch(event: string): Promise<void> {
65
- if (event !== undefined && isStateTransitionEnabled(process, event)) {
66
- await process.dispatch(event);
154
+
155
+ async function dumpProcess(...args: unknown[]): Promise<FsmProcessDump> {
156
+ return process.dump(...args);
157
+ }
158
+
159
+ async function restoreProcess(
160
+ dumpData: FsmProcessDump,
161
+ ...args: unknown[]
162
+ ): Promise<void> {
163
+ statesStack.length = 0;
164
+ terminated = false;
165
+ await process.restore(dumpData, ...args);
166
+ for (
167
+ let state: FsmState | undefined = process.state;
168
+ state;
169
+ state = state.parent
170
+ ) {
171
+ statesStack.unshift(state.key);
67
172
  }
173
+ ctx[KEY_STATES] = [...statesStack];
174
+ ctx[KEY_EVENT] = process.event;
68
175
  }
176
+
69
177
  ctx[KEY_DISPATCH] = dispatch;
70
178
  ctx[KEY_TERMINATE] = terminate;
71
179
  await process.dispatch(startEvent);
72
- return terminate;
180
+ return {
181
+ shutdown: terminate,
182
+ dump: dumpProcess,
183
+ restore: restoreProcess,
184
+ };
73
185
  }
186
+
187
+ /** @deprecated Use `startProcess` instead */
188
+ export const startFsmProcess = startProcess;
@@ -1,4 +1,2 @@
1
1
  export * from "./printer.ts";
2
- export * from "./process.ts";
3
2
  export * from "./tracer.ts";
4
- export * from "./transitions.ts";
@@ -1,5 +1,4 @@
1
1
  import type { FsmProcess } from "../core/fsm-process.ts";
2
- import type { FsmState } from "../core/fsm-state.ts";
3
2
  export type Printer = (...args: unknown[]) => void;
4
3
  export type PrinterConfig = {
5
4
  prefix?: string;
@@ -7,7 +6,7 @@ export type PrinterConfig = {
7
6
  lineNumbers?: boolean;
8
7
  };
9
8
 
10
- export const KEY_PRINTER = "printer";
9
+ const printerStore = new WeakMap<object, Printer>();
11
10
 
12
11
  export function preparePrinter(
13
12
  process: FsmProcess,
@@ -26,23 +25,18 @@ export function preparePrinter(
26
25
  return printer;
27
26
  }
28
27
 
29
- export function setPrinter(state: FsmState, config: PrinterConfig = {}) {
30
- const printer = preparePrinter(state.process, config);
31
- state.setData(KEY_PRINTER, printer);
32
- }
33
-
34
28
  export function setProcessPrinter(
35
29
  process: FsmProcess,
36
30
  config: PrinterConfig = {},
37
31
  ) {
38
32
  const printer = preparePrinter(process, config);
39
- process.setData(KEY_PRINTER, printer);
33
+ printerStore.set(process, printer);
40
34
  }
41
35
 
42
36
  export function getProcessPrinter(process: FsmProcess): Printer {
43
- return process.getData(KEY_PRINTER) || console.log;
37
+ return printerStore.get(process) || console.log;
44
38
  }
45
39
 
46
- export function getPrinter(state: FsmState): Printer {
47
- return state.getData(KEY_PRINTER, true) || getProcessPrinter(state.process);
40
+ export function getPrinter(state: { process: FsmProcess }): Printer {
41
+ return printerStore.get(state) || getProcessPrinter(state.process);
48
42
  }
@@ -3,7 +3,7 @@ import type { FsmState } from "../core/fsm-state.ts";
3
3
  import { getPrinter, type Printer } from "./printer.ts";
4
4
 
5
5
  export function setProcessTracer(process: FsmProcess, print?: Printer) {
6
- return process.onStateCreate((state) => {
6
+ return process.onStateCreate((state: FsmState) => {
7
7
  setStateTracer(state, print);
8
8
  });
9
9
  }
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