@statewalker/fsm 0.28.0 → 0.30.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/dist/index.d.ts +170 -13
- package/dist/index.js +268 -93
- package/package.json +8 -9
- package/src/FsmBaseClass.ts +13 -6
- package/src/FsmProcess.ts +7 -3
- package/src/FsmState.ts +7 -7
- package/src/FsmStateDescriptor.ts +8 -5
- package/src/index.ts +2 -2
- package/src/newFsmProcess.ts +46 -17
- package/src/orchestrator/ProcessConfigManager.ts +70 -0
- package/src/orchestrator/constants.ts +130 -0
- package/src/orchestrator/index.ts +6 -0
- package/src/orchestrator/isGenerator.ts +12 -0
- package/src/orchestrator/launcher.ts +72 -0
- package/src/orchestrator/startFsmProcess.ts +73 -0
- package/src/orchestrator/types.ts +34 -0
- package/src/utils/handlers.ts +9 -4
- package/src/utils/newFsmStateHandler.ts +7 -3
- package/src/utils/printer.ts +1 -1
- package/src/startFsmProcess copy 2.ts +0 -74
- package/src/startFsmProcess copy.ts +0 -77
- package/src/startFsmProcess.ts +0 -78
package/src/FsmBaseClass.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
type Handler<P extends unknown[] = unknown[], R = unknown> = (...args: P) => R; // Function;
|
|
2
|
+
// type Handler = Function;
|
|
3
|
+
|
|
1
4
|
export class FsmBaseClass {
|
|
2
|
-
handlers: Record<string,
|
|
5
|
+
handlers: Record<string, Handler[]> = {};
|
|
3
6
|
data: Record<string, unknown> = {};
|
|
4
7
|
constructor() {
|
|
5
8
|
bindMethods(this, "setData", "getData");
|
|
@@ -14,13 +17,17 @@ export class FsmBaseClass {
|
|
|
14
17
|
|
|
15
18
|
// ----------------------------------------------
|
|
16
19
|
// internal methods
|
|
17
|
-
_addHandler(type: string, handler:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
protected _addHandler<T>(type: string, handler: T, direct: boolean = true) {
|
|
21
|
+
let list = this.handlers[type];
|
|
22
|
+
if (!list) {
|
|
23
|
+
list = this.handlers[type] = [];
|
|
24
|
+
}
|
|
25
|
+
const h = handler as Handler;
|
|
26
|
+
direct ? list.push(h) : list.unshift(h);
|
|
27
|
+
return () => this._removeHandler(type, h);
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
_removeHandler(type: string, handler:
|
|
30
|
+
protected _removeHandler<T>(type: string, handler: T) {
|
|
24
31
|
let list = this.handlers[type];
|
|
25
32
|
if (!list) return;
|
|
26
33
|
list = list.filter((h) => h !== handler);
|
package/src/FsmProcess.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { bindMethods, FsmBaseClass } from "./FsmBaseClass.ts";
|
|
2
|
+
import {
|
|
3
|
+
FsmState,
|
|
4
|
+
type FsmStateDump,
|
|
5
|
+
type FsmStateHandler,
|
|
6
|
+
} from "./FsmState.ts";
|
|
3
7
|
import {
|
|
4
8
|
EVENT_EMPTY,
|
|
5
|
-
FsmStateConfig,
|
|
9
|
+
type FsmStateConfig,
|
|
6
10
|
STATE_FINAL,
|
|
7
11
|
STATE_INITIAL,
|
|
8
12
|
} from "./FsmStateConfig.ts";
|
package/src/FsmState.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FsmProcess } from "./FsmProcess.ts";
|
|
3
|
-
import { FsmStateDescriptor } from "./FsmStateDescriptor.ts";
|
|
1
|
+
import { bindMethods, FsmBaseClass } from "./FsmBaseClass.ts";
|
|
2
|
+
import type { FsmProcess } from "./FsmProcess.ts";
|
|
3
|
+
import type { FsmStateDescriptor } from "./FsmStateDescriptor.ts";
|
|
4
4
|
|
|
5
5
|
export type FsmStateDump = Record<string, unknown> & {
|
|
6
6
|
key: string;
|
|
@@ -13,12 +13,12 @@ export type FsmStateHandler = (
|
|
|
13
13
|
|
|
14
14
|
export type FsmStateDumpHandler = (
|
|
15
15
|
state: FsmState,
|
|
16
|
-
dump: FsmStateDump
|
|
16
|
+
dump: FsmStateDump,
|
|
17
17
|
) => void | Promise<void>;
|
|
18
18
|
|
|
19
19
|
export type FsmStateErrorHandler = (
|
|
20
20
|
state: FsmState,
|
|
21
|
-
error: unknown
|
|
21
|
+
error: unknown,
|
|
22
22
|
) => void | Promise<void>;
|
|
23
23
|
|
|
24
24
|
export class FsmState extends FsmBaseClass {
|
|
@@ -31,7 +31,7 @@ export class FsmState extends FsmBaseClass {
|
|
|
31
31
|
process: FsmProcess,
|
|
32
32
|
parent: FsmState | undefined,
|
|
33
33
|
key: string,
|
|
34
|
-
descriptor?: FsmStateDescriptor
|
|
34
|
+
descriptor?: FsmStateDescriptor,
|
|
35
35
|
) {
|
|
36
36
|
super();
|
|
37
37
|
this.process = process;
|
|
@@ -45,7 +45,7 @@ export class FsmState extends FsmBaseClass {
|
|
|
45
45
|
"dump",
|
|
46
46
|
"restore",
|
|
47
47
|
"useData",
|
|
48
|
-
"onStateError"
|
|
48
|
+
"onStateError",
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type FsmStateConfig,
|
|
3
2
|
EVENT_ANY,
|
|
3
|
+
type FsmStateConfig,
|
|
4
4
|
STATE_ANY,
|
|
5
5
|
STATE_FINAL,
|
|
6
6
|
} from "./FsmStateConfig.ts";
|
|
@@ -12,13 +12,16 @@ export class FsmStateDescriptor {
|
|
|
12
12
|
static build(config: FsmStateConfig) {
|
|
13
13
|
const descriptor = new FsmStateDescriptor();
|
|
14
14
|
for (const [from, event, to] of config.transitions || []) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let index = descriptor.transitions[from];
|
|
16
|
+
if (!index) {
|
|
17
|
+
index = descriptor.transitions[from] = {};
|
|
18
|
+
}
|
|
17
19
|
index[event] = to;
|
|
18
20
|
}
|
|
19
21
|
if (config.states) {
|
|
20
22
|
for (const substateConfig of config.states) {
|
|
21
|
-
descriptor.states[substateConfig.key] =
|
|
23
|
+
descriptor.states[substateConfig.key] =
|
|
24
|
+
FsmStateDescriptor.build(substateConfig);
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
return descriptor;
|
|
@@ -31,7 +34,7 @@ export class FsmStateDescriptor {
|
|
|
31
34
|
[stateKey, EVENT_ANY],
|
|
32
35
|
[STATE_ANY, EVENT_ANY],
|
|
33
36
|
];
|
|
34
|
-
let targetKey;
|
|
37
|
+
let targetKey: string | undefined;
|
|
35
38
|
for (
|
|
36
39
|
let i = 0, len = pairs.length;
|
|
37
40
|
targetKey === undefined && i < len;
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,6 @@ export * from "./FsmProcess.ts";
|
|
|
2
2
|
export * from "./FsmState.ts";
|
|
3
3
|
export * from "./FsmStateConfig.ts";
|
|
4
4
|
export * from "./FsmStateDescriptor.ts";
|
|
5
|
+
export * from "./newFsmProcess.ts";
|
|
6
|
+
export * from "./orchestrator/index.ts";
|
|
5
7
|
export * from "./utils/index.ts";
|
|
6
|
-
export * from "./newFsmProcess.js";
|
|
7
|
-
export * from "./startFsmProcess.js";
|
package/src/newFsmProcess.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { FsmProcess } from "./FsmProcess.
|
|
2
|
-
import type { FsmStateConfig } from "./FsmStateConfig.
|
|
3
|
-
import { isStateTransitionEnabled } from "./utils/transitions.
|
|
1
|
+
import { FsmProcess } from "./FsmProcess.ts";
|
|
2
|
+
import type { FsmStateConfig } from "./FsmStateConfig.ts";
|
|
3
|
+
import { isStateTransitionEnabled } from "./utils/transitions.ts";
|
|
4
4
|
|
|
5
|
-
export type Module<C = unknown> = (
|
|
6
|
-
context: C,
|
|
7
|
-
) =>
|
|
5
|
+
export type Module<C = unknown> = (context: C) =>
|
|
8
6
|
| void
|
|
9
7
|
| (() => void | Promise<void>)
|
|
10
|
-
| Promise<void | (() => void | Promise<void>)
|
|
8
|
+
| Promise<void | (() => void | Promise<void>)>
|
|
9
|
+
// Event emitters / triggers
|
|
10
|
+
| Generator<string>
|
|
11
|
+
| AsyncGenerator<string>;
|
|
11
12
|
|
|
12
13
|
export function newFsmProcess<C>(
|
|
13
14
|
context: C,
|
|
@@ -27,28 +28,56 @@ export function newFsmProcess<C>(
|
|
|
27
28
|
let started = false;
|
|
28
29
|
let terminated = false;
|
|
29
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
|
+
}
|
|
30
38
|
process.onStateCreate((state) => {
|
|
31
39
|
started = true;
|
|
32
40
|
state.onEnter(async () => {
|
|
33
41
|
const modules = (await load(state.key, process.event)) ?? [];
|
|
34
42
|
for (const module of Array.isArray(modules) ? modules : [modules]) {
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
state.onExit(
|
|
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);
|
|
38
61
|
}
|
|
39
62
|
}
|
|
40
63
|
});
|
|
41
64
|
});
|
|
42
|
-
async function dispatch(event: string): Promise<boolean> {
|
|
43
|
-
return terminated ||
|
|
44
|
-
(event !== undefined &&
|
|
45
|
-
(!started || isStateTransitionEnabled(process, event)))
|
|
46
|
-
? process.dispatch(event)
|
|
47
|
-
: false;
|
|
48
|
-
}
|
|
49
65
|
async function shutdown(): Promise<void> {
|
|
50
66
|
await process.shutdown();
|
|
51
67
|
terminated = true;
|
|
52
68
|
}
|
|
53
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
|
+
}
|
|
54
83
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { FsmStateConfig } from "../FsmStateConfig.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
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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;
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { FsmStateConfig } from "../FsmStateConfig.ts";
|
|
2
|
+
import { ProcessConfigManager } from "./ProcessConfigManager.ts";
|
|
3
|
+
import { startFsmProcess } from "./startFsmProcess.ts";
|
|
4
|
+
import type { StageHandler } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export type ProcessConfig = {
|
|
7
|
+
name: string;
|
|
8
|
+
config?: FsmStateConfig;
|
|
9
|
+
handlers?: (
|
|
10
|
+
| StageHandler
|
|
11
|
+
| {
|
|
12
|
+
[state: string]: StageHandler | StageHandler[];
|
|
13
|
+
}
|
|
14
|
+
)[];
|
|
15
|
+
};
|
|
16
|
+
export type ProcessLauncherConfig = {
|
|
17
|
+
start: string[];
|
|
18
|
+
context?: Record<string, unknown>;
|
|
19
|
+
processes: ProcessConfig[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type LauncherRootContext = Record<string, unknown> & {
|
|
23
|
+
launch: (
|
|
24
|
+
processName: string,
|
|
25
|
+
context: Record<string, unknown>,
|
|
26
|
+
startEvent?: string,
|
|
27
|
+
) => Promise<() => Promise<void>>;
|
|
28
|
+
};
|
|
29
|
+
export async function launcher(
|
|
30
|
+
init: (
|
|
31
|
+
root: LauncherRootContext,
|
|
32
|
+
) => ProcessLauncherConfig | Promise<ProcessLauncherConfig>,
|
|
33
|
+
) {
|
|
34
|
+
const configsManager = new ProcessConfigManager();
|
|
35
|
+
|
|
36
|
+
async function launch(
|
|
37
|
+
processName: string,
|
|
38
|
+
context: Record<string, unknown>,
|
|
39
|
+
startEvent: string = "start",
|
|
40
|
+
) {
|
|
41
|
+
const config = configsManager.getProcessConfig(processName);
|
|
42
|
+
const load = configsManager.getHandlersLoader(processName);
|
|
43
|
+
return startFsmProcess(context, config, load, startEvent);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rootContext: LauncherRootContext = {
|
|
47
|
+
launch,
|
|
48
|
+
};
|
|
49
|
+
const config = await init(rootContext);
|
|
50
|
+
for (const process of config.processes) {
|
|
51
|
+
if (process.config) {
|
|
52
|
+
configsManager.registerConfig(process.name, process.config);
|
|
53
|
+
}
|
|
54
|
+
if (process.handlers) {
|
|
55
|
+
configsManager.registerHandlers(process.name, ...process.handlers);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const shutdowns: (() => Promise<void>)[] = [];
|
|
60
|
+
for (const processName of config.start ?? []) {
|
|
61
|
+
const parent = config.context ?? rootContext;
|
|
62
|
+
const context = { parent };
|
|
63
|
+
const shutdown = await launch(processName, context);
|
|
64
|
+
shutdowns.push(shutdown);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return async () => {
|
|
68
|
+
for (const shutdown of shutdowns) {
|
|
69
|
+
await shutdown();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { FsmProcess } from "../FsmProcess.ts";
|
|
2
|
+
import type { FsmStateConfig } from "../FsmStateConfig.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 "./isGenerator.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
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
}
|
package/src/utils/handlers.ts
CHANGED
|
@@ -6,24 +6,29 @@ export function callStateHandlers(state: FsmState) {
|
|
|
6
6
|
const key = state.key;
|
|
7
7
|
for (
|
|
8
8
|
let parent: FsmState | undefined = state.parent;
|
|
9
|
-
|
|
9
|
+
parent;
|
|
10
10
|
parent = parent?.parent
|
|
11
11
|
) {
|
|
12
12
|
const handlers =
|
|
13
13
|
parent.getData<Record<string, FsmStateHandler[]>>(KEY_HANDLERS)?.[key];
|
|
14
|
-
handlers?.forEach((handler) =>
|
|
14
|
+
handlers?.forEach((handler) => {
|
|
15
|
+
handler(state);
|
|
16
|
+
});
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function addSubstateHandlers(
|
|
19
21
|
state: FsmState,
|
|
20
|
-
handlers: Record<string, FsmStateHandler
|
|
22
|
+
handlers: Record<string, FsmStateHandler>,
|
|
21
23
|
) {
|
|
22
24
|
const oldIndex =
|
|
23
25
|
state.getData<Record<string, FsmStateHandler[]>>(KEY_HANDLERS);
|
|
24
26
|
const index = oldIndex ? { ...oldIndex } : {};
|
|
25
27
|
for (const [key, handler] of Object.entries(handlers)) {
|
|
26
|
-
|
|
28
|
+
let list = index[key];
|
|
29
|
+
if (!list) {
|
|
30
|
+
list = index[key] = [];
|
|
31
|
+
}
|
|
27
32
|
list.push(handler);
|
|
28
33
|
}
|
|
29
34
|
state.setData(KEY_HANDLERS, index);
|