@statewalker/fsm 0.23.1 → 0.25.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 +5 -1
- package/dist/index.js +45 -16
- package/package.json +1 -1
- package/src/FsmBaseClass.ts +16 -3
- package/src/FsmProcess.ts +29 -10
- package/src/utils/newFsmStateHandler.ts +95 -55
- package/src/utils/tracer.ts +5 -3
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ declare class FsmBaseClass {
|
|
|
6
6
|
getData<T>(key: string): T | undefined;
|
|
7
7
|
_addHandler(type: string, handler: Function, direct?: boolean): () => void;
|
|
8
8
|
_removeHandler(type: string, handler: Function): void;
|
|
9
|
+
_runHandlerParallel(type: string, ...args: unknown[]): Promise<void>;
|
|
9
10
|
_runHandler(type: string, ...args: unknown[]): Promise<void>;
|
|
10
11
|
_handleError(error: Error | unknown): Promise<void>;
|
|
11
12
|
}
|
|
@@ -71,12 +72,15 @@ type FsmProcessDumpHandler = (process: FsmProcess, dump: FsmProcessDump) => void
|
|
|
71
72
|
declare class FsmProcess extends FsmBaseClass {
|
|
72
73
|
state?: FsmState;
|
|
73
74
|
event?: string;
|
|
75
|
+
nextEvent?: string;
|
|
76
|
+
running: boolean;
|
|
77
|
+
mask: number;
|
|
74
78
|
status: number;
|
|
75
79
|
config: FsmStateConfig;
|
|
76
80
|
rootDescriptor: FsmStateDescriptor;
|
|
77
81
|
constructor(config: FsmStateConfig);
|
|
78
82
|
shutdown(event?: string): Promise<void>;
|
|
79
|
-
dispatch(event: string
|
|
83
|
+
dispatch(event: string): Promise<boolean>;
|
|
80
84
|
dump(...args: unknown[]): Promise<FsmProcessDump>;
|
|
81
85
|
restore(dump: FsmProcessDump, ...args: unknown[]): Promise<this>;
|
|
82
86
|
onStateCreate(handler: FsmStateHandler): () => void;
|
package/dist/index.js
CHANGED
|
@@ -29,12 +29,23 @@ var FsmBaseClass = class {
|
|
|
29
29
|
delete this.handlers[type];
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
async _runHandlerParallel(type, ...args) {
|
|
33
|
+
const list = this.handlers[type] || [];
|
|
34
|
+
await Promise.all(
|
|
35
|
+
list.map(async (handler) => {
|
|
36
|
+
try {
|
|
37
|
+
await handler(...args);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
this._handleError(error);
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
}
|
|
32
44
|
async _runHandler(type, ...args) {
|
|
33
45
|
const list = this.handlers[type] || [];
|
|
34
|
-
const
|
|
35
|
-
for (const promise of promises) {
|
|
46
|
+
for (const handler of list) {
|
|
36
47
|
try {
|
|
37
|
-
await
|
|
48
|
+
await handler(...args);
|
|
38
49
|
} catch (error) {
|
|
39
50
|
await this._handleError(error);
|
|
40
51
|
}
|
|
@@ -158,6 +169,8 @@ var STATUS_EXIT = STATUS_LEAF | STATUS_LAST;
|
|
|
158
169
|
var FsmProcess = class extends FsmBaseClass {
|
|
159
170
|
constructor(config) {
|
|
160
171
|
super();
|
|
172
|
+
this.running = false;
|
|
173
|
+
this.mask = STATUS_LEAF;
|
|
161
174
|
this.status = 0;
|
|
162
175
|
this.rootDescriptor = FsmStateDescriptor.build(config);
|
|
163
176
|
this.config = config;
|
|
@@ -171,18 +184,32 @@ var FsmProcess = class extends FsmBaseClass {
|
|
|
171
184
|
this.state = this.state.parent;
|
|
172
185
|
}
|
|
173
186
|
}
|
|
174
|
-
async dispatch(event
|
|
175
|
-
this.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
async dispatch(event) {
|
|
188
|
+
this.nextEvent = event;
|
|
189
|
+
if (!this.running && !(this.status & STATUS_FINISHED)) {
|
|
190
|
+
this.running = true;
|
|
191
|
+
try {
|
|
192
|
+
this.event = this.nextEvent;
|
|
193
|
+
this.nextEvent = void 0;
|
|
194
|
+
while (true) {
|
|
195
|
+
if (this.status & STATUS_EXIT) {
|
|
196
|
+
await this.state?._runHandler("onExit", this.state);
|
|
197
|
+
}
|
|
198
|
+
if (!this._update()) break;
|
|
199
|
+
if (this.status & STATUS_ENTER) {
|
|
200
|
+
await this.state?._runHandler("onEnter", this.state);
|
|
201
|
+
}
|
|
202
|
+
if (this.status & this.mask) break;
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
this.running = false;
|
|
179
206
|
}
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
207
|
+
const nextEvent = this.nextEvent;
|
|
208
|
+
if (nextEvent !== void 0) {
|
|
209
|
+
return Promise.resolve().then(() => this.dispatch(nextEvent));
|
|
183
210
|
}
|
|
184
|
-
if (this.status & mask) return true;
|
|
185
211
|
}
|
|
212
|
+
return !(this.status & STATUS_FINISHED);
|
|
186
213
|
}
|
|
187
214
|
async dump(...args) {
|
|
188
215
|
const dumpState = async (state) => {
|
|
@@ -234,7 +261,7 @@ var FsmProcess = class extends FsmBaseClass {
|
|
|
234
261
|
}
|
|
235
262
|
_newState(parent, key, descriptor) {
|
|
236
263
|
const state = new FsmState(this, parent, key, descriptor);
|
|
237
|
-
this.
|
|
264
|
+
this._runHandlerParallel("onStateCreate", state);
|
|
238
265
|
return state;
|
|
239
266
|
}
|
|
240
267
|
_getSubstate(parent, prevStateKey) {
|
|
@@ -313,9 +340,11 @@ function setStateTracer(state, print) {
|
|
|
313
340
|
const printLine = print || getPrinter(state);
|
|
314
341
|
printLine(`<${state?.key} event="${state.process.event}">`);
|
|
315
342
|
});
|
|
316
|
-
state.onExit(() => {
|
|
317
|
-
|
|
318
|
-
|
|
343
|
+
state.onExit(async () => {
|
|
344
|
+
await Promise.resolve().then(async () => {
|
|
345
|
+
const printLine = print || getPrinter(state);
|
|
346
|
+
printLine(`</${state.key}> <!-- event="${state.process.event}" -->`);
|
|
347
|
+
});
|
|
319
348
|
});
|
|
320
349
|
}
|
|
321
350
|
|
package/package.json
CHANGED
package/src/FsmBaseClass.ts
CHANGED
|
@@ -31,17 +31,30 @@ export class FsmBaseClass {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
async _runHandlerParallel(type: string, ...args: unknown[]) {
|
|
35
|
+
const list = this.handlers[type] || [];
|
|
36
|
+
await Promise.all(
|
|
37
|
+
list.map(async (handler) => {
|
|
38
|
+
try {
|
|
39
|
+
await handler(...args);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
this._handleError(error);
|
|
42
|
+
}
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
async _runHandler(type: string, ...args: unknown[]) {
|
|
35
48
|
const list = this.handlers[type] || [];
|
|
36
|
-
const
|
|
37
|
-
for (const promise of promises) {
|
|
49
|
+
for (const handler of list) {
|
|
38
50
|
try {
|
|
39
|
-
await
|
|
51
|
+
await handler(...args);
|
|
40
52
|
} catch (error) {
|
|
41
53
|
await this._handleError(error);
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
}
|
|
57
|
+
|
|
45
58
|
async _handleError(error: Error | unknown) {
|
|
46
59
|
console.error(error);
|
|
47
60
|
}
|
package/src/FsmProcess.ts
CHANGED
|
@@ -38,6 +38,9 @@ export type FsmProcessDumpHandler = (
|
|
|
38
38
|
export class FsmProcess extends FsmBaseClass {
|
|
39
39
|
state?: FsmState;
|
|
40
40
|
event?: string;
|
|
41
|
+
nextEvent?: string;
|
|
42
|
+
running: boolean = false;
|
|
43
|
+
mask: number = STATUS_LEAF;
|
|
41
44
|
status: number = 0;
|
|
42
45
|
config: FsmStateConfig;
|
|
43
46
|
rootDescriptor: FsmStateDescriptor;
|
|
@@ -58,18 +61,34 @@ export class FsmProcess extends FsmBaseClass {
|
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
async dispatch(event: string
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
async dispatch(event: string): Promise<boolean> {
|
|
65
|
+
this.nextEvent = event;
|
|
66
|
+
if (!this.running && !(this.status & STATUS_FINISHED)) {
|
|
67
|
+
this.running = true;
|
|
68
|
+
try {
|
|
69
|
+
this.event = this.nextEvent;
|
|
70
|
+
this.nextEvent = undefined;
|
|
71
|
+
while (true) {
|
|
72
|
+
// ---
|
|
73
|
+
if (this.status & STATUS_EXIT) {
|
|
74
|
+
await this.state?._runHandler("onExit", this.state);
|
|
75
|
+
}
|
|
76
|
+
if (!this._update()) break;
|
|
77
|
+
if (this.status & STATUS_ENTER) {
|
|
78
|
+
await this.state?._runHandler("onEnter", this.state);
|
|
79
|
+
}
|
|
80
|
+
if (this.status & this.mask) break;
|
|
81
|
+
// ---
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
this.running = false;
|
|
66
85
|
}
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
86
|
+
const nextEvent = this.nextEvent;
|
|
87
|
+
if (nextEvent !== undefined) {
|
|
88
|
+
return Promise.resolve().then(() => this.dispatch(nextEvent));
|
|
70
89
|
}
|
|
71
|
-
if (this.status & mask) return true;
|
|
72
90
|
}
|
|
91
|
+
return !(this.status & STATUS_FINISHED);
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
async dump(...args: unknown[]): Promise<FsmProcessDump> {
|
|
@@ -138,7 +157,7 @@ export class FsmProcess extends FsmBaseClass {
|
|
|
138
157
|
descriptor: FsmStateDescriptor | undefined,
|
|
139
158
|
) {
|
|
140
159
|
const state = new FsmState(this, parent, key, descriptor);
|
|
141
|
-
this.
|
|
160
|
+
this._runHandlerParallel("onStateCreate", state);
|
|
142
161
|
return state;
|
|
143
162
|
}
|
|
144
163
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FsmState } from "../FsmState";
|
|
2
|
+
import { isStateTransitionEnabled } from "./transitions";
|
|
2
3
|
|
|
3
4
|
export function newModuleLoader<T>(baseUrl: string) {
|
|
4
5
|
const cache: Record<string, Promise<T>> = {};
|
|
@@ -23,15 +24,15 @@ export async function newHandlerLoader(
|
|
|
23
24
|
baseUrl: string,
|
|
24
25
|
loader: (
|
|
25
26
|
path: string,
|
|
26
|
-
) => Promise<undefined |
|
|
27
|
-
): Promise<(state: FsmState) => Promise<
|
|
27
|
+
) => Promise<undefined | FsmStateModule> = newModuleLoader(baseUrl),
|
|
28
|
+
): Promise<(state: FsmState) => Promise<FsmStateModule[]>> {
|
|
28
29
|
return async (state: FsmState) => {
|
|
29
30
|
const stack: string[] = [];
|
|
30
31
|
for (let s: FsmState | undefined = state; s; s = s.parent) {
|
|
31
32
|
stack.unshift(s.key);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const handlers:
|
|
35
|
+
const handlers: FsmStateModule[] = [];
|
|
35
36
|
let topName: undefined | string;
|
|
36
37
|
while (stack.length > 0) {
|
|
37
38
|
const lastSegment = stack.pop();
|
|
@@ -63,24 +64,51 @@ export async function newHandlerLoader(
|
|
|
63
64
|
- could return a promise with optional cleanup function; the state is not
|
|
64
65
|
activated until all handlers return the control
|
|
65
66
|
*/
|
|
66
|
-
export type
|
|
67
|
-
FsmStateContext
|
|
67
|
+
export type FsmStateModule<
|
|
68
|
+
FsmStateContext = Record<string, unknown>,
|
|
68
69
|
FsmStateStore = Record<string, unknown>,
|
|
69
70
|
> = {
|
|
70
|
-
context?:
|
|
71
|
-
|
|
71
|
+
context?:
|
|
72
|
+
| ((...stack: FsmStateStore[]) => FsmStateContext)
|
|
73
|
+
| (new (...stack: FsmStateStore[]) => FsmStateContext);
|
|
74
|
+
// trigger?: (context: FsmStateContext) => AsyncGenerator<string>;
|
|
75
|
+
trigger?: (context: FsmStateContext) => AsyncIterator<string>;
|
|
76
|
+
handler?: (
|
|
72
77
|
context: FsmStateContext,
|
|
73
|
-
|
|
74
|
-
) => void | (() => void);
|
|
75
|
-
default?: (context: FsmStateContext) => void | (() => void);
|
|
76
|
-
handler?: (context: FsmStateContext) => void | (() => void);
|
|
78
|
+
) => void | (() => void) | Promise<void | (() => void)>;
|
|
77
79
|
};
|
|
78
80
|
|
|
81
|
+
export function newModuleContext<
|
|
82
|
+
C = Record<string, unknown>,
|
|
83
|
+
S = Record<string, unknown>,
|
|
84
|
+
>(module: FsmStateModule<C, S>, ...stack: S[]): C {
|
|
85
|
+
if (isClass<S[], C>(module.context)) {
|
|
86
|
+
return new module.context(...stack) as unknown as C;
|
|
87
|
+
} else if (isFunction<S[], C>(module.context)) {
|
|
88
|
+
return module.context(...stack);
|
|
89
|
+
} else {
|
|
90
|
+
return stack[0] as unknown as C;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function isFunction<T extends Array<unknown>, R = unknown>(
|
|
94
|
+
value: unknown,
|
|
95
|
+
): value is (...args: T) => R {
|
|
96
|
+
return typeof value === "function";
|
|
97
|
+
}
|
|
98
|
+
function isClass<T extends Array<unknown>, R = unknown>(
|
|
99
|
+
value: unknown,
|
|
100
|
+
): value is new (...args: T) => R {
|
|
101
|
+
if (!isFunction(value)) return false;
|
|
102
|
+
return value?.prototype?.constructor?.toString().match(/^class/);
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
export function newStateHandlers<
|
|
80
|
-
FsmStateContext
|
|
106
|
+
FsmStateContext = Record<string, unknown>,
|
|
81
107
|
FsmStateStore = Record<string, unknown>,
|
|
82
108
|
>(
|
|
83
|
-
loader: (
|
|
109
|
+
loader: (
|
|
110
|
+
state: FsmState,
|
|
111
|
+
) => Promise<FsmStateModule<FsmStateContext, FsmStateStore>[]>,
|
|
84
112
|
newContext: (state: FsmState, ...stack: FsmStateStore[]) => FsmStateStore = (
|
|
85
113
|
state,
|
|
86
114
|
) =>
|
|
@@ -88,7 +116,12 @@ export function newStateHandlers<
|
|
|
88
116
|
state: state.key,
|
|
89
117
|
event: state.process.event,
|
|
90
118
|
}) as unknown as FsmStateStore,
|
|
119
|
+
contextCache: Map<unknown, FsmStateContext> = new Map<
|
|
120
|
+
unknown,
|
|
121
|
+
FsmStateContext
|
|
122
|
+
>(),
|
|
91
123
|
) {
|
|
124
|
+
const triggers = new Map<unknown, () => void>();
|
|
92
125
|
return (state: FsmState) => {
|
|
93
126
|
const stack: FsmStateStore[] = [];
|
|
94
127
|
for (let s: FsmState | undefined = state.parent; !!s; s = s.parent) {
|
|
@@ -99,15 +132,11 @@ export function newStateHandlers<
|
|
|
99
132
|
stack.unshift(store);
|
|
100
133
|
state.setData("context", store);
|
|
101
134
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// }
|
|
108
|
-
let registrations: (void | (() => void))[] = [];
|
|
109
|
-
state.onExit(() => {
|
|
110
|
-
for (const registration of registrations) {
|
|
135
|
+
let registrations: (void | (() => void) | Promise<void | (() => void)>)[] =
|
|
136
|
+
[];
|
|
137
|
+
state.onExit(async () => {
|
|
138
|
+
for (const r of registrations) {
|
|
139
|
+
const registration = await r;
|
|
111
140
|
registration?.();
|
|
112
141
|
}
|
|
113
142
|
});
|
|
@@ -115,40 +144,51 @@ export function newStateHandlers<
|
|
|
115
144
|
const modules = await loader(state);
|
|
116
145
|
if (!modules?.length) return;
|
|
117
146
|
for (const module of modules) {
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (event) {
|
|
129
|
-
// TODO: Check that the event is available in this state;
|
|
130
|
-
state.process.dispatch(event);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
const removeTrigger = module?.trigger?.(stateContext, (event) =>
|
|
134
|
-
notifyTrigger(event),
|
|
135
|
-
);
|
|
136
|
-
registrations.push(() => (notifyTrigger = () => {}));
|
|
137
|
-
removeTrigger && registrations.push(removeTrigger);
|
|
147
|
+
const contextKey: unknown = module.context;
|
|
148
|
+
// Create state context or get it from the local cache
|
|
149
|
+
let stateContext = contextCache.get(contextKey);
|
|
150
|
+
if (!stateContext) {
|
|
151
|
+
stateContext = newModuleContext(module, ...stack);
|
|
152
|
+
contextCache.set(contextKey, stateContext);
|
|
153
|
+
registrations.push(() => {
|
|
154
|
+
contextCache.delete(contextKey);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
138
157
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
// Initialize triggers
|
|
159
|
+
if (module.trigger && !triggers.has(module.trigger)) {
|
|
160
|
+
const process = state.process;
|
|
161
|
+
const iterator = module.trigger(stateContext);
|
|
162
|
+
let finalize: undefined | (() => void);
|
|
163
|
+
const end = new Promise<{ done: boolean; value?: string }>(
|
|
164
|
+
(r) => (finalize = () => r({ done: true })),
|
|
165
|
+
);
|
|
166
|
+
registrations.push(() => triggers.delete(module.trigger));
|
|
167
|
+
registrations.push(finalize);
|
|
168
|
+
registrations.push(() => iterator?.return?.(void 0));
|
|
169
|
+
(async () => {
|
|
170
|
+
try {
|
|
171
|
+
while (true) {
|
|
172
|
+
const { done, value: event } = await Promise.race([
|
|
173
|
+
iterator.next(),
|
|
174
|
+
end,
|
|
175
|
+
]);
|
|
176
|
+
if (done) break;
|
|
177
|
+
if (event && isStateTransitionEnabled(process, event)) {
|
|
178
|
+
process.dispatch(event);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
finalize?.();
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Run actions associated with this event
|
|
188
|
+
if (module.handler) {
|
|
189
|
+
const cleanup = module.handler(stateContext);
|
|
190
|
+
registrations.push(cleanup);
|
|
191
|
+
}
|
|
152
192
|
}
|
|
153
193
|
});
|
|
154
194
|
};
|
package/src/utils/tracer.ts
CHANGED
|
@@ -13,8 +13,10 @@ export function setStateTracer(state: FsmState, print?: Printer) {
|
|
|
13
13
|
const printLine = print || getPrinter(state);
|
|
14
14
|
printLine(`<${state?.key} event="${state.process.event}">`);
|
|
15
15
|
});
|
|
16
|
-
state.onExit(() => {
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
state.onExit(async () => {
|
|
17
|
+
await Promise.resolve().then(async () => {
|
|
18
|
+
const printLine = print || getPrinter(state);
|
|
19
|
+
printLine(`</${state.key}> <!-- event="${state.process.event}" -->`);
|
|
20
|
+
});
|
|
19
21
|
});
|
|
20
22
|
}
|