@statewalker/fsm 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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 | FsmStateHandler> = newModuleLoader(baseUrl),
27
- ): Promise<(state: FsmState) => Promise<FsmStateHandler[]>> {
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: FsmStateHandler[] = [];
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,57 @@ 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 FsmStateHandler<
67
- FsmStateContext extends Record<string, unknown> = Record<string, unknown>,
67
+ export type FsmStateModule<
68
+ FsmStateContext = Record<string, unknown>,
68
69
  FsmStateStore = Record<string, unknown>,
69
70
  > = {
70
- context?: (...stack: FsmStateStore[]) => FsmStateContext;
71
+ context?:
72
+ | ((...stack: FsmStateStore[]) => FsmStateContext)
73
+ | (new (...stack: FsmStateStore[]) => FsmStateContext);
74
+ // trigger?: (context: FsmStateContext) => AsyncGenerator<string>;
71
75
  trigger?: (
72
76
  context: FsmStateContext,
73
- listener: (event: string) => void,
77
+ onEvent: (event: string) => void,
74
78
  ) => void | (() => void);
75
- default?: (context: FsmStateContext) => void | (() => void);
76
- handler?: (context: FsmStateContext) => void | (() => void);
79
+ handler?: (
80
+ context: FsmStateContext,
81
+ ) => void | (() => void) | Promise<void | (() => void)>;
77
82
  };
78
83
 
84
+
85
+ export function newModuleContext<
86
+ C = Record<string, unknown>,
87
+ S = Record<string, unknown>,
88
+ >(module: FsmStateModule<C, S>, ...stack: S[]): C {
89
+ if (isClass<S[], C>(module.context)) {
90
+ return new module.context(...stack) as unknown as C;
91
+ } else if (isFunction<S[], C>(module.context)) {
92
+ return module.context(...stack);
93
+ } else {
94
+ return stack[0] as unknown as C;
95
+ }
96
+ }
97
+ function isFunction<T extends Array<unknown>, R = unknown>(
98
+ value: unknown,
99
+ ): value is (...args: T) => R {
100
+ return typeof value === "function";
101
+ }
102
+ function isClass<T extends Array<unknown>, R = unknown>(
103
+ value: unknown,
104
+ ): value is new (...args: T) => R {
105
+ if (!isFunction(value)) return false;
106
+ // console.log('============')
107
+ // console.log('???', value, value?.prototype, value?.prototype?.constructor);
108
+ return value?.prototype?.constructor?.toString().match(/^class/);
109
+ }
110
+
79
111
  export function newStateHandlers<
80
- FsmStateContext extends Record<string, unknown> = Record<string, unknown>,
112
+ FsmStateContext = Record<string, unknown>,
81
113
  FsmStateStore = Record<string, unknown>,
82
114
  >(
83
- loader: (state: FsmState) => Promise<FsmStateHandler[]>,
115
+ loader: (
116
+ state: FsmState,
117
+ ) => Promise<FsmStateModule<FsmStateContext, FsmStateStore>[]>,
84
118
  newContext: (state: FsmState, ...stack: FsmStateStore[]) => FsmStateStore = (
85
119
  state,
86
120
  ) =>
@@ -88,6 +122,10 @@ export function newStateHandlers<
88
122
  state: state.key,
89
123
  event: state.process.event,
90
124
  }) as unknown as FsmStateStore,
125
+ contextCache: Map<unknown, FsmStateContext> = new Map<
126
+ unknown,
127
+ FsmStateContext
128
+ >(),
91
129
  ) {
92
130
  return (state: FsmState) => {
93
131
  const stack: FsmStateStore[] = [];
@@ -99,15 +137,11 @@ export function newStateHandlers<
99
137
  stack.unshift(store);
100
138
  state.setData("context", store);
101
139
 
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) {
140
+ let registrations: (void | (() => void) | Promise<void | (() => void)>)[] =
141
+ [];
142
+ state.onExit(async () => {
143
+ for (const r of registrations) {
144
+ const registration = await r;
111
145
  registration?.();
112
146
  }
113
147
  });
@@ -115,40 +149,36 @@ export function newStateHandlers<
115
149
  const modules = await loader(state);
116
150
  if (!modules?.length) return;
117
151
  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);
152
+ const contextKey: unknown = module.context;
153
+ // Create state context or get it from the local cache
154
+ let stateContext = contextCache.get(contextKey);
155
+ if (!stateContext) {
156
+ stateContext = newModuleContext(module, ...stack);
157
+ contextCache.set(contextKey, stateContext);
158
+ registrations.push(() => {
159
+ contextCache.delete(contextKey);
160
+ });
161
+ }
138
162
 
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/);
163
+ // Initialize triggers
164
+ if (module.trigger) {
165
+ const process = state.process;
166
+ // Set trigger
167
+ let notify = async (event: string) => {
168
+ if (event && isStateTransitionEnabled(process, event)) {
169
+ process.dispatch(event);
170
+ }
171
+ };
172
+ const cleanup = module.trigger(stateContext, notify);
173
+ registrations.push(() => {
174
+ notify = async () => {};
175
+ cleanup?.();
176
+ });
177
+ }
178
+ if (module.handler) {
179
+ const cleanup = module.handler(stateContext);
180
+ registrations.push(cleanup);
181
+ }
152
182
  }
153
183
  });
154
184
  };
@@ -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
- const printLine = print || getPrinter(state);
18
- printLine(`</${state.key}> <!-- event="${state.process.event}" -->`);
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
  }
@@ -1,292 +0,0 @@
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
- };
@@ -1,42 +0,0 @@
1
- import { FsmState, FsmStateHandler as FsmStateHandler$1, FsmProcess, FsmStateConfig } from '../index.js';
2
-
3
- declare const KEY_HANDLERS = "handlers";
4
- declare function callStateHandlers(state: FsmState): void;
5
- declare function addSubstateHandlers(state: FsmState, handlers: Record<string, FsmStateHandler$1>): void;
6
-
7
- type Printer = (...args: unknown[]) => void;
8
- type PrinterConfig = {
9
- prefix?: string;
10
- print?: (...args: unknown[]) => void;
11
- lineNumbers?: boolean;
12
- };
13
- declare const KEY_PRINTER = "printer";
14
- declare function preparePrinter(process: FsmProcess, { prefix, print, lineNumbers }: PrinterConfig): Printer;
15
- declare function setPrinter(state: FsmState, config?: PrinterConfig): void;
16
- declare function setProcessPrinter(process: FsmProcess, config?: PrinterConfig): void;
17
- declare function getProcessPrinter(process: FsmProcess): Printer;
18
- declare function getPrinter(state: FsmState): Printer;
19
-
20
- declare function newProcess(config: FsmStateConfig, { prefix, print, lineNumbers, }: {
21
- prefix?: string;
22
- print?: (...args: any[]) => void;
23
- lineNumbers?: boolean;
24
- }): FsmProcess;
25
-
26
- declare function setProcessTracer(process: FsmProcess, print?: Printer): () => void;
27
- declare function setStateTracer(state: FsmState, print?: Printer): void;
28
-
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 };