@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.
@@ -1,130 +0,0 @@
1
- /**
2
- * Key used to bind the dispatch function to the process context.
3
- * The dispatch function allows triggering state transitions by dispatching events.
4
- *
5
- * Example usage:
6
- * ```typescript
7
- * const context: Record<string, unknown> = {};
8
- * // Initialize the FSM process using `startFsmProcess`
9
- * const dispatch = context["fsm:dispatch"] as (event: string) => Promise<void>;
10
- * await dispatch("someEvent");
11
- */
12
- export const KEY_DISPATCH = "fsm:dispatch" as const;
13
-
14
- /**
15
- * Key used to bind the terminate function to the process context.
16
- * The terminate function is used to gracefully shut down the FSM process.
17
- * It returns a Promise that resolves when the process has been fully terminated.
18
- * Example usage:
19
- * ```typescript
20
- * const context: Record<string, unknown> = {};
21
- * // Initialize the FSM process using `startFsmProcess`
22
- * const terminate = context["fsm:terminate"] as () => Promise<void>;
23
- * await terminate();
24
- * ```
25
- */
26
- export const KEY_TERMINATE = "fsm:terminate" as const;
27
-
28
- /**
29
- * Key used to bind the current stack of active states to the process context.
30
- * This provides a snapshot of the states the FSM process has entered.
31
- * It is represented as an array of strings -- state keys.
32
- *
33
- * Example usage:
34
- * ```typescript
35
- * const context: Record<string, unknown> = {};
36
- * // Initialize the FSM process using `startFsmProcess`
37
- * const states = context["fsm:states"] as string[];
38
- * console.log("Current active states:", states);
39
- * ```
40
- */
41
- export const KEY_STATES = "fsm:states" as const;
42
-
43
- /**
44
- * Key used to bind the current event being processed to the process context.
45
- * This represents the event that triggered the current state transition.
46
- * It is represented as a string.
47
- * Example usage:
48
- * ```typescript
49
- * const context: Record<string, unknown> = {};
50
- * // Initialize the FSM process using `startFsmProcess`
51
- * const event = context["fsm:event"] as string;
52
- * console.log("Current event:", event);
53
- * ```
54
- */
55
- export const KEY_EVENT = "fsm:event" as const;
56
-
57
- //-------------------------------------------------
58
-
59
- // export const KEY_START_PROCESS = "fsm:sys:start" as const;
60
- /**
61
- * Key used to register a new FSM process configuration in the process context.
62
- * This key is associated with a function that allows adding new FSM configurations.
63
- * The function takes a process name and its configuration, and returns a cleanup function
64
- * to remove the registered configuration.
65
- * Example usage:
66
- * ```typescript
67
- * const context: Record<string, unknown> = {};
68
- * initProcessManager(context); // Initialize the process manager in the context
69
- * ...
70
- * type StateConfig = { key: string; transitions?: string[][]; states?: StateConfig[] };
71
- * const registerConfig = context["fsm:sys:registerConfig"] as (name: string, config: StateConfig) => () => void;
72
- * const unregister = registerConfig("myProcess", { key: "Main", transitions: [...] });
73
- * // To unregister the configuration later
74
- * unregister();
75
- * ```
76
- */
77
- export const KEY_REGISTER_CONFIG = "fsm:sys:registerConfig" as const;
78
-
79
- /**
80
- * Key used to register state and event handlers for FSM processes in the process context.
81
- * This key is associated with a function that allows adding handler modules for a specific process.
82
- * The function takes a process name and a variable number of handler modules, returning a cleanup function
83
- * to remove the registered handlers.
84
- * Example usage:
85
- * ```typescript
86
- * const context: Record<string, unknown> = {};
87
- * initProcessManager(context); // Initialize the process manager in the context
88
- * ...
89
- * const registerHandlers = context["fsm:sys:registerHandlers"] as (name: string, ...modules: unknown[]) => () => void;
90
- * const handlerModule1 = {
91
- * Main: async (context) => { ... },
92
- * SomeState: async (context) => { ... },
93
- * }
94
- * // Views layer can be defined in the same or separate modules
95
- * const handlerModule3 = {
96
- * MainView: async (context) => { ... },
97
- * SomeStateView: async (context) => { ... },
98
- * }
99
- * // A common handler function to call for all states
100
- * const handlerModule2 = (context) => { ... };
101
- * const unregister = registerHandlers(
102
- * "myProcess",
103
- * handlerModule1,
104
- * handlerModule2,
105
- * handlerModule3
106
- * );
107
- * // To unregister the handlers later
108
- * unregister();
109
- * ```
110
- */
111
- export const KEY_REGISTER_HANDLERS = "fsm:sys:registerHandlers" as const;
112
-
113
- /**
114
- * Key used to launch an FSM process in the process context.
115
- * This key is associated with a function that initiates the FSM process based on its name and context.
116
- * The function takes the process name and context, returning either nothing, a cleanup function,
117
- * or a Promise that resolves to either nothing or a cleanup function.
118
- * Example usage:
119
- * ```typescript
120
- * const context: Record<string, unknown> = {};
121
- * initProcessManager(context); // Initialize the process manager in the context
122
- * // Register process configurations and handlers as needed
123
- *
124
- * const launchProcess = context["fsm:sys:launch"] as (name: string, context: Record<string, unknown>) => () => Promise<void>;
125
- * const terminate = await launchProcess("myProcess", context);
126
- * // To terminate the process later
127
- * terminate?.();
128
- * ```
129
- */
130
- export const KEY_LAUNCH_PROCESS = "fsm:sys:launch" as const;
@@ -1,9 +0,0 @@
1
- export * from "./constants.ts";
2
- export * from "./index.ts";
3
- export * from "./launcher.ts";
4
- export * from "./process-config-manager.ts";
5
- export * from "./resolve-module-refs.ts";
6
- export * from "./start-node-processes.ts";
7
- export * from "./start-process.ts";
8
- export * from "./start-processes.ts";
9
- export * from "./types.ts";
@@ -1,12 +0,0 @@
1
- export function isGenerator(
2
- value: unknown,
3
- ): value is
4
- | Generator<string, void, unknown>
5
- | AsyncGenerator<string, void, unknown> {
6
- return (
7
- typeof value === "object" &&
8
- value !== null &&
9
- "next" in value &&
10
- typeof (value as Generator<string, void, unknown>).next === "function"
11
- );
12
- }
@@ -1,172 +0,0 @@
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";
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
- );
34
-
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[];
42
- }
43
-
44
- export async function launcher(options: unknown) {
45
- const configsManager = new ProcessConfigManager();
46
-
47
- async function launch(
48
- processName: string,
49
- context: Record<string, unknown>,
50
- startEvent: string = "start",
51
- ) {
52
- const config = configsManager.getProcessConfig(processName);
53
- const load = configsManager.getHandlersLoader(processName);
54
- return startFsmProcess(context, config, load, startEvent);
55
- }
56
-
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
- }
68
-
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
- }
78
-
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);
95
- }
96
- }
97
-
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
- }
104
-
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
- );
122
-
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
- let context: Record<string, unknown> = {
154
- parent: rootContext,
155
- "fsm:name": processName,
156
- };
157
- 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);
165
- }
166
-
167
- return async () => {
168
- for (const shutdown of shutdowns) {
169
- await shutdown();
170
- }
171
- };
172
- }
@@ -1,70 +0,0 @@
1
- import type { FsmStateConfig } from "../core/fsm-state-config.ts";
2
- import { type StageHandler, toStageHandlers } from "./types.ts";
3
-
4
- export interface IProcessConfigManager {
5
- getProcessConfig(processName: string): FsmStateConfig;
6
- getHandlersLoader<C = unknown>(
7
- processName: string,
8
- ): (state: string, event: undefined | string) => StageHandler<C>[];
9
- registerConfig(name: string, config: FsmStateConfig): () => void;
10
- registerHandlers(name: string, ...modules: unknown[]): () => void;
11
- }
12
-
13
- export class ProcessConfigManager implements IProcessConfigManager {
14
- protected configs: Record<string, FsmStateConfig> = {};
15
- protected handlers: Record<string, unknown[]> = {};
16
-
17
- getProcessConfig(processName: string): FsmStateConfig {
18
- return this.configs[processName] ?? { key: "Main" };
19
- }
20
-
21
- getHandlersLoader<C = unknown>(
22
- processName: string,
23
- ): (state: string, event: undefined | string) => StageHandler<C>[] {
24
- const config = this.getProcessConfig(processName);
25
- return (stateKey: string) => {
26
- const modulesKeys: string[] = [];
27
- if (stateKey === config.key) {
28
- modulesKeys.push("default");
29
- }
30
- modulesKeys.push(
31
- stateKey,
32
- `${stateKey}Controller`,
33
- `${stateKey}StateController`,
34
- `${stateKey}View`,
35
- `${stateKey}StateView`,
36
- `${stateKey}Trigger`,
37
- `${stateKey}StateTrigger`,
38
- `${stateKey}Test`,
39
- `${stateKey}StateTest`,
40
- );
41
- const keysSet = new Set(modulesKeys);
42
- const processHandlers = this.handlers[processName] ?? [];
43
- const handlers = toStageHandlers(processHandlers, (key: string) =>
44
- keysSet.has(key),
45
- );
46
- return handlers;
47
- };
48
- }
49
-
50
- registerConfig(name: string, config: FsmStateConfig): () => void {
51
- this.configs[name] = config;
52
- return () => {
53
- delete this.configs[name];
54
- };
55
- }
56
-
57
- registerHandlers(name: string, ...modules: unknown[]): () => void {
58
- let list: unknown[] = this.handlers[name];
59
- if (!list) {
60
- list = this.handlers[name] = [];
61
- }
62
- list.push(modules);
63
- return () => {
64
- list = list.filter((v) => v !== modules);
65
- if (list.length === 0) {
66
- delete this.handlers[name];
67
- }
68
- };
69
- }
70
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * Resolve module paths relative to a base URL.
3
- * This is useful for converting relative paths to absolute URLs.
4
- *
5
- * @param baseUrl - The base URL to resolve against.
6
- * @param refs - An array of module references (relative or absolute).
7
- * @returns An array of resolved absolute module URLs as strings.
8
- * @example
9
- * ```ts
10
- * const urls = resolveModulesUrls(
11
- * 'file:///home/user/project/', // base URL
12
- * 'https://example.com/js/my-module.js', // absolute URL
13
- * './module1.js', // relative path
14
- * '../module2.js' // relative path
15
- * );
16
- * console.log(urls);
17
- * // Output: [
18
- * // 'https://example.com/js/my-module.js',
19
- * // 'file:///home/user/project/module1.js',
20
- * // 'file:///home/user/module2.js'
21
- * // ] *
22
- * ```
23
- */
24
- export function resolveModulesUrls(
25
- baseUrl: URL | string,
26
- ...refs: (URL | string)[]
27
- ): string[] {
28
- return refs.map((ref) => new URL(ref, baseUrl).href);
29
- }
30
-
31
- /**
32
- * Resolves module paths relative to a base URL.
33
- * This is useful for converting relative paths to absolute file system paths.
34
- * @param baseUrl - The base URL to resolve against
35
- * @param refs - An array of module references (relative or absolute)
36
- * @returns - An array of resolved absolute module paths as strings
37
- * @example
38
- * ```ts
39
- * const paths = resolveModulesPaths('file:///home/user/project/', './module1.js', '../module2.js');
40
- * console.log(paths);
41
- * // Output: [
42
- * // '/home/user/project/module1.js',
43
- * // '/home/user/module2.js'
44
- * // ]
45
- * ```
46
- */
47
- export function resolveModulesPaths(
48
- baseUrl: URL | string,
49
- ...refs: (URL | string)[]
50
- ): string[] {
51
- return refs.map((ref) => new URL(ref, baseUrl).pathname);
52
- }
@@ -1,19 +0,0 @@
1
- import { resolveModulesPaths } from "./resolve-module-refs.ts";
2
- import { startProcesses } from "./start-processes.ts";
3
- export async function startNodeProcesses(modules?: string[]): Promise<void> {
4
- try {
5
- if (!modules) {
6
- modules = process.argv.slice(2);
7
- }
8
- const terminate = await startProcesses({
9
- modules: resolveModulesPaths(`file://${process.cwd()}/`, ...modules),
10
- onExit: () => process.exit(0),
11
- });
12
- process.on("SIGINT", terminate);
13
- process.on("SIGTERM", terminate);
14
- process.stdin.resume();
15
- } catch (err) {
16
- console.error("Fatal error:", err);
17
- process.exit(1);
18
- }
19
- }
@@ -1,73 +0,0 @@
1
- import { FsmProcess } from "../core/fsm-process.ts";
2
- 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>(
14
- context: C,
15
- config: FsmStateConfig,
16
- load: (
17
- state: string,
18
- event: undefined | string,
19
- ) => StageHandler<C>[] | Promise<StageHandler<C>[]>,
20
- startEvent = "",
21
- ): Promise<() => Promise<void>> {
22
- let terminated = false;
23
- const ctx = context as Record<string, unknown>;
24
- const process = new FsmProcess(config);
25
- const statesStack: string[] = [];
26
- // ctx[KEY_PROCESS] = process;
27
- process.onStateCreate((state) => {
28
- state.onEnter(async () => {
29
- statesStack.push(state.key);
30
- ctx[KEY_STATES] = [...statesStack];
31
- ctx[KEY_EVENT] = process.event;
32
- const modules = (await load(state.key, process.event)) ?? [];
33
- for (const module of Array.isArray(modules) ? modules : [modules]) {
34
- const result = await module?.(context);
35
- if (isGenerator(result)) {
36
- let stateExited = false;
37
- state.onExit(() => {
38
- stateExited = true;
39
- result.return?.(void 0);
40
- });
41
- (async () => {
42
- for await (const event of result) {
43
- if (stateExited || terminated) {
44
- break;
45
- }
46
- await dispatch(event);
47
- }
48
- })();
49
- } else if (typeof result === "function") {
50
- state.onExit(result);
51
- }
52
- }
53
- });
54
- state.onExit(() => {
55
- statesStack.pop();
56
- ctx[KEY_STATES] = [...statesStack];
57
- ctx[KEY_EVENT] = process.event;
58
- });
59
- });
60
- async function terminate(): Promise<void> {
61
- terminated = true;
62
- await process.shutdown();
63
- }
64
- async function dispatch(event: string): Promise<void> {
65
- if (event !== undefined && isStateTransitionEnabled(process, event)) {
66
- await process.dispatch(event);
67
- }
68
- }
69
- ctx[KEY_DISPATCH] = dispatch;
70
- ctx[KEY_TERMINATE] = terminate;
71
- await process.dispatch(startEvent);
72
- return terminate;
73
- }
@@ -1,32 +0,0 @@
1
- import { launcher } from "./launcher.ts";
2
-
3
- export async function startProcesses({
4
- onExit,
5
- modules,
6
- init,
7
- }: {
8
- onExit?: () => Promise<void> | void;
9
- modules: unknown[];
10
- init?: (
11
- context: Record<string, unknown>,
12
- ) => Promise<Record<string, unknown>> | Record<string, unknown>;
13
- }) {
14
- const modulesList: unknown[] = [];
15
- for (const module of modules) {
16
- modulesList.push(
17
- typeof module === "string" || module instanceof URL
18
- ? await import(String(module))
19
- : module,
20
- );
21
- }
22
- return await launcher({
23
- init,
24
- processes: [
25
- ...modulesList,
26
- // This function is called on exit at the end of the process chain
27
- function Exit() {
28
- return onExit;
29
- },
30
- ],
31
- });
32
- }
@@ -1,34 +0,0 @@
1
- export type StageHandler<C = Record<string, unknown>> = (
2
- context: C,
3
- ) =>
4
- | void
5
- | (() => void | Promise<void>)
6
- | Promise<void | (() => void | Promise<void>)>
7
- | AsyncGenerator<string, void, unknown>
8
- | Generator<string, void, unknown>;
9
-
10
- export function toStageHandlers<C>(
11
- value: unknown,
12
- accept: (key: string, value: unknown) => boolean,
13
- ): StageHandler<C>[] {
14
- return visit(value);
15
-
16
- function visit(val: unknown): StageHandler<C>[] {
17
- if (!val) {
18
- return [];
19
- }
20
- if (typeof val === "function") {
21
- return [val] as StageHandler<C>[];
22
- }
23
- if (typeof val !== "object") {
24
- return [];
25
- }
26
- if (Array.isArray(val)) {
27
- return val.flatMap(visit);
28
- }
29
- const modulesIndex = val as Record<string, unknown>;
30
- return Object.entries(modulesIndex).flatMap(([key, value]) => {
31
- return accept(key, value) ? visit(value) : [];
32
- });
33
- }
34
- }
@@ -1,35 +0,0 @@
1
- import type { FsmState, FsmStateHandler } from "../core/fsm-state.ts";
2
-
3
- export const KEY_HANDLERS = "handlers";
4
-
5
- export function callStateHandlers(state: FsmState) {
6
- const key = state.key;
7
- for (
8
- let parent: FsmState | undefined = state.parent;
9
- parent;
10
- parent = parent?.parent
11
- ) {
12
- const handlers =
13
- parent.getData<Record<string, FsmStateHandler[]>>(KEY_HANDLERS)?.[key];
14
- handlers?.forEach((handler) => {
15
- handler(state);
16
- });
17
- }
18
- }
19
-
20
- export function addSubstateHandlers(
21
- state: FsmState,
22
- handlers: Record<string, FsmStateHandler>,
23
- ) {
24
- const oldIndex =
25
- state.getData<Record<string, FsmStateHandler[]>>(KEY_HANDLERS);
26
- const index = oldIndex ? { ...oldIndex } : {};
27
- for (const [key, handler] of Object.entries(handlers)) {
28
- let list = index[key];
29
- if (!list) {
30
- list = index[key] = [];
31
- }
32
- list.push(handler);
33
- }
34
- state.setData(KEY_HANDLERS, index);
35
- }
@@ -1,4 +0,0 @@
1
- export * from "./printer.ts";
2
- export * from "./process.ts";
3
- export * from "./tracer.ts";
4
- export * from "./transitions.ts";