@statewalker/fsm 0.22.0 → 0.23.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/chunk-CRID7TZR.js +292 -0
- package/dist/index.d.ts +8 -10
- package/dist/index.js +18 -274
- package/dist/utils/index.d.ts +24 -5
- package/dist/utils/index.js +160 -0
- package/package.json +16 -6
- package/src/FsmBaseClass.ts +10 -10
- package/src/FsmProcess.ts +13 -11
- package/src/FsmState.ts +2 -4
- package/src/FsmStateConfig.ts +1 -1
- package/src/utils/index.ts +3 -0
- package/src/utils/newFsmStateHandler.ts +155 -0
- package/src/utils/printer.ts +5 -5
- package/src/utils/process.ts +26 -0
- package/src/utils/transitions.ts +56 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// src/FsmBaseClass.ts
|
|
2
|
+
var FsmBaseClass = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.handlers = {};
|
|
5
|
+
this.data = {};
|
|
6
|
+
bindMethods(this, "setData", "getData");
|
|
7
|
+
}
|
|
8
|
+
setData(key, value) {
|
|
9
|
+
this.data[key] = value;
|
|
10
|
+
return this;
|
|
11
|
+
}
|
|
12
|
+
getData(key) {
|
|
13
|
+
return this.data[key];
|
|
14
|
+
}
|
|
15
|
+
// ----------------------------------------------
|
|
16
|
+
// internal methods
|
|
17
|
+
_addHandler(type, handler, direct = true) {
|
|
18
|
+
const list = this.handlers[type] = this.handlers[type] || [];
|
|
19
|
+
direct ? list.push(handler) : list.unshift(handler);
|
|
20
|
+
return () => this._removeHandler(type, handler);
|
|
21
|
+
}
|
|
22
|
+
_removeHandler(type, handler) {
|
|
23
|
+
let list = this.handlers[type];
|
|
24
|
+
if (!list) return;
|
|
25
|
+
list = list.filter((h) => h !== handler);
|
|
26
|
+
if (list.length > 0) {
|
|
27
|
+
this.handlers[type] = list;
|
|
28
|
+
} else {
|
|
29
|
+
delete this.handlers[type];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async _runHandler(type, ...args) {
|
|
33
|
+
const list = this.handlers[type] || [];
|
|
34
|
+
const promises = list.map((handler) => handler(...args));
|
|
35
|
+
for (const promise of promises) {
|
|
36
|
+
try {
|
|
37
|
+
await promise;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
await this._handleError(error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async _handleError(error) {
|
|
44
|
+
console.error(error);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function bindMethods(obj, ...methods) {
|
|
48
|
+
const o = obj;
|
|
49
|
+
for (const methodName of methods) {
|
|
50
|
+
const method = o[methodName];
|
|
51
|
+
if (typeof method !== "function") continue;
|
|
52
|
+
o[methodName] = method.bind(o);
|
|
53
|
+
}
|
|
54
|
+
return obj;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/FsmState.ts
|
|
58
|
+
var FsmState = class extends FsmBaseClass {
|
|
59
|
+
constructor(process, parent, key, descriptor) {
|
|
60
|
+
super();
|
|
61
|
+
this.process = process;
|
|
62
|
+
this.key = key;
|
|
63
|
+
this.parent = parent;
|
|
64
|
+
this.descriptor = descriptor;
|
|
65
|
+
bindMethods(
|
|
66
|
+
this,
|
|
67
|
+
"onEnter",
|
|
68
|
+
"onExit",
|
|
69
|
+
"dump",
|
|
70
|
+
"restore",
|
|
71
|
+
"useData",
|
|
72
|
+
"onStateError"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
onEnter(handler) {
|
|
76
|
+
return this._addHandler("onEnter", handler, true);
|
|
77
|
+
}
|
|
78
|
+
onExit(handler) {
|
|
79
|
+
return this._addHandler("onExit", handler, false);
|
|
80
|
+
}
|
|
81
|
+
onStateError(handler) {
|
|
82
|
+
return this._addHandler("onStateError", handler);
|
|
83
|
+
}
|
|
84
|
+
dump(handler) {
|
|
85
|
+
return this._addHandler("dump", handler, true);
|
|
86
|
+
}
|
|
87
|
+
restore(handler) {
|
|
88
|
+
return this._addHandler("restore", handler, true);
|
|
89
|
+
}
|
|
90
|
+
getData(key, recursive = true) {
|
|
91
|
+
return this.data[key] ?? (recursive ? this.parent?.getData(key, recursive) : void 0);
|
|
92
|
+
}
|
|
93
|
+
useData(key) {
|
|
94
|
+
return [
|
|
95
|
+
(recursive = true) => this.getData(key, recursive),
|
|
96
|
+
(value) => this.setData(key, value)
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
async _handleError(error) {
|
|
100
|
+
await this._runHandler("onStateError", this, error);
|
|
101
|
+
await this.process._handleStateError(this, error);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/FsmStateConfig.ts
|
|
106
|
+
var STATE_ANY = "*";
|
|
107
|
+
var STATE_INITIAL = "";
|
|
108
|
+
var STATE_FINAL = "";
|
|
109
|
+
var EVENT_ANY = "*";
|
|
110
|
+
var EVENT_EMPTY = "";
|
|
111
|
+
|
|
112
|
+
// src/FsmStateDescriptor.ts
|
|
113
|
+
var FsmStateDescriptor = class _FsmStateDescriptor {
|
|
114
|
+
constructor() {
|
|
115
|
+
this.transitions = {};
|
|
116
|
+
this.states = {};
|
|
117
|
+
}
|
|
118
|
+
static build(config) {
|
|
119
|
+
const descriptor = new _FsmStateDescriptor();
|
|
120
|
+
for (const [from, event, to] of config.transitions || []) {
|
|
121
|
+
const index = descriptor.transitions[from] = descriptor.transitions[from] || {};
|
|
122
|
+
index[event] = to;
|
|
123
|
+
}
|
|
124
|
+
if (config.states) {
|
|
125
|
+
for (const substateConfig of config.states) {
|
|
126
|
+
descriptor.states[substateConfig.key] = this.build(substateConfig);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return descriptor;
|
|
130
|
+
}
|
|
131
|
+
getTargetStateKey(stateKey, eventKey) {
|
|
132
|
+
const pairs = [
|
|
133
|
+
[stateKey, eventKey],
|
|
134
|
+
[STATE_ANY, eventKey],
|
|
135
|
+
[stateKey, EVENT_ANY],
|
|
136
|
+
[STATE_ANY, EVENT_ANY]
|
|
137
|
+
];
|
|
138
|
+
let targetKey;
|
|
139
|
+
for (let i = 0, len = pairs.length; targetKey === void 0 && i < len; i++) {
|
|
140
|
+
const [stateKey2, eventKey2] = pairs[i];
|
|
141
|
+
const stateTransitions = this.transitions[stateKey2];
|
|
142
|
+
if (!stateTransitions) continue;
|
|
143
|
+
targetKey = stateTransitions[eventKey2];
|
|
144
|
+
}
|
|
145
|
+
return targetKey !== void 0 ? targetKey : STATE_FINAL;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/FsmProcess.ts
|
|
150
|
+
var STATUS_NONE = 0;
|
|
151
|
+
var STATUS_FIRST = 1;
|
|
152
|
+
var STATUS_NEXT = 2;
|
|
153
|
+
var STATUS_LEAF = 4;
|
|
154
|
+
var STATUS_LAST = 8;
|
|
155
|
+
var STATUS_FINISHED = 16;
|
|
156
|
+
var STATUS_ENTER = STATUS_FIRST | STATUS_NEXT;
|
|
157
|
+
var STATUS_EXIT = STATUS_LEAF | STATUS_LAST;
|
|
158
|
+
var FsmProcess = class extends FsmBaseClass {
|
|
159
|
+
constructor(config) {
|
|
160
|
+
super();
|
|
161
|
+
this.status = 0;
|
|
162
|
+
this.rootDescriptor = FsmStateDescriptor.build(config);
|
|
163
|
+
this.config = config;
|
|
164
|
+
bindMethods(this, "dispatch", "dump", "restore");
|
|
165
|
+
}
|
|
166
|
+
async shutdown(event) {
|
|
167
|
+
while (this.state) {
|
|
168
|
+
this.event = event;
|
|
169
|
+
this.status = STATUS_FINISHED;
|
|
170
|
+
await this.state?._runHandler("onExit", this.state);
|
|
171
|
+
this.state = this.state.parent;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async dispatch(event, mask = STATUS_LEAF) {
|
|
175
|
+
this.event = event;
|
|
176
|
+
while (true) {
|
|
177
|
+
if (this.status & STATUS_EXIT) {
|
|
178
|
+
await this.state?._runHandler("onExit", this.state);
|
|
179
|
+
}
|
|
180
|
+
if (!this._update()) return false;
|
|
181
|
+
if (this.status & STATUS_ENTER) {
|
|
182
|
+
await this.state?._runHandler("onEnter", this.state);
|
|
183
|
+
}
|
|
184
|
+
if (this.status & mask) return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async dump(...args) {
|
|
188
|
+
const dumpState = async (state) => {
|
|
189
|
+
const stateDump = {
|
|
190
|
+
key: state.key,
|
|
191
|
+
data: {}
|
|
192
|
+
};
|
|
193
|
+
await state._runHandler("dump", state, stateDump.data, ...args);
|
|
194
|
+
return stateDump;
|
|
195
|
+
};
|
|
196
|
+
const dumpStates = async (state, stack = []) => {
|
|
197
|
+
if (!state) return stack;
|
|
198
|
+
state.parent && await dumpStates(state.parent, stack);
|
|
199
|
+
stack.push(await dumpState(state));
|
|
200
|
+
return stack;
|
|
201
|
+
};
|
|
202
|
+
const dump = {
|
|
203
|
+
status: this.status,
|
|
204
|
+
event: this.event,
|
|
205
|
+
stack: await dumpStates(this.state)
|
|
206
|
+
};
|
|
207
|
+
return dump;
|
|
208
|
+
}
|
|
209
|
+
async restore(dump, ...args) {
|
|
210
|
+
this.status = dump.status || 0;
|
|
211
|
+
this.event = dump.event;
|
|
212
|
+
this.state = void 0;
|
|
213
|
+
for (let i = 0; i < dump.stack.length; i++) {
|
|
214
|
+
const stateDump = dump.stack[i];
|
|
215
|
+
this.state = this.state ? this._newSubstate(this.state, stateDump.key) : this._newState(void 0, stateDump.key, this.rootDescriptor);
|
|
216
|
+
await this.state._runHandler(
|
|
217
|
+
"restore",
|
|
218
|
+
this.state,
|
|
219
|
+
stateDump.data,
|
|
220
|
+
...args
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
onStateCreate(handler) {
|
|
226
|
+
return this._addHandler("onStateCreate", handler, true);
|
|
227
|
+
}
|
|
228
|
+
onStateError(handler) {
|
|
229
|
+
return this._addHandler("onStateError", handler);
|
|
230
|
+
}
|
|
231
|
+
async _handleStateError(state, error) {
|
|
232
|
+
await this._runHandler("onStateError", state, error);
|
|
233
|
+
this._handleError(error);
|
|
234
|
+
}
|
|
235
|
+
_newState(parent, key, descriptor) {
|
|
236
|
+
const state = new FsmState(this, parent, key, descriptor);
|
|
237
|
+
this._runHandler("onStateCreate", state);
|
|
238
|
+
return state;
|
|
239
|
+
}
|
|
240
|
+
_getSubstate(parent, prevStateKey) {
|
|
241
|
+
if (!parent) return;
|
|
242
|
+
const toState = parent.descriptor?.getTargetStateKey(
|
|
243
|
+
prevStateKey || STATE_INITIAL,
|
|
244
|
+
this.event || EVENT_EMPTY
|
|
245
|
+
) || STATE_FINAL;
|
|
246
|
+
if (!toState) return;
|
|
247
|
+
return this._newSubstate(parent, toState);
|
|
248
|
+
}
|
|
249
|
+
_newSubstate(parent, toState) {
|
|
250
|
+
let descriptor;
|
|
251
|
+
for (let state = parent; !descriptor && state; state = state.parent) {
|
|
252
|
+
descriptor = state.descriptor?.states[toState];
|
|
253
|
+
}
|
|
254
|
+
return this._newState(parent, toState, descriptor);
|
|
255
|
+
}
|
|
256
|
+
_update() {
|
|
257
|
+
if (this.status & STATUS_FINISHED) return false;
|
|
258
|
+
const nextState = this.status !== STATUS_NONE ? this.status & STATUS_ENTER ? this._getSubstate(this.state, STATE_INITIAL) : this._getSubstate(this.state?.parent, this.state?.key) : this._newState(void 0, this.config.key, this.rootDescriptor);
|
|
259
|
+
if (nextState !== void 0) {
|
|
260
|
+
this.state = nextState;
|
|
261
|
+
this.status = this.status & STATUS_EXIT ? STATUS_NEXT : STATUS_FIRST;
|
|
262
|
+
} else {
|
|
263
|
+
if (this.status & STATUS_EXIT) {
|
|
264
|
+
this.state = this.state?.parent;
|
|
265
|
+
this.status = STATUS_LAST;
|
|
266
|
+
} else {
|
|
267
|
+
this.status = STATUS_LEAF;
|
|
268
|
+
}
|
|
269
|
+
if (!this.state) this.status = STATUS_FINISHED;
|
|
270
|
+
}
|
|
271
|
+
return !(this.status & STATUS_FINISHED);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export {
|
|
276
|
+
FsmState,
|
|
277
|
+
STATE_ANY,
|
|
278
|
+
STATE_INITIAL,
|
|
279
|
+
STATE_FINAL,
|
|
280
|
+
EVENT_ANY,
|
|
281
|
+
EVENT_EMPTY,
|
|
282
|
+
FsmStateDescriptor,
|
|
283
|
+
STATUS_NONE,
|
|
284
|
+
STATUS_FIRST,
|
|
285
|
+
STATUS_NEXT,
|
|
286
|
+
STATUS_LEAF,
|
|
287
|
+
STATUS_LAST,
|
|
288
|
+
STATUS_FINISHED,
|
|
289
|
+
STATUS_ENTER,
|
|
290
|
+
STATUS_EXIT,
|
|
291
|
+
FsmProcess
|
|
292
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ 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
|
-
_runHandlerSync(type: string, ...args: unknown[]): any[];
|
|
10
9
|
_runHandler(type: string, ...args: unknown[]): Promise<void>;
|
|
11
10
|
_handleError(error: Error | unknown): Promise<void>;
|
|
12
11
|
}
|
|
@@ -22,7 +21,7 @@ type FsmStateConfig = {
|
|
|
22
21
|
key: FsmStateKey;
|
|
23
22
|
transitions?: [from: FsmStateKey, event: FsmEventKey, to: FsmStateKey][];
|
|
24
23
|
states?: FsmStateConfig[];
|
|
25
|
-
} & Record<string,
|
|
24
|
+
} & Record<string, unknown>;
|
|
26
25
|
|
|
27
26
|
declare class FsmStateDescriptor {
|
|
28
27
|
transitions: Record<string, Record<string, string>>;
|
|
@@ -31,14 +30,13 @@ declare class FsmStateDescriptor {
|
|
|
31
30
|
getTargetStateKey(stateKey: string, eventKey: string): string;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
type FsmStateDump = Record<string,
|
|
33
|
+
type FsmStateDump = Record<string, unknown> & {
|
|
35
34
|
key: string;
|
|
36
35
|
data: Record<string, unknown>;
|
|
37
36
|
};
|
|
38
37
|
type FsmStateHandler = (state: FsmState, ...args: unknown[]) => void | Promise<void>;
|
|
39
|
-
type FsmStateSyncHandler = (state: FsmState, ...args: unknown[]) => void;
|
|
40
38
|
type FsmStateDumpHandler = (state: FsmState, dump: FsmStateDump) => void | Promise<void>;
|
|
41
|
-
type FsmStateErrorHandler = (state: FsmState, error:
|
|
39
|
+
type FsmStateErrorHandler = (state: FsmState, error: unknown) => void | Promise<void>;
|
|
42
40
|
declare class FsmState extends FsmBaseClass {
|
|
43
41
|
process: FsmProcess;
|
|
44
42
|
key: string;
|
|
@@ -63,8 +61,8 @@ declare const STATUS_LAST = 8;
|
|
|
63
61
|
declare const STATUS_FINISHED = 16;
|
|
64
62
|
declare const STATUS_ENTER: number;
|
|
65
63
|
declare const STATUS_EXIT: number;
|
|
66
|
-
type FsmProcessHandler = (process: FsmProcess, ...args:
|
|
67
|
-
type FsmProcessDump = Record<string,
|
|
64
|
+
type FsmProcessHandler = (process: FsmProcess, ...args: unknown[]) => void | Promise<void>;
|
|
65
|
+
type FsmProcessDump = Record<string, unknown> & {
|
|
68
66
|
status: number;
|
|
69
67
|
event?: string;
|
|
70
68
|
stack: FsmStateDump[];
|
|
@@ -81,8 +79,8 @@ declare class FsmProcess extends FsmBaseClass {
|
|
|
81
79
|
dispatch(event: string, mask?: number): Promise<boolean>;
|
|
82
80
|
dump(...args: unknown[]): Promise<FsmProcessDump>;
|
|
83
81
|
restore(dump: FsmProcessDump, ...args: unknown[]): Promise<this>;
|
|
84
|
-
onStateCreate(handler:
|
|
85
|
-
onStateError(handler: (state: FsmState, error:
|
|
82
|
+
onStateCreate(handler: FsmStateHandler): () => void;
|
|
83
|
+
onStateError(handler: (state: FsmState, error: unknown) => void | Promise<void>): () => void;
|
|
86
84
|
_handleStateError(state: FsmState, error: Error | unknown): Promise<void>;
|
|
87
85
|
_newState(parent: FsmState | undefined, key: string, descriptor: FsmStateDescriptor | undefined): FsmState;
|
|
88
86
|
_getSubstate(parent: FsmState | undefined, prevStateKey: string | undefined): FsmState | undefined;
|
|
@@ -90,4 +88,4 @@ declare class FsmProcess extends FsmBaseClass {
|
|
|
90
88
|
_update(): boolean;
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
export { EVENT_ANY, EVENT_EMPTY, type FsmEventKey, FsmProcess, type FsmProcessDump, type FsmProcessDumpHandler, type FsmProcessHandler, FsmState, type FsmStateConfig, FsmStateDescriptor, type FsmStateDump, type FsmStateDumpHandler, type FsmStateErrorHandler, type FsmStateHandler, type FsmStateKey,
|
|
91
|
+
export { EVENT_ANY, EVENT_EMPTY, type FsmEventKey, FsmProcess, type FsmProcessDump, type FsmProcessDumpHandler, type FsmProcessHandler, FsmState, type FsmStateConfig, FsmStateDescriptor, type FsmStateDump, type FsmStateDumpHandler, type FsmStateErrorHandler, type FsmStateHandler, type FsmStateKey, STATE_ANY, STATE_FINAL, STATE_INITIAL, STATUS_ENTER, STATUS_EXIT, STATUS_FINISHED, STATUS_FIRST, STATUS_LAST, STATUS_LEAF, STATUS_NEXT, STATUS_NONE };
|
package/dist/index.js
CHANGED
|
@@ -1,277 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
direct ? list.push(handler) : list.unshift(handler);
|
|
20
|
-
return () => this._removeHandler(type, handler);
|
|
21
|
-
}
|
|
22
|
-
_removeHandler(type, handler) {
|
|
23
|
-
let list = this.handlers[type];
|
|
24
|
-
if (!list) return;
|
|
25
|
-
list = list.filter((h) => h !== handler);
|
|
26
|
-
if (list.length > 0) {
|
|
27
|
-
this.handlers[type] = list;
|
|
28
|
-
} else {
|
|
29
|
-
delete this.handlers[type];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
_runHandlerSync(type, ...args) {
|
|
33
|
-
const list = this.handlers[type] || [];
|
|
34
|
-
return list.map((handler) => handler(...args));
|
|
35
|
-
}
|
|
36
|
-
async _runHandler(type, ...args) {
|
|
37
|
-
const promises = this._runHandlerSync(type, ...args);
|
|
38
|
-
for (const promise of promises) {
|
|
39
|
-
try {
|
|
40
|
-
await promise;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
await this._handleError(error);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
async _handleError(error) {
|
|
47
|
-
console.error(error);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
function bindMethods(obj, ...methods) {
|
|
51
|
-
const o = obj;
|
|
52
|
-
methods.forEach((methodName) => {
|
|
53
|
-
o[methodName] = o[methodName].bind(o);
|
|
54
|
-
});
|
|
55
|
-
return obj;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// src/FsmState.ts
|
|
59
|
-
var FsmState = class extends FsmBaseClass {
|
|
60
|
-
constructor(process, parent, key, descriptor) {
|
|
61
|
-
super();
|
|
62
|
-
this.process = process;
|
|
63
|
-
this.key = key;
|
|
64
|
-
this.parent = parent;
|
|
65
|
-
this.descriptor = descriptor;
|
|
66
|
-
bindMethods(
|
|
67
|
-
this,
|
|
68
|
-
"onEnter",
|
|
69
|
-
"onExit",
|
|
70
|
-
"dump",
|
|
71
|
-
"restore",
|
|
72
|
-
"useData",
|
|
73
|
-
"onStateError"
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
onEnter(handler) {
|
|
77
|
-
return this._addHandler("onEnter", handler, true);
|
|
78
|
-
}
|
|
79
|
-
onExit(handler) {
|
|
80
|
-
return this._addHandler("onExit", handler, false);
|
|
81
|
-
}
|
|
82
|
-
onStateError(handler) {
|
|
83
|
-
return this._addHandler("onStateError", handler);
|
|
84
|
-
}
|
|
85
|
-
dump(handler) {
|
|
86
|
-
return this._addHandler("dump", handler, true);
|
|
87
|
-
}
|
|
88
|
-
restore(handler) {
|
|
89
|
-
return this._addHandler("restore", handler, true);
|
|
90
|
-
}
|
|
91
|
-
getData(key, recursive = true) {
|
|
92
|
-
return this.data[key] ?? (recursive ? this.parent?.getData(key, recursive) : void 0);
|
|
93
|
-
}
|
|
94
|
-
useData(key) {
|
|
95
|
-
return [
|
|
96
|
-
(recursive = true) => this.getData(key, recursive),
|
|
97
|
-
(value) => this.setData(key, value)
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
|
-
async _handleError(error) {
|
|
101
|
-
await this._runHandler("onStateError", this, error);
|
|
102
|
-
await this.process._handleStateError(this, error);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// src/FsmStateConfig.ts
|
|
107
|
-
var STATE_ANY = "*";
|
|
108
|
-
var STATE_INITIAL = "";
|
|
109
|
-
var STATE_FINAL = "";
|
|
110
|
-
var EVENT_ANY = "*";
|
|
111
|
-
var EVENT_EMPTY = "";
|
|
112
|
-
|
|
113
|
-
// src/FsmStateDescriptor.ts
|
|
114
|
-
var FsmStateDescriptor = class _FsmStateDescriptor {
|
|
115
|
-
constructor() {
|
|
116
|
-
this.transitions = {};
|
|
117
|
-
this.states = {};
|
|
118
|
-
}
|
|
119
|
-
static build(config) {
|
|
120
|
-
const descriptor = new _FsmStateDescriptor();
|
|
121
|
-
for (const [from, event, to] of config.transitions || []) {
|
|
122
|
-
const index = descriptor.transitions[from] = descriptor.transitions[from] || {};
|
|
123
|
-
index[event] = to;
|
|
124
|
-
}
|
|
125
|
-
if (config.states) {
|
|
126
|
-
for (const substateConfig of config.states) {
|
|
127
|
-
descriptor.states[substateConfig.key] = this.build(substateConfig);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return descriptor;
|
|
131
|
-
}
|
|
132
|
-
getTargetStateKey(stateKey, eventKey) {
|
|
133
|
-
const pairs = [
|
|
134
|
-
[stateKey, eventKey],
|
|
135
|
-
[STATE_ANY, eventKey],
|
|
136
|
-
[stateKey, EVENT_ANY],
|
|
137
|
-
[STATE_ANY, EVENT_ANY]
|
|
138
|
-
];
|
|
139
|
-
let targetKey;
|
|
140
|
-
for (let i = 0, len = pairs.length; targetKey === void 0 && i < len; i++) {
|
|
141
|
-
const [stateKey2, eventKey2] = pairs[i];
|
|
142
|
-
const stateTransitions = this.transitions[stateKey2];
|
|
143
|
-
if (!stateTransitions) continue;
|
|
144
|
-
targetKey = stateTransitions[eventKey2];
|
|
145
|
-
}
|
|
146
|
-
return targetKey !== void 0 ? targetKey : STATE_FINAL;
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// src/FsmProcess.ts
|
|
151
|
-
var STATUS_NONE = 0;
|
|
152
|
-
var STATUS_FIRST = 1;
|
|
153
|
-
var STATUS_NEXT = 2;
|
|
154
|
-
var STATUS_LEAF = 4;
|
|
155
|
-
var STATUS_LAST = 8;
|
|
156
|
-
var STATUS_FINISHED = 16;
|
|
157
|
-
var STATUS_ENTER = STATUS_FIRST | STATUS_NEXT;
|
|
158
|
-
var STATUS_EXIT = STATUS_LEAF | STATUS_LAST;
|
|
159
|
-
var FsmProcess = class extends FsmBaseClass {
|
|
160
|
-
constructor(config) {
|
|
161
|
-
super();
|
|
162
|
-
this.status = 0;
|
|
163
|
-
this.rootDescriptor = FsmStateDescriptor.build(config);
|
|
164
|
-
this.config = config;
|
|
165
|
-
bindMethods(this, "dispatch", "dump", "restore");
|
|
166
|
-
}
|
|
167
|
-
async shutdown(event) {
|
|
168
|
-
while (this.state) {
|
|
169
|
-
this.event = event;
|
|
170
|
-
this.status = STATUS_FINISHED;
|
|
171
|
-
await this.state?._runHandler("onExit", this.state);
|
|
172
|
-
this.state = this.state.parent;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
async dispatch(event, mask = STATUS_LEAF) {
|
|
176
|
-
this.event = event;
|
|
177
|
-
while (true) {
|
|
178
|
-
if (this.status & STATUS_EXIT) {
|
|
179
|
-
await this.state?._runHandler("onExit", this.state);
|
|
180
|
-
}
|
|
181
|
-
if (!this._update()) return false;
|
|
182
|
-
if (this.status & STATUS_ENTER) {
|
|
183
|
-
await this.state?._runHandler("onEnter", this.state);
|
|
184
|
-
}
|
|
185
|
-
if (this.status & mask) return true;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
async dump(...args) {
|
|
189
|
-
const dumpState = async (state) => {
|
|
190
|
-
const stateDump = {
|
|
191
|
-
key: state.key,
|
|
192
|
-
data: {}
|
|
193
|
-
};
|
|
194
|
-
await state._runHandler("dump", state, stateDump.data, ...args);
|
|
195
|
-
return stateDump;
|
|
196
|
-
};
|
|
197
|
-
const dumpStates = async (state, stack = []) => {
|
|
198
|
-
if (!state) return stack;
|
|
199
|
-
state.parent && await dumpStates(state.parent, stack);
|
|
200
|
-
stack.push(await dumpState(state));
|
|
201
|
-
return stack;
|
|
202
|
-
};
|
|
203
|
-
const dump = {
|
|
204
|
-
status: this.status,
|
|
205
|
-
event: this.event,
|
|
206
|
-
stack: await dumpStates(this.state)
|
|
207
|
-
};
|
|
208
|
-
return dump;
|
|
209
|
-
}
|
|
210
|
-
async restore(dump, ...args) {
|
|
211
|
-
this.status = dump.status || 0;
|
|
212
|
-
this.event = dump.event;
|
|
213
|
-
this.state = void 0;
|
|
214
|
-
for (let i = 0; i < dump.stack.length; i++) {
|
|
215
|
-
const stateDump = dump.stack[i];
|
|
216
|
-
this.state = this.state ? this._newSubstate(this.state, stateDump.key) : this._newState(void 0, stateDump.key, this.rootDescriptor);
|
|
217
|
-
await this.state._runHandler(
|
|
218
|
-
"restore",
|
|
219
|
-
this.state,
|
|
220
|
-
stateDump.data,
|
|
221
|
-
...args
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
return this;
|
|
225
|
-
}
|
|
226
|
-
onStateCreate(handler) {
|
|
227
|
-
return this._addHandler("onStateCreate", handler, true);
|
|
228
|
-
}
|
|
229
|
-
onStateError(handler) {
|
|
230
|
-
return this._addHandler("onStateError", handler);
|
|
231
|
-
}
|
|
232
|
-
async _handleStateError(state, error) {
|
|
233
|
-
await this._runHandler("onStateError", state, error);
|
|
234
|
-
this._handleError(error);
|
|
235
|
-
}
|
|
236
|
-
_newState(parent, key, descriptor) {
|
|
237
|
-
const state = new FsmState(this, parent, key, descriptor);
|
|
238
|
-
this._runHandlerSync("onStateCreate", state);
|
|
239
|
-
return state;
|
|
240
|
-
}
|
|
241
|
-
_getSubstate(parent, prevStateKey) {
|
|
242
|
-
if (!parent) return;
|
|
243
|
-
const toState = parent.descriptor?.getTargetStateKey(
|
|
244
|
-
prevStateKey || STATE_INITIAL,
|
|
245
|
-
this.event || EVENT_EMPTY
|
|
246
|
-
) || STATE_FINAL;
|
|
247
|
-
if (!toState) return;
|
|
248
|
-
return this._newSubstate(parent, toState);
|
|
249
|
-
}
|
|
250
|
-
_newSubstate(parent, toState) {
|
|
251
|
-
let descriptor;
|
|
252
|
-
for (let state = parent; !descriptor && state; state = state.parent) {
|
|
253
|
-
descriptor = state.descriptor?.states[toState];
|
|
254
|
-
}
|
|
255
|
-
return this._newState(parent, toState, descriptor);
|
|
256
|
-
}
|
|
257
|
-
_update() {
|
|
258
|
-
if (this.status & STATUS_FINISHED) return false;
|
|
259
|
-
const nextState = this.status !== STATUS_NONE ? this.status & STATUS_ENTER ? this._getSubstate(this.state, STATE_INITIAL) : this._getSubstate(this.state?.parent, this.state?.key) : this._newState(void 0, this.config.key, this.rootDescriptor);
|
|
260
|
-
if (nextState !== void 0) {
|
|
261
|
-
this.state = nextState;
|
|
262
|
-
this.status = this.status & STATUS_EXIT ? STATUS_NEXT : STATUS_FIRST;
|
|
263
|
-
} else {
|
|
264
|
-
if (this.status & STATUS_EXIT) {
|
|
265
|
-
this.state = this.state?.parent;
|
|
266
|
-
this.status = STATUS_LAST;
|
|
267
|
-
} else {
|
|
268
|
-
this.status = STATUS_LEAF;
|
|
269
|
-
}
|
|
270
|
-
if (!this.state) this.status = STATUS_FINISHED;
|
|
271
|
-
}
|
|
272
|
-
return !(this.status & STATUS_FINISHED);
|
|
273
|
-
}
|
|
274
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
EVENT_ANY,
|
|
3
|
+
EVENT_EMPTY,
|
|
4
|
+
FsmProcess,
|
|
5
|
+
FsmState,
|
|
6
|
+
FsmStateDescriptor,
|
|
7
|
+
STATE_ANY,
|
|
8
|
+
STATE_FINAL,
|
|
9
|
+
STATE_INITIAL,
|
|
10
|
+
STATUS_ENTER,
|
|
11
|
+
STATUS_EXIT,
|
|
12
|
+
STATUS_FINISHED,
|
|
13
|
+
STATUS_FIRST,
|
|
14
|
+
STATUS_LAST,
|
|
15
|
+
STATUS_LEAF,
|
|
16
|
+
STATUS_NEXT,
|
|
17
|
+
STATUS_NONE
|
|
18
|
+
} from "./chunk-CRID7TZR.js";
|
|
275
19
|
export {
|
|
276
20
|
EVENT_ANY,
|
|
277
21
|
EVENT_EMPTY,
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { FsmState, FsmStateHandler, FsmProcess } from '../index.js';
|
|
1
|
+
import { FsmState, FsmStateHandler as FsmStateHandler$1, FsmProcess, FsmStateConfig } from '../index.js';
|
|
2
2
|
|
|
3
3
|
declare const KEY_HANDLERS = "handlers";
|
|
4
4
|
declare function callStateHandlers(state: FsmState): void;
|
|
5
|
-
declare function addSubstateHandlers(state: FsmState, handlers: Record<string, FsmStateHandler>): void;
|
|
5
|
+
declare function addSubstateHandlers(state: FsmState, handlers: Record<string, FsmStateHandler$1>): void;
|
|
6
6
|
|
|
7
|
-
type Printer = (...args:
|
|
7
|
+
type Printer = (...args: unknown[]) => void;
|
|
8
8
|
type PrinterConfig = {
|
|
9
9
|
prefix?: string;
|
|
10
|
-
print?: (...args:
|
|
10
|
+
print?: (...args: unknown[]) => void;
|
|
11
11
|
lineNumbers?: boolean;
|
|
12
12
|
};
|
|
13
13
|
declare const KEY_PRINTER = "printer";
|
|
@@ -17,7 +17,26 @@ declare function setProcessPrinter(process: FsmProcess, config?: PrinterConfig):
|
|
|
17
17
|
declare function getProcessPrinter(process: FsmProcess): Printer;
|
|
18
18
|
declare function getPrinter(state: FsmState): Printer;
|
|
19
19
|
|
|
20
|
+
declare function newProcess(config: FsmStateConfig, { prefix, print, lineNumbers, }: {
|
|
21
|
+
prefix?: string;
|
|
22
|
+
print?: (...args: any[]) => void;
|
|
23
|
+
lineNumbers?: boolean;
|
|
24
|
+
}): FsmProcess;
|
|
25
|
+
|
|
20
26
|
declare function setProcessTracer(process: FsmProcess, print?: Printer): () => void;
|
|
21
27
|
declare function setStateTracer(state: FsmState, print?: Printer): void;
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
declare function isStateTransitionEnabled(process: FsmProcess, event: string): boolean;
|
|
30
|
+
declare function getStateTransitions(state?: FsmState): [from: string, event: string, to: string][];
|
|
31
|
+
|
|
32
|
+
declare function newModuleLoader<T>(baseUrl: string): (path: string) => Promise<T>;
|
|
33
|
+
declare function newHandlerLoader(baseUrl: string, loader?: (path: string) => Promise<undefined | FsmStateHandler>): Promise<(state: FsmState) => Promise<FsmStateHandler[]>>;
|
|
34
|
+
type FsmStateHandler<FsmStateContext extends Record<string, unknown> = Record<string, unknown>, FsmStateStore = Record<string, unknown>> = {
|
|
35
|
+
context?: (...stack: FsmStateStore[]) => FsmStateContext;
|
|
36
|
+
trigger?: (context: FsmStateContext, listener: (event: string) => void) => void | (() => void);
|
|
37
|
+
default?: (context: FsmStateContext) => void | (() => void);
|
|
38
|
+
handler?: (context: FsmStateContext) => void | (() => void);
|
|
39
|
+
};
|
|
40
|
+
declare function newStateHandlers<FsmStateContext extends Record<string, unknown> = Record<string, unknown>, FsmStateStore = Record<string, unknown>>(loader: (state: FsmState) => Promise<FsmStateHandler[]>, newContext?: (state: FsmState, ...stack: FsmStateStore[]) => FsmStateStore): (state: FsmState) => void;
|
|
41
|
+
|
|
42
|
+
export { type FsmStateHandler, KEY_HANDLERS, KEY_PRINTER, type Printer, type PrinterConfig, addSubstateHandlers, callStateHandlers, getPrinter, getProcessPrinter, getStateTransitions, isStateTransitionEnabled, newHandlerLoader, newModuleLoader, newProcess, newStateHandlers, preparePrinter, setPrinter, setProcessPrinter, setProcessTracer, setStateTracer };
|
package/dist/utils/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FsmProcess
|
|
3
|
+
} from "../chunk-CRID7TZR.js";
|
|
4
|
+
|
|
1
5
|
// src/utils/handlers.ts
|
|
2
6
|
var KEY_HANDLERS = "handlers";
|
|
3
7
|
function callStateHandlers(state) {
|
|
@@ -63,6 +67,156 @@ function setStateTracer(state, print) {
|
|
|
63
67
|
printLine(`</${state.key}> <!-- event="${state.process.event}" -->`);
|
|
64
68
|
});
|
|
65
69
|
}
|
|
70
|
+
|
|
71
|
+
// src/utils/process.ts
|
|
72
|
+
function newProcess(config, {
|
|
73
|
+
prefix,
|
|
74
|
+
print,
|
|
75
|
+
lineNumbers = true
|
|
76
|
+
}) {
|
|
77
|
+
let process = new FsmProcess(config);
|
|
78
|
+
setProcessPrinter(process, {
|
|
79
|
+
prefix,
|
|
80
|
+
print,
|
|
81
|
+
lineNumbers
|
|
82
|
+
});
|
|
83
|
+
setProcessTracer(process);
|
|
84
|
+
return process;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/utils/transitions.ts
|
|
88
|
+
function isStateTransitionEnabled(process, event) {
|
|
89
|
+
const transitions = getStateTransitions(process.state);
|
|
90
|
+
let active = false;
|
|
91
|
+
for (const [from, ev, to] of transitions) {
|
|
92
|
+
if (ev === event) {
|
|
93
|
+
active = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return active;
|
|
98
|
+
}
|
|
99
|
+
function getStateTransitions(state) {
|
|
100
|
+
const result = [];
|
|
101
|
+
let index = {};
|
|
102
|
+
if (state) {
|
|
103
|
+
let prevStateKey = state.key;
|
|
104
|
+
for (let parent = state?.parent; parent; parent = parent.parent) {
|
|
105
|
+
if (!parent.descriptor) continue;
|
|
106
|
+
result.push(
|
|
107
|
+
...getTransitionsFromDescriptor(parent.descriptor, prevStateKey, index)
|
|
108
|
+
);
|
|
109
|
+
prevStateKey = parent.key;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result.reverse();
|
|
113
|
+
}
|
|
114
|
+
function getTransitionsFromDescriptor(descriptor, prevStateKey, index = {}) {
|
|
115
|
+
const result = [];
|
|
116
|
+
const prevStateKeys = [prevStateKey, "*"];
|
|
117
|
+
for (const prevKey of prevStateKeys) {
|
|
118
|
+
const targets = descriptor.transitions[prevKey];
|
|
119
|
+
if (targets) {
|
|
120
|
+
for (const [event, target] of Object.entries(targets)) {
|
|
121
|
+
if (index[event]) continue;
|
|
122
|
+
if (target) {
|
|
123
|
+
index[event] = true;
|
|
124
|
+
}
|
|
125
|
+
result.push([prevStateKey, event, target]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/utils/newFsmStateHandler.ts
|
|
133
|
+
function newModuleLoader(baseUrl) {
|
|
134
|
+
const cache = {};
|
|
135
|
+
return async (path) => {
|
|
136
|
+
return cache[path] = cache[path] || (async () => {
|
|
137
|
+
const suffixes = ["/", "", ".js", "/index.js"];
|
|
138
|
+
for (const suffix of suffixes) {
|
|
139
|
+
const url = new URL(path + suffix, baseUrl).pathname;
|
|
140
|
+
try {
|
|
141
|
+
return await import(url);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function newHandlerLoader(baseUrl, loader = newModuleLoader(baseUrl)) {
|
|
150
|
+
return async (state) => {
|
|
151
|
+
const stack = [];
|
|
152
|
+
for (let s = state; s; s = s.parent) {
|
|
153
|
+
stack.unshift(s.key);
|
|
154
|
+
}
|
|
155
|
+
const handlers = [];
|
|
156
|
+
let topName;
|
|
157
|
+
while (stack.length > 0) {
|
|
158
|
+
const lastSegment = stack.pop();
|
|
159
|
+
if (!topName) {
|
|
160
|
+
topName = lastSegment;
|
|
161
|
+
if (topName === void 0) break;
|
|
162
|
+
}
|
|
163
|
+
const path = [...stack, topName].join("/");
|
|
164
|
+
const handler = await loader(path);
|
|
165
|
+
handler && handlers.push(handler);
|
|
166
|
+
}
|
|
167
|
+
return handlers;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function newStateHandlers(loader, newContext = (state) => ({
|
|
171
|
+
state: state.key,
|
|
172
|
+
event: state.process.event
|
|
173
|
+
})) {
|
|
174
|
+
return (state) => {
|
|
175
|
+
const stack = [];
|
|
176
|
+
for (let s = state.parent; !!s; s = s.parent) {
|
|
177
|
+
const store2 = s.getData("context");
|
|
178
|
+
store2 && stack.push(store2);
|
|
179
|
+
}
|
|
180
|
+
const store = newContext(state, ...stack);
|
|
181
|
+
stack.unshift(store);
|
|
182
|
+
state.setData("context", store);
|
|
183
|
+
let registrations = [];
|
|
184
|
+
state.onExit(() => {
|
|
185
|
+
for (const registration of registrations) {
|
|
186
|
+
registration?.();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
state.onEnter(async () => {
|
|
190
|
+
const modules = await loader(state);
|
|
191
|
+
if (!modules?.length) return;
|
|
192
|
+
for (const module of modules) {
|
|
193
|
+
const ctx = isClass(module.context) ? new module.context(...stack) : isFunction(module.context) ? module.context(...stack) || store : store;
|
|
194
|
+
const stateContext = ctx;
|
|
195
|
+
let notifyTrigger = (event) => {
|
|
196
|
+
if (event) {
|
|
197
|
+
state.process.dispatch(event);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const removeTrigger = module?.trigger?.(
|
|
201
|
+
stateContext,
|
|
202
|
+
(event) => notifyTrigger(event)
|
|
203
|
+
);
|
|
204
|
+
registrations.push(() => notifyTrigger = () => {
|
|
205
|
+
});
|
|
206
|
+
removeTrigger && registrations.push(removeTrigger);
|
|
207
|
+
const handler = module.default || module.handler;
|
|
208
|
+
registrations.push(handler?.(stateContext));
|
|
209
|
+
}
|
|
210
|
+
function isFunction(value) {
|
|
211
|
+
return typeof value === "function";
|
|
212
|
+
}
|
|
213
|
+
function isClass(value) {
|
|
214
|
+
if (!isFunction(value)) return false;
|
|
215
|
+
return value.prototype.constructor.toString().match(/^class/);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
}
|
|
66
220
|
export {
|
|
67
221
|
KEY_HANDLERS,
|
|
68
222
|
KEY_PRINTER,
|
|
@@ -70,6 +224,12 @@ export {
|
|
|
70
224
|
callStateHandlers,
|
|
71
225
|
getPrinter,
|
|
72
226
|
getProcessPrinter,
|
|
227
|
+
getStateTransitions,
|
|
228
|
+
isStateTransitionEnabled,
|
|
229
|
+
newHandlerLoader,
|
|
230
|
+
newModuleLoader,
|
|
231
|
+
newProcess,
|
|
232
|
+
newStateHandlers,
|
|
73
233
|
preparePrinter,
|
|
74
234
|
setPrinter,
|
|
75
235
|
setProcessPrinter,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/fsm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "HFSM Implementation",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/statewalker/statewalker-fsm",
|
|
@@ -14,14 +14,23 @@
|
|
|
14
14
|
"dist",
|
|
15
15
|
"src"
|
|
16
16
|
],
|
|
17
|
+
"source": "./src/index.ts",
|
|
17
18
|
"module": "./dist/index.js",
|
|
18
19
|
"main": "./dist/index.js",
|
|
19
|
-
"jsdelivr": "./dist/index.js",
|
|
20
|
-
"unpkg": "./dist/index.js",
|
|
21
20
|
"types": "./dist/index.d.ts",
|
|
22
21
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
-
|
|
22
|
+
".": {
|
|
23
|
+
"source": "./src/index.ts",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./utils": {
|
|
29
|
+
"source": "./src/utils/index.ts",
|
|
30
|
+
"types": "./dist/utils/index.d.ts",
|
|
31
|
+
"import": "./dist/utils/index.js",
|
|
32
|
+
"default": "./dist/utils/index.js"
|
|
33
|
+
}
|
|
25
34
|
},
|
|
26
35
|
"devDependencies": {
|
|
27
36
|
"@changesets/cli": "^2.27.7",
|
|
@@ -43,7 +52,8 @@
|
|
|
43
52
|
"clean": "rm -rf dist",
|
|
44
53
|
"lint": "eslint \"**/*.(js|ts)\"",
|
|
45
54
|
"test": "vitest --run",
|
|
46
|
-
"test:watch": "vitest"
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"prepublish": "yarn dist"
|
|
47
57
|
},
|
|
48
58
|
"sideEffects": false,
|
|
49
59
|
"publishConfig": {
|
package/src/FsmBaseClass.ts
CHANGED
|
@@ -30,12 +30,10 @@ export class FsmBaseClass {
|
|
|
30
30
|
delete this.handlers[type];
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
const list = this.handlers[type] || [];
|
|
35
|
-
return list.map((handler) => handler(...args));
|
|
36
|
-
}
|
|
33
|
+
|
|
37
34
|
async _runHandler(type: string, ...args: unknown[]) {
|
|
38
|
-
const
|
|
35
|
+
const list = this.handlers[type] || [];
|
|
36
|
+
const promises = list.map((handler) => handler(...args));
|
|
39
37
|
for (const promise of promises) {
|
|
40
38
|
try {
|
|
41
39
|
await promise;
|
|
@@ -49,10 +47,12 @@ export class FsmBaseClass {
|
|
|
49
47
|
}
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
export function bindMethods<T>(obj: T, ...methods: string[]) {
|
|
53
|
-
const o = obj as
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
export function bindMethods<T>(obj: T, ...methods: (string | symbol)[]) {
|
|
51
|
+
const o = obj as Record<string | symbol, unknown>;
|
|
52
|
+
for (const methodName of methods) {
|
|
53
|
+
const method = o[methodName];
|
|
54
|
+
if (typeof method !== "function") continue;
|
|
55
|
+
o[methodName] = method.bind(o);
|
|
56
|
+
}
|
|
57
57
|
return obj;
|
|
58
58
|
}
|
package/src/FsmProcess.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FsmBaseClass, bindMethods } from "./FsmBaseClass.ts";
|
|
2
|
-
import { FsmState, FsmStateDump,
|
|
2
|
+
import { FsmState, FsmStateDump, FsmStateHandler } from "./FsmState.ts";
|
|
3
3
|
import {
|
|
4
4
|
EVENT_EMPTY,
|
|
5
5
|
FsmStateConfig,
|
|
@@ -21,10 +21,10 @@ export const STATUS_EXIT = STATUS_LEAF | STATUS_LAST;
|
|
|
21
21
|
|
|
22
22
|
export type FsmProcessHandler = (
|
|
23
23
|
process: FsmProcess,
|
|
24
|
-
...args:
|
|
24
|
+
...args: unknown[]
|
|
25
25
|
) => void | Promise<void>;
|
|
26
26
|
|
|
27
|
-
export type FsmProcessDump = Record<string,
|
|
27
|
+
export type FsmProcessDump = Record<string, unknown> & {
|
|
28
28
|
status: number;
|
|
29
29
|
event?: string;
|
|
30
30
|
stack: FsmStateDump[];
|
|
@@ -32,7 +32,7 @@ export type FsmProcessDump = Record<string, any> & {
|
|
|
32
32
|
|
|
33
33
|
export type FsmProcessDumpHandler = (
|
|
34
34
|
process: FsmProcess,
|
|
35
|
-
dump: FsmProcessDump
|
|
35
|
+
dump: FsmProcessDump,
|
|
36
36
|
) => void | Promise<void>;
|
|
37
37
|
|
|
38
38
|
export class FsmProcess extends FsmBaseClass {
|
|
@@ -83,7 +83,7 @@ export class FsmProcess extends FsmBaseClass {
|
|
|
83
83
|
};
|
|
84
84
|
const dumpStates = async (
|
|
85
85
|
state: FsmState | undefined,
|
|
86
|
-
stack: FsmStateDump[] = []
|
|
86
|
+
stack: FsmStateDump[] = [],
|
|
87
87
|
) => {
|
|
88
88
|
if (!state) return stack;
|
|
89
89
|
state.parent && (await dumpStates(state.parent, stack));
|
|
@@ -111,17 +111,19 @@ export class FsmProcess extends FsmBaseClass {
|
|
|
111
111
|
"restore",
|
|
112
112
|
this.state,
|
|
113
113
|
stateDump.data,
|
|
114
|
-
...args
|
|
114
|
+
...args,
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
return this;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
onStateCreate(handler:
|
|
120
|
+
onStateCreate(handler: FsmStateHandler) {
|
|
121
121
|
return this._addHandler("onStateCreate", handler, true);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
onStateError(
|
|
124
|
+
onStateError(
|
|
125
|
+
handler: (state: FsmState, error: unknown) => void | Promise<void>,
|
|
126
|
+
) {
|
|
125
127
|
return this._addHandler("onStateError", handler);
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -133,10 +135,10 @@ export class FsmProcess extends FsmBaseClass {
|
|
|
133
135
|
_newState(
|
|
134
136
|
parent: FsmState | undefined,
|
|
135
137
|
key: string,
|
|
136
|
-
descriptor: FsmStateDescriptor | undefined
|
|
138
|
+
descriptor: FsmStateDescriptor | undefined,
|
|
137
139
|
) {
|
|
138
140
|
const state = new FsmState(this, parent, key, descriptor);
|
|
139
|
-
this.
|
|
141
|
+
this._runHandler("onStateCreate", state);
|
|
140
142
|
return state;
|
|
141
143
|
}
|
|
142
144
|
|
|
@@ -145,7 +147,7 @@ export class FsmProcess extends FsmBaseClass {
|
|
|
145
147
|
const toState =
|
|
146
148
|
parent.descriptor?.getTargetStateKey(
|
|
147
149
|
prevStateKey || STATE_INITIAL,
|
|
148
|
-
this.event || EVENT_EMPTY
|
|
150
|
+
this.event || EVENT_EMPTY,
|
|
149
151
|
) || STATE_FINAL;
|
|
150
152
|
if (!toState) return;
|
|
151
153
|
return this._newSubstate(parent, toState);
|
package/src/FsmState.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { FsmBaseClass, bindMethods } from "./FsmBaseClass.ts";
|
|
|
2
2
|
import { FsmProcess } from "./FsmProcess.ts";
|
|
3
3
|
import { FsmStateDescriptor } from "./FsmStateDescriptor.ts";
|
|
4
4
|
|
|
5
|
-
export type FsmStateDump = Record<string,
|
|
5
|
+
export type FsmStateDump = Record<string, unknown> & {
|
|
6
6
|
key: string;
|
|
7
7
|
data: Record<string, unknown>;
|
|
8
8
|
};
|
|
@@ -11,8 +11,6 @@ export type FsmStateHandler = (
|
|
|
11
11
|
...args: unknown[]
|
|
12
12
|
) => void | Promise<void>;
|
|
13
13
|
|
|
14
|
-
export type FsmStateSyncHandler = (state: FsmState, ...args: unknown[]) => void;
|
|
15
|
-
|
|
16
14
|
export type FsmStateDumpHandler = (
|
|
17
15
|
state: FsmState,
|
|
18
16
|
dump: FsmStateDump
|
|
@@ -20,7 +18,7 @@ export type FsmStateDumpHandler = (
|
|
|
20
18
|
|
|
21
19
|
export type FsmStateErrorHandler = (
|
|
22
20
|
state: FsmState,
|
|
23
|
-
error:
|
|
21
|
+
error: unknown
|
|
24
22
|
) => void | Promise<void>;
|
|
25
23
|
|
|
26
24
|
export class FsmState extends FsmBaseClass {
|
package/src/FsmStateConfig.ts
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { FsmState } from "../FsmState";
|
|
2
|
+
|
|
3
|
+
export function newModuleLoader<T>(baseUrl: string) {
|
|
4
|
+
const cache: Record<string, Promise<T>> = {};
|
|
5
|
+
return async (path: string) => {
|
|
6
|
+
return (cache[path] =
|
|
7
|
+
cache[path] ||
|
|
8
|
+
(async () => {
|
|
9
|
+
const suffixes = ["/", "", ".js", "/index.js"];
|
|
10
|
+
for (const suffix of suffixes) {
|
|
11
|
+
const url = new URL(path + suffix, baseUrl).pathname;
|
|
12
|
+
try {
|
|
13
|
+
return (await import(url)) as T;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})());
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function newHandlerLoader(
|
|
23
|
+
baseUrl: string,
|
|
24
|
+
loader: (
|
|
25
|
+
path: string,
|
|
26
|
+
) => Promise<undefined | FsmStateHandler> = newModuleLoader(baseUrl),
|
|
27
|
+
): Promise<(state: FsmState) => Promise<FsmStateHandler[]>> {
|
|
28
|
+
return async (state: FsmState) => {
|
|
29
|
+
const stack: string[] = [];
|
|
30
|
+
for (let s: FsmState | undefined = state; s; s = s.parent) {
|
|
31
|
+
stack.unshift(s.key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handlers: FsmStateHandler[] = [];
|
|
35
|
+
let topName: undefined | string;
|
|
36
|
+
while (stack.length > 0) {
|
|
37
|
+
const lastSegment = stack.pop();
|
|
38
|
+
if (!topName) {
|
|
39
|
+
topName = lastSegment;
|
|
40
|
+
if (topName === undefined) break;
|
|
41
|
+
}
|
|
42
|
+
const path = [...stack, topName].join("/");
|
|
43
|
+
const handler = await loader(path);
|
|
44
|
+
handler && handlers.push(handler);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return handlers;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* StateContext:
|
|
53
|
+
- Each context is a function or a class
|
|
54
|
+
- It recieves the stack of state objects
|
|
55
|
+
- The returned value is used to pass to the handler function
|
|
56
|
+
* Trigger function
|
|
57
|
+
- recieves the context instance
|
|
58
|
+
- notifies about changes using the given callback
|
|
59
|
+
* Handler
|
|
60
|
+
- recieves the context
|
|
61
|
+
- starts activities on call
|
|
62
|
+
- optionally returns a cleanup function
|
|
63
|
+
- could return a promise with optional cleanup function; the state is not
|
|
64
|
+
activated until all handlers return the control
|
|
65
|
+
*/
|
|
66
|
+
export type FsmStateHandler<
|
|
67
|
+
FsmStateContext extends Record<string, unknown> = Record<string, unknown>,
|
|
68
|
+
FsmStateStore = Record<string, unknown>,
|
|
69
|
+
> = {
|
|
70
|
+
context?: (...stack: FsmStateStore[]) => FsmStateContext;
|
|
71
|
+
trigger?: (
|
|
72
|
+
context: FsmStateContext,
|
|
73
|
+
listener: (event: string) => void,
|
|
74
|
+
) => void | (() => void);
|
|
75
|
+
default?: (context: FsmStateContext) => void | (() => void);
|
|
76
|
+
handler?: (context: FsmStateContext) => void | (() => void);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function newStateHandlers<
|
|
80
|
+
FsmStateContext extends Record<string, unknown> = Record<string, unknown>,
|
|
81
|
+
FsmStateStore = Record<string, unknown>,
|
|
82
|
+
>(
|
|
83
|
+
loader: (state: FsmState) => Promise<FsmStateHandler[]>,
|
|
84
|
+
newContext: (state: FsmState, ...stack: FsmStateStore[]) => FsmStateStore = (
|
|
85
|
+
state,
|
|
86
|
+
) =>
|
|
87
|
+
({
|
|
88
|
+
state: state.key,
|
|
89
|
+
event: state.process.event,
|
|
90
|
+
}) as unknown as FsmStateStore,
|
|
91
|
+
) {
|
|
92
|
+
return (state: FsmState) => {
|
|
93
|
+
const stack: FsmStateStore[] = [];
|
|
94
|
+
for (let s: FsmState | undefined = state.parent; !!s; s = s.parent) {
|
|
95
|
+
const store = s.getData<FsmStateStore>("context");
|
|
96
|
+
store && stack.push(store);
|
|
97
|
+
}
|
|
98
|
+
const store = newContext(state, ...stack);
|
|
99
|
+
stack.unshift(store);
|
|
100
|
+
state.setData("context", store);
|
|
101
|
+
|
|
102
|
+
// const process = state.process;
|
|
103
|
+
// let processContext = process.getData<FsmProcessContext>("context");
|
|
104
|
+
// if (!processContext) {
|
|
105
|
+
// processContext = newContext(process);
|
|
106
|
+
// process.setData("context", processContext);
|
|
107
|
+
// }
|
|
108
|
+
let registrations: (void | (() => void))[] = [];
|
|
109
|
+
state.onExit(() => {
|
|
110
|
+
for (const registration of registrations) {
|
|
111
|
+
registration?.();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
state.onEnter(async () => {
|
|
115
|
+
const modules = await loader(state);
|
|
116
|
+
if (!modules?.length) return;
|
|
117
|
+
for (const module of modules) {
|
|
118
|
+
// Update state context
|
|
119
|
+
// TODO: use a stack of context objects
|
|
120
|
+
const ctx = isClass(module.context)
|
|
121
|
+
? new module.context(...stack)
|
|
122
|
+
: isFunction(module.context)
|
|
123
|
+
? module.context(...stack) || store
|
|
124
|
+
: store;
|
|
125
|
+
const stateContext = ctx as FsmStateContext;
|
|
126
|
+
// Set trigger
|
|
127
|
+
let notifyTrigger = (event: string) => {
|
|
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);
|
|
138
|
+
|
|
139
|
+
const handler = module.default || module.handler;
|
|
140
|
+
registrations.push(handler?.(stateContext));
|
|
141
|
+
}
|
|
142
|
+
function isFunction<T extends Array<unknown>, R = unknown>(
|
|
143
|
+
value: unknown,
|
|
144
|
+
): value is (...args: T) => R {
|
|
145
|
+
return typeof value === "function";
|
|
146
|
+
}
|
|
147
|
+
function isClass<T extends Array<unknown>, R = unknown>(
|
|
148
|
+
value: unknown,
|
|
149
|
+
): value is new (...args: T) => R {
|
|
150
|
+
if (!isFunction(value)) return false;
|
|
151
|
+
return value.prototype.constructor.toString().match(/^class/);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/utils/printer.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { FsmProcess } from "../FsmProcess.ts";
|
|
2
2
|
import type { FsmState } from "../FsmState.ts";
|
|
3
|
-
export type Printer = (...args:
|
|
3
|
+
export type Printer = (...args: unknown[]) => void;
|
|
4
4
|
export type PrinterConfig = {
|
|
5
5
|
prefix?: string;
|
|
6
|
-
print?: (...args:
|
|
6
|
+
print?: (...args: unknown[]) => void;
|
|
7
7
|
lineNumbers?: boolean;
|
|
8
8
|
};
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ export const KEY_PRINTER = "printer";
|
|
|
11
11
|
|
|
12
12
|
export function preparePrinter(
|
|
13
13
|
process: FsmProcess,
|
|
14
|
-
{ prefix = "", print = console.log, lineNumbers = false }: PrinterConfig
|
|
14
|
+
{ prefix = "", print = console.log, lineNumbers = false }: PrinterConfig,
|
|
15
15
|
): Printer {
|
|
16
16
|
let lineCounter = 0;
|
|
17
17
|
const shift = () => {
|
|
@@ -22,7 +22,7 @@ export function preparePrinter(
|
|
|
22
22
|
return prefix;
|
|
23
23
|
};
|
|
24
24
|
const getPrefix = lineNumbers ? () => `[${++lineCounter}]${shift()}` : shift;
|
|
25
|
-
const printer = (...args:
|
|
25
|
+
const printer = (...args: unknown[]) => print(prefix, getPrefix(), ...args);
|
|
26
26
|
return printer;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -33,7 +33,7 @@ export function setPrinter(state: FsmState, config: PrinterConfig = {}) {
|
|
|
33
33
|
|
|
34
34
|
export function setProcessPrinter(
|
|
35
35
|
process: FsmProcess,
|
|
36
|
-
config: PrinterConfig = {}
|
|
36
|
+
config: PrinterConfig = {},
|
|
37
37
|
) {
|
|
38
38
|
const printer = preparePrinter(process, config);
|
|
39
39
|
process.setData(KEY_PRINTER, printer);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FsmProcess } from "../FsmProcess.ts";
|
|
2
|
+
import type { FsmStateConfig } from "../FsmStateConfig.ts";
|
|
3
|
+
import { setProcessPrinter } from "./printer.ts";
|
|
4
|
+
import { setProcessTracer } from "./tracer.ts";
|
|
5
|
+
|
|
6
|
+
export function newProcess(
|
|
7
|
+
config: FsmStateConfig,
|
|
8
|
+
{
|
|
9
|
+
prefix,
|
|
10
|
+
print,
|
|
11
|
+
lineNumbers = true,
|
|
12
|
+
}: {
|
|
13
|
+
prefix?: string;
|
|
14
|
+
print?: (...args: any[]) => void;
|
|
15
|
+
lineNumbers?: boolean;
|
|
16
|
+
},
|
|
17
|
+
): FsmProcess {
|
|
18
|
+
let process = new FsmProcess(config);
|
|
19
|
+
setProcessPrinter(process, {
|
|
20
|
+
prefix,
|
|
21
|
+
print,
|
|
22
|
+
lineNumbers,
|
|
23
|
+
});
|
|
24
|
+
setProcessTracer(process);
|
|
25
|
+
return process;
|
|
26
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { FsmProcess } from "../FsmProcess.ts";
|
|
2
|
+
import type { FsmState } from "../FsmState.ts";
|
|
3
|
+
import type { FsmStateDescriptor } from "../FsmStateDescriptor.ts";
|
|
4
|
+
|
|
5
|
+
export function isStateTransitionEnabled(process: FsmProcess, event: string) {
|
|
6
|
+
const transitions = getStateTransitions(process.state);
|
|
7
|
+
let active = false;
|
|
8
|
+
for (const [from, ev, to] of transitions) {
|
|
9
|
+
if (ev === event) {
|
|
10
|
+
active = true;
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return active;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getStateTransitions(
|
|
18
|
+
state?: FsmState,
|
|
19
|
+
): [from: string, event: string, to: string][] {
|
|
20
|
+
const result: [from: string, event: string, to: string][] = [];
|
|
21
|
+
let index = {};
|
|
22
|
+
if (state) {
|
|
23
|
+
let prevStateKey = state.key;
|
|
24
|
+
for (let parent = state?.parent; parent; parent = parent.parent) {
|
|
25
|
+
if (!parent.descriptor) continue;
|
|
26
|
+
result.push(
|
|
27
|
+
...getTransitionsFromDescriptor(parent.descriptor, prevStateKey, index),
|
|
28
|
+
);
|
|
29
|
+
prevStateKey = parent.key;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result.reverse();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getTransitionsFromDescriptor(
|
|
36
|
+
descriptor: FsmStateDescriptor,
|
|
37
|
+
prevStateKey: string,
|
|
38
|
+
index: Record<string, boolean> = {},
|
|
39
|
+
) {
|
|
40
|
+
const result: [from: string, event: string, to: string][] = [];
|
|
41
|
+
const prevStateKeys = [prevStateKey, "*"];
|
|
42
|
+
for (const prevKey of prevStateKeys) {
|
|
43
|
+
const targets: undefined | Record<string, string> =
|
|
44
|
+
descriptor.transitions[prevKey];
|
|
45
|
+
if (targets) {
|
|
46
|
+
for (const [event, target] of Object.entries(targets)) {
|
|
47
|
+
if (index[event]) continue;
|
|
48
|
+
if (target) {
|
|
49
|
+
index[event] = true;
|
|
50
|
+
}
|
|
51
|
+
result.push([prevStateKey, event, target]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|