@statewalker/fsm 0.16.0 → 0.20.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.js CHANGED
@@ -1,295 +1,373 @@
1
- // @statewalker/fsm v0.16.0 https://github.com/statewalker/statewalker-fsm Copyright 2022 Mikhail Kotelnikov
2
- import { MODE, newAsyncTreeWalker, newTreeWalker } from '@statewalker/tree';
1
+ // src/utils/bindMethods.ts
2
+ function bindMethods(obj, ...methods) {
3
+ const o = obj;
4
+ methods.forEach((methodName) => {
5
+ o[methodName] = o[methodName].bind(o);
6
+ });
7
+ return obj;
8
+ }
3
9
 
4
- function buildStateDescriptor(config = {}) {
5
- const { key, transitions = [], states = {}, options, ...params } = config;
6
- const result = {
7
- key,
8
- transitions: {},
9
- states: {},
10
- options: Object.assign(options || {}, params)
11
- };
12
- for (const t of transitions) {
13
- let stateKey, eventKey, targetStateKey;
14
- if (Array.isArray(t)) {
15
- stateKey = t[0];
16
- eventKey = t[1];
17
- targetStateKey = t[2];
10
+ // src/FsmBaseClass.ts
11
+ var FsmBaseClass = class {
12
+ handlers = {};
13
+ data = {};
14
+ constructor() {
15
+ bindMethods(this, "setData", "getData");
16
+ }
17
+ setData(key, value) {
18
+ this.data[key] = value;
19
+ return this;
20
+ }
21
+ getData(key) {
22
+ return this.data[key];
23
+ }
24
+ // ----------------------------------------------
25
+ // internal methods
26
+ _addHandler(type, handler, direct = true) {
27
+ const list = this.handlers[type] = this.handlers[type] || [];
28
+ direct ? list.push(handler) : list.unshift(handler);
29
+ return () => this._removeHandler(type, handler);
30
+ }
31
+ _removeHandler(type, handler) {
32
+ let list = this.handlers[type];
33
+ if (!list)
34
+ return;
35
+ list = list.filter((h) => h !== handler);
36
+ if (list.length > 0) {
37
+ this.handlers[type] = list;
18
38
  } else {
19
- const { from, event, to } = t;
20
- stateKey = from;
21
- eventKey = event;
22
- targetStateKey = to;
39
+ delete this.handlers[type];
23
40
  }
24
- const index = result.transitions[stateKey] = result.transitions[stateKey] || {};
25
- index[eventKey] = targetStateKey;
26
41
  }
27
- if (Array.isArray(states)) {
28
- for (const s of states) {
29
- result.states[s.key] = buildStateDescriptor(s);
30
- }
31
- } else if (states) {
32
- for (const key of Object.keys(states)) {
33
- result.states[key] = buildStateDescriptor(states[key]);
42
+ _runHandlerSync(type, ...args) {
43
+ const list = this.handlers[type] || [];
44
+ return list.map((handler) => handler(...args));
45
+ }
46
+ async _runHandler(type, ...args) {
47
+ const promises = this._runHandlerSync(type, ...args);
48
+ for (const promise of promises) {
49
+ try {
50
+ await promise;
51
+ } catch (error) {
52
+ await this._handleError(error);
53
+ }
34
54
  }
35
55
  }
36
- return result;
37
- }
56
+ async _handleError(error) {
57
+ console.error(error);
58
+ }
59
+ };
38
60
 
39
- var KEYS = {
40
- STATE_ANY : "*",
41
- STATE_INITIAL : "",
42
- STATE_FINAL : "",
43
- EVENT_ANY : "*",
44
- EVENT_EMPTY : "",
61
+ // src/FsmState.ts
62
+ var FsmState = class extends FsmBaseClass {
63
+ process;
64
+ key;
65
+ parent;
66
+ descriptor;
67
+ constructor(process, parent, key, descriptor) {
68
+ super();
69
+ this.process = process;
70
+ this.key = key;
71
+ this.parent = parent;
72
+ this.descriptor = descriptor;
73
+ bindMethods(
74
+ this,
75
+ "onEnter",
76
+ "onExit",
77
+ "dump",
78
+ "restore",
79
+ "useData",
80
+ "onStateError"
81
+ );
82
+ }
83
+ onEnter(handler) {
84
+ return this._addHandler("onEnter", handler, true);
85
+ }
86
+ onExit(handler) {
87
+ return this._addHandler("onExit", handler, false);
88
+ }
89
+ onStateError(handler) {
90
+ return this._addHandler("onStateError", handler);
91
+ }
92
+ dump(handler) {
93
+ return this._addHandler("dump", handler, true);
94
+ }
95
+ restore(handler) {
96
+ return this._addHandler("restore", handler, true);
97
+ }
98
+ getData(key, recursive = true) {
99
+ return this.data[key] ?? (recursive ? this.parent?.getData(key, recursive) : void 0);
100
+ }
101
+ useData(key) {
102
+ return [
103
+ (recursive = true) => this.getData(key, recursive),
104
+ (value) => this.setData(key, value)
105
+ ];
106
+ }
107
+ async _handleError(error) {
108
+ await this._runHandler("onStateError", this, error);
109
+ await this.process._handleStateError(this, error);
110
+ }
45
111
  };
46
112
 
47
- function checkProcessDescriptor(descriptor) {
48
- return checkStateDescriptor(descriptor);
113
+ // src/FsmStateConfig.ts
114
+ var STATE_ANY = "*";
115
+ var STATE_INITIAL = "";
116
+ var STATE_FINAL = "";
117
+ var EVENT_ANY = "*";
118
+ var EVENT_EMPTY = "";
49
119
 
50
- function checkStateDescriptor(descriptor, report = [], stack = []) {
51
- stack = [...stack, descriptor.key];
52
- const result = Object.assign({ key: descriptor.key }, checkStateTransitions(descriptor));
53
- if (result.deadendStates.length || result.unreachableStates.length) {
54
- const info = { path: stack };
55
- report.push(info);
56
- if (result.deadendStates.length) info.deadendStates = result.deadendStates;
57
- if (result.unreachableStates.length) info.unreachableStates = result.unreachableStates;
120
+ // src/FsmStateDescriptor.ts
121
+ var FsmStateDescriptor = class _FsmStateDescriptor {
122
+ transitions = {};
123
+ states = {};
124
+ static build(config) {
125
+ const descriptor = new _FsmStateDescriptor();
126
+ for (const [from, event, to] of config.transitions) {
127
+ const index = descriptor.transitions[from] = descriptor.transitions[from] || {};
128
+ index[event] = to;
58
129
  }
59
- for (const subDescriptor of Object.values(descriptor.states)) {
60
- checkStateDescriptor(subDescriptor, report, stack);
130
+ if (config.states) {
131
+ for (const substateConfig of config.states) {
132
+ descriptor.states[substateConfig.key] = this.build(substateConfig);
133
+ }
61
134
  }
62
- return report;
135
+ return descriptor;
63
136
  }
64
-
65
- function checkStateTransitions(descriptor) {
66
- const sourceStates = {};
67
- const targetStates = {};
68
- for (const [from, destinations] of Object.entries(descriptor.transitions)) {
69
- sourceStates[from] = (sourceStates[from] || 0) + 1;
70
- for (const targetKey of Object.values(destinations)) {
71
- targetStates[targetKey] = (targetStates[targetKey] || 0) + 1;
72
- }
137
+ getTargetStateKey(stateKey, eventKey) {
138
+ const pairs = [
139
+ [stateKey, eventKey],
140
+ [STATE_ANY, eventKey],
141
+ [stateKey, EVENT_ANY],
142
+ [STATE_ANY, EVENT_ANY]
143
+ ];
144
+ let targetKey;
145
+ for (let i = 0, len = pairs.length; targetKey === void 0 && i < len; i++) {
146
+ const [stateKey2, eventKey2] = pairs[i];
147
+ const stateTransitions = this.transitions[stateKey2];
148
+ if (!stateTransitions)
149
+ continue;
150
+ targetKey = stateTransitions[eventKey2];
73
151
  }
74
- // Unreachable states - no transitions leading to these states
75
- const unreachableStates = [];
76
- // Deadend states - no transitions from these states
77
- const deadendStates = [];
152
+ return targetKey !== void 0 ? targetKey : STATE_FINAL;
153
+ }
154
+ };
78
155
 
79
- // Unreachable states - no transitions leading to these states
80
- for (const sourceKey of Object.keys(sourceStates)) {
81
- if (sourceKey !== KEYS.STATE_ANY &&
82
- sourceKey !== KEYS.STATE_FINAL &&
83
- sourceKey !== KEYS.STATE_INITIAL &&
84
- !(sourceKey in targetStates)) {
85
- unreachableStates.push(sourceKey);
156
+ // src/FsmProcess.ts
157
+ var STATUS_NONE = 0;
158
+ var STATUS_FIRST = 1;
159
+ var STATUS_NEXT = 2;
160
+ var STATUS_LEAF = 4;
161
+ var STATUS_LAST = 8;
162
+ var STATUS_FINISHED = 16;
163
+ var STATUS_ENTER = STATUS_FIRST | STATUS_NEXT;
164
+ var STATUS_EXIT = STATUS_LEAF | STATUS_LAST;
165
+ var FsmProcess = class extends FsmBaseClass {
166
+ state;
167
+ event;
168
+ status = 0;
169
+ config;
170
+ rootDescriptor;
171
+ constructor(config) {
172
+ super();
173
+ this.rootDescriptor = FsmStateDescriptor.build(config);
174
+ this.config = config;
175
+ bindMethods(this, "dispatch", "dump", "restore");
176
+ }
177
+ async dispatch(event, mask = STATUS_LEAF) {
178
+ this.event = event;
179
+ while (true) {
180
+ if (this.status & STATUS_EXIT) {
181
+ await this.state?._runHandler("onExit", this.state);
86
182
  }
87
- }
88
-
89
- // Deadend states - no transitions from these states
90
- for (const targetKey of Object.keys(targetStates)) {
91
- if (targetKey !== KEYS.STATE_FINAL && !(targetKey in sourceStates)) {
92
- deadendStates.push(targetKey);
183
+ if (!this._update())
184
+ return false;
185
+ if (this.status & STATUS_ENTER) {
186
+ await this.state?._runHandler("onEnter", this.state);
93
187
  }
188
+ if (this.status & mask)
189
+ return true;
94
190
  }
95
-
96
- return { unreachableStates, deadendStates }
97
191
  }
98
- }
99
-
100
- function getStateDescriptor(process, stateKey) {
101
- let descriptor;
102
- for (let i = process.stack.length - 1; !descriptor && i >= 0; i--) {
103
- const states = process.stack[i].descriptor.states || {};
104
- descriptor = states[stateKey];
192
+ async dump(...args) {
193
+ const dumpState = async (state) => {
194
+ const stateDump = {
195
+ key: state.key,
196
+ data: {}
197
+ };
198
+ await state._runHandler("dump", state, stateDump.data, ...args);
199
+ return stateDump;
200
+ };
201
+ const dumpStates = async (state, stack = []) => {
202
+ if (!state)
203
+ return stack;
204
+ state.parent && await dumpStates(state.parent, stack);
205
+ stack.push(await dumpState(state));
206
+ return stack;
207
+ };
208
+ const dump = {
209
+ status: this.status,
210
+ event: this.event,
211
+ stack: await dumpStates(this.state)
212
+ };
213
+ return dump;
105
214
  }
106
- return descriptor || { transitions: {}, states: {} };
107
- }
108
-
109
- /**
110
- * This method returns a list of all states used in
111
- * the specified state descriptor.
112
- *
113
- * @param {object} descriptor compiled state descriptor
114
- * defining transitions between sub-states and sub-states
115
- * @returns a sorted list of all states
116
- */
117
- function getAllStateKeys(descriptor) {
118
- const index = {};
119
- const add = (key) => index[key] = (index[key] || 0) + 1;
120
- visit(descriptor);
121
- return Object.keys(index).sort();
122
- function visit(descriptor) {
123
- add(descriptor.key);
124
- for (const [from, destinations] of Object.entries(descriptor.transitions)) {
125
- add(from);
126
- for (const targetKey of Object.values(destinations)) {
127
- add(targetKey);
128
- }
215
+ async restore(dump, ...args) {
216
+ this.status = dump.status || 0;
217
+ this.event = dump.event;
218
+ this.state = void 0;
219
+ for (let i = 0; i < dump.stack.length; i++) {
220
+ const stateDump = dump.stack[i];
221
+ this.state = this.state ? this._newSubstate(this.state, stateDump.key) : this._newState(void 0, stateDump.key, this.rootDescriptor);
222
+ await this.state._runHandler(
223
+ "restore",
224
+ this.state,
225
+ stateDump.data,
226
+ ...args
227
+ );
129
228
  }
130
- for (const [stateKey, stateDescriptor] of Object.entries(descriptor.states)) {
131
- add(stateKey);
132
- visit(stateDescriptor);
229
+ return this;
230
+ }
231
+ onStateCreate(handler) {
232
+ return this._addHandler("onStateCreate", handler, true);
233
+ }
234
+ onStateError(handler) {
235
+ return this._addHandler("onStateError", handler);
236
+ }
237
+ async _handleStateError(state, error) {
238
+ await this._runHandler("onStateError", state, error);
239
+ this._handleError(error);
240
+ }
241
+ _newState(parent, key, descriptor) {
242
+ const state = new FsmState(this, parent, key, descriptor);
243
+ this._runHandlerSync("onStateCreate", state);
244
+ return state;
245
+ }
246
+ _getSubstate(parent, prevStateKey) {
247
+ if (!parent)
248
+ return;
249
+ const toState = parent.descriptor?.getTargetStateKey(
250
+ prevStateKey || STATE_INITIAL,
251
+ this.event || EVENT_EMPTY
252
+ ) || STATE_FINAL;
253
+ if (!toState)
254
+ return;
255
+ return this._newSubstate(parent, toState);
256
+ }
257
+ _newSubstate(parent, toState) {
258
+ let descriptor;
259
+ for (let state = parent; !descriptor && state; state = state.parent) {
260
+ descriptor = state.descriptor?.states[toState];
133
261
  }
262
+ return this._newState(parent, toState, descriptor);
134
263
  }
135
- }
136
-
137
- function getTargetStateKey(descriptor, stateKey, eventKey) {
138
- const pairs = [
139
- [stateKey, eventKey],
140
- [KEYS.STATE_ANY, eventKey],
141
- [stateKey, KEYS.EVENT_ANY],
142
- [KEYS.STATE_ANY, KEYS.EVENT_ANY]
143
- ];
144
- let targetKey;
145
- for (let i = 0, len = pairs.length; (targetKey === undefined) && i < len; i++) {
146
- const [stateKey, eventKey] = pairs[i];
147
- const stateTransitions = descriptor.transitions[stateKey];
148
- if (!stateTransitions) continue;
149
- targetKey = stateTransitions[eventKey];
150
- }
151
- return (targetKey !== undefined) ? targetKey : KEYS.STATE_FINAL;
152
- }
153
-
154
- function newProcessInstance({
155
- config,
156
- descriptor,
157
- newState = (obj) => obj,
158
- newProcess = (descriptor) => ({
159
- descriptor,
160
- status: 0,
161
- stack: [],
162
- event: undefined,
163
- current: undefined
164
- })
165
- }) {
166
- descriptor = descriptor || buildStateDescriptor(config);
167
- const process = newProcess(descriptor);
168
- const loadNextState = () => {
169
- let key, descriptor;
170
- if (!process.status) {
171
- descriptor = process.descriptor;
172
- key = descriptor.key;
264
+ _update() {
265
+ if (this.status & STATUS_FINISHED)
266
+ return false;
267
+ 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);
268
+ if (nextState !== void 0) {
269
+ this.state = nextState;
270
+ this.status = this.status & STATUS_EXIT ? STATUS_NEXT : STATUS_FIRST;
173
271
  } else {
174
- if (!process.stack.length) {
175
- key = KEYS.STATE_FINAL;
272
+ if (this.status & STATUS_EXIT) {
273
+ this.state = this.state?.parent;
274
+ this.status = STATUS_LAST;
176
275
  } else {
177
- const eventKey = process.event.key || KEYS.EVENT_EMPTY;
178
- const parentDescriptor = process.stack[process.stack.length - 1].descriptor;
179
- const stateKey = process.status & MODE.ENTER
180
- ? KEYS.STATE_INITIAL
181
- : process.current ? process.current.key : KEYS.STATE_FINAL;
182
- key = getTargetStateKey(parentDescriptor, stateKey, eventKey);
183
- if (key) {
184
- descriptor = getStateDescriptor(process, key);
185
- }
276
+ this.status = STATUS_LEAF;
186
277
  }
278
+ if (!this.state)
279
+ this.status = STATUS_FINISHED;
187
280
  }
188
- return key ? newState({ process, key, descriptor }) : null;
189
- };
190
- return [process, loadNextState];
191
- }
281
+ return !(this.status & STATUS_FINISHED);
282
+ }
283
+ };
192
284
 
193
- function newAsyncProcess({
194
- config,
195
- descriptor,
196
- before, after,
197
- newProcess,
198
- newState,
199
- handleError = console.error
200
- }) {
201
- const [process, loadNextState] = newProcessInstance({
202
- config,
203
- descriptor,
204
- newProcess,
205
- newState
206
- });
207
- const shift = newAsyncTreeWalker(before, after, process);
208
- process.finished = false;
209
- process.next = (event) => {
210
- process.nextEvent = event;
211
- return process.running = process.running || Promise
212
- .resolve()
213
- .then(async () => {
214
- try {
215
- process.event = process.nextEvent;
216
- process.nextEvent = undefined;
217
- while (!process.finished && process.event) {
218
- process.finished = !await shift(loadNextState);
219
- if ((process.status & MODE.EXIT) && process.nextEvent) {
220
- // Consume the next event if it exists.
221
- process.event = process.nextEvent;
222
- process.nextEvent = undefined;
223
- } else if (process.status & MODE.LEAF) {
224
- // Returns control when the process is in a "leaf" state (a state without children)
225
- break;
226
- }
227
- }
228
- } catch (error) {
229
- handleError(error);
230
- }
231
- })
232
- .finally(() => process.running = undefined)
233
- .then(() => !process.finished);
234
- };
235
- return process;
285
+ // src/context/handlers.ts
286
+ var KEY_HANDLERS = "handlers";
287
+ function callStateHandlers(state) {
288
+ const key = state.key;
289
+ for (let parent = state.parent; !!parent; parent = parent?.parent) {
290
+ const handlers = parent.getData(KEY_HANDLERS)?.[key];
291
+ handlers?.forEach((handler) => handler(state));
292
+ }
236
293
  }
237
-
238
- /**
239
- * Returns all possible transitions from the current state of the process.
240
- * @param {Object} options - method parameters
241
- * @param {Process} options.process - the current process for which
242
- * all transitions should be returned
243
- * @returns {Array<Object>} - an array of all transitions;
244
- * each entry in this returned array contains the following keys:
245
- * 1) parentStateKey - key of the state where the transition can be activated
246
- * 2) sourceStateKey - the initial transition state
247
- * 3) eventKey - key of the event activating transition
248
- * 4) targetStateKey - the resulting state for this transition
249
- *
250
- */
251
- function getAllTransitions(process) {
252
- const list = [];
253
- let stateKey = process.current ? process.current.key : KEYS.STATE_INITIAL;
254
- for (let i = process.stack.length - 1; i >= 0; i--) {
255
- const parentState = process.stack[i];
256
- const parentStateKey = parentState.key;
257
- const descriptor = parentState.descriptor;
258
- for (const sKey of [stateKey, KEYS.STATE_ANY]) {
259
- const stateTransitions = descriptor.transitions[sKey];
260
- if (!stateTransitions) continue;
261
- for (const[eventKey, targetKey] of Object.entries(stateTransitions)) {
262
- list.push([parentStateKey, sKey, eventKey, targetKey]);
263
- }
264
- }
265
- stateKey = parentStateKey;
294
+ function addSubstateHandlers(state, handlers) {
295
+ const oldIndex = state.getData(KEY_HANDLERS);
296
+ const index = oldIndex ? { ...oldIndex } : {};
297
+ for (const [key, handler] of Object.entries(handlers)) {
298
+ const list = index[key] = index[key] || [];
299
+ list.push(handler);
266
300
  }
267
- return list;
301
+ state.setData(KEY_HANDLERS, index);
268
302
  }
269
303
 
270
- function newSyncProcess({
271
- config,
272
- descriptor,
273
- before, after,
274
- newProcess,
275
- newState,
276
- }) {
277
- const [process, loadNextState] = newProcessInstance({
278
- config,
279
- descriptor,
280
- newProcess,
281
- newState,
282
- });
283
- const shift = newTreeWalker(before, after, process);
284
- process.dispatch = process.next = (event) => {
285
- if (typeof event === 'string') event = { key : event };
286
- process.event = event;
287
- while (process.event) {
288
- if (!shift(loadNextState)) return false;
289
- if (process.status & MODE.LEAF) return true;
304
+ // src/context/printer.ts
305
+ var KEY_PRINTER = "printer";
306
+ function preparePrinter(process, { prefix = "", print = console.log, lineNumbers = false }) {
307
+ let lineCounter = 0;
308
+ const shift = () => {
309
+ let prefix2 = "";
310
+ for (let s = process.state?.parent; !!s; s = s.parent) {
311
+ prefix2 += " ";
290
312
  }
313
+ return prefix2;
291
314
  };
292
- return process;
315
+ const getPrefix = lineNumbers ? () => `[${++lineCounter}]${shift()}` : shift;
316
+ const printer = (...args) => print(prefix, getPrefix(), ...args);
317
+ return printer;
318
+ }
319
+ function setPrinter(state, config = {}) {
320
+ const printer = preparePrinter(state.process, config);
321
+ state.setData(KEY_PRINTER, printer);
322
+ }
323
+ function setProcessPrinter(process, config = {}) {
324
+ const printer = preparePrinter(process, config);
325
+ process.setData(KEY_PRINTER, printer);
326
+ }
327
+ function getPrinter(state) {
328
+ return state.getData(KEY_PRINTER, true) || state.process.getData(KEY_PRINTER) || console.log;
293
329
  }
294
330
 
295
- export { KEYS, buildStateDescriptor, checkProcessDescriptor, getAllStateKeys, getAllTransitions, getStateDescriptor, getTargetStateKey, newAsyncProcess, newProcessInstance, newSyncProcess };
331
+ // src/context/tracer.ts
332
+ function setProcessTracer(process, print) {
333
+ return process.onStateCreate((state) => {
334
+ setStateTracer(state, print);
335
+ });
336
+ }
337
+ function setStateTracer(state, print) {
338
+ const printLine = print || getPrinter(state);
339
+ state.onEnter(() => {
340
+ printLine(`<${state?.key} event="${state.process.event}">`);
341
+ });
342
+ state.onExit(() => {
343
+ printLine(`</${state.key}> <!-- event="${state.process.event}" -->`);
344
+ });
345
+ }
346
+ export {
347
+ EVENT_ANY,
348
+ EVENT_EMPTY,
349
+ FsmProcess,
350
+ FsmState,
351
+ FsmStateDescriptor,
352
+ KEY_HANDLERS,
353
+ KEY_PRINTER,
354
+ STATE_ANY,
355
+ STATE_FINAL,
356
+ STATE_INITIAL,
357
+ STATUS_ENTER,
358
+ STATUS_EXIT,
359
+ STATUS_FINISHED,
360
+ STATUS_FIRST,
361
+ STATUS_LAST,
362
+ STATUS_LEAF,
363
+ STATUS_NEXT,
364
+ STATUS_NONE,
365
+ addSubstateHandlers,
366
+ callStateHandlers,
367
+ getPrinter,
368
+ preparePrinter,
369
+ setPrinter,
370
+ setProcessPrinter,
371
+ setProcessTracer,
372
+ setStateTracer
373
+ };