@statewalker/fsm 0.23.1 → 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.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ declare class FsmBaseClass {
6
6
  getData<T>(key: string): T | undefined;
7
7
  _addHandler(type: string, handler: Function, direct?: boolean): () => void;
8
8
  _removeHandler(type: string, handler: Function): void;
9
+ _runHandlerParallel(type: string, ...args: unknown[]): Promise<void>;
9
10
  _runHandler(type: string, ...args: unknown[]): Promise<void>;
10
11
  _handleError(error: Error | unknown): Promise<void>;
11
12
  }
@@ -71,12 +72,15 @@ type FsmProcessDumpHandler = (process: FsmProcess, dump: FsmProcessDump) => void
71
72
  declare class FsmProcess extends FsmBaseClass {
72
73
  state?: FsmState;
73
74
  event?: string;
75
+ nextEvent?: string;
76
+ running: boolean;
77
+ mask: number;
74
78
  status: number;
75
79
  config: FsmStateConfig;
76
80
  rootDescriptor: FsmStateDescriptor;
77
81
  constructor(config: FsmStateConfig);
78
82
  shutdown(event?: string): Promise<void>;
79
- dispatch(event: string, mask?: number): Promise<boolean>;
83
+ dispatch(event: string): Promise<boolean>;
80
84
  dump(...args: unknown[]): Promise<FsmProcessDump>;
81
85
  restore(dump: FsmProcessDump, ...args: unknown[]): Promise<this>;
82
86
  onStateCreate(handler: FsmStateHandler): () => void;
package/dist/index.js CHANGED
@@ -29,12 +29,23 @@ var FsmBaseClass = class {
29
29
  delete this.handlers[type];
30
30
  }
31
31
  }
32
+ async _runHandlerParallel(type, ...args) {
33
+ const list = this.handlers[type] || [];
34
+ await Promise.all(
35
+ list.map(async (handler) => {
36
+ try {
37
+ await handler(...args);
38
+ } catch (error) {
39
+ this._handleError(error);
40
+ }
41
+ })
42
+ );
43
+ }
32
44
  async _runHandler(type, ...args) {
33
45
  const list = this.handlers[type] || [];
34
- const promises = list.map((handler) => handler(...args));
35
- for (const promise of promises) {
46
+ for (const handler of list) {
36
47
  try {
37
- await promise;
48
+ await handler(...args);
38
49
  } catch (error) {
39
50
  await this._handleError(error);
40
51
  }
@@ -158,6 +169,8 @@ var STATUS_EXIT = STATUS_LEAF | STATUS_LAST;
158
169
  var FsmProcess = class extends FsmBaseClass {
159
170
  constructor(config) {
160
171
  super();
172
+ this.running = false;
173
+ this.mask = STATUS_LEAF;
161
174
  this.status = 0;
162
175
  this.rootDescriptor = FsmStateDescriptor.build(config);
163
176
  this.config = config;
@@ -171,18 +184,32 @@ var FsmProcess = class extends FsmBaseClass {
171
184
  this.state = this.state.parent;
172
185
  }
173
186
  }
174
- async dispatch(event, mask = STATUS_LEAF) {
175
- this.event = event;
176
- while (true) {
177
- if (this.status & STATUS_EXIT) {
178
- await this.state?._runHandler("onExit", this.state);
187
+ async dispatch(event) {
188
+ this.nextEvent = event;
189
+ if (!this.running && !(this.status & STATUS_FINISHED)) {
190
+ this.running = true;
191
+ try {
192
+ this.event = this.nextEvent;
193
+ this.nextEvent = void 0;
194
+ while (true) {
195
+ if (this.status & STATUS_EXIT) {
196
+ await this.state?._runHandler("onExit", this.state);
197
+ }
198
+ if (!this._update()) break;
199
+ if (this.status & STATUS_ENTER) {
200
+ await this.state?._runHandler("onEnter", this.state);
201
+ }
202
+ if (this.status & this.mask) break;
203
+ }
204
+ } finally {
205
+ this.running = false;
179
206
  }
180
- if (!this._update()) return false;
181
- if (this.status & STATUS_ENTER) {
182
- await this.state?._runHandler("onEnter", this.state);
207
+ if (this.nextEvent !== void 0) {
208
+ await new Promise((r) => setImmediate(r));
209
+ return this.dispatch(this.nextEvent);
183
210
  }
184
- if (this.status & mask) return true;
185
211
  }
212
+ return !(this.status & STATUS_FINISHED);
186
213
  }
187
214
  async dump(...args) {
188
215
  const dumpState = async (state) => {
@@ -234,7 +261,7 @@ var FsmProcess = class extends FsmBaseClass {
234
261
  }
235
262
  _newState(parent, key, descriptor) {
236
263
  const state = new FsmState(this, parent, key, descriptor);
237
- this._runHandler("onStateCreate", state);
264
+ this._runHandlerParallel("onStateCreate", state);
238
265
  return state;
239
266
  }
240
267
  _getSubstate(parent, prevStateKey) {
@@ -313,9 +340,11 @@ function setStateTracer(state, print) {
313
340
  const printLine = print || getPrinter(state);
314
341
  printLine(`<${state?.key} event="${state.process.event}">`);
315
342
  });
316
- state.onExit(() => {
317
- const printLine = print || getPrinter(state);
318
- printLine(`</${state.key}> <!-- event="${state.process.event}" -->`);
343
+ state.onExit(async () => {
344
+ await Promise.resolve().then(async () => {
345
+ const printLine = print || getPrinter(state);
346
+ printLine(`</${state.key}> <!-- event="${state.process.event}" -->`);
347
+ });
319
348
  });
320
349
  }
321
350
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/fsm",
3
- "version": "0.23.1",
3
+ "version": "0.24.0",
4
4
  "description": "HFSM Implementation",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/statewalker/statewalker-fsm",
@@ -31,17 +31,30 @@ export class FsmBaseClass {
31
31
  }
32
32
  }
33
33
 
34
+ async _runHandlerParallel(type: string, ...args: unknown[]) {
35
+ const list = this.handlers[type] || [];
36
+ await Promise.all(
37
+ list.map(async (handler) => {
38
+ try {
39
+ await handler(...args);
40
+ } catch (error) {
41
+ this._handleError(error);
42
+ }
43
+ }),
44
+ );
45
+ }
46
+
34
47
  async _runHandler(type: string, ...args: unknown[]) {
35
48
  const list = this.handlers[type] || [];
36
- const promises = list.map((handler) => handler(...args));
37
- for (const promise of promises) {
49
+ for (const handler of list) {
38
50
  try {
39
- await promise;
51
+ await handler(...args);
40
52
  } catch (error) {
41
53
  await this._handleError(error);
42
54
  }
43
55
  }
44
56
  }
57
+
45
58
  async _handleError(error: Error | unknown) {
46
59
  console.error(error);
47
60
  }
package/src/FsmProcess.ts CHANGED
@@ -38,6 +38,9 @@ export type FsmProcessDumpHandler = (
38
38
  export class FsmProcess extends FsmBaseClass {
39
39
  state?: FsmState;
40
40
  event?: string;
41
+ nextEvent?: string;
42
+ running: boolean = false;
43
+ mask: number = STATUS_LEAF;
41
44
  status: number = 0;
42
45
  config: FsmStateConfig;
43
46
  rootDescriptor: FsmStateDescriptor;
@@ -58,18 +61,34 @@ export class FsmProcess extends FsmBaseClass {
58
61
  }
59
62
  }
60
63
 
61
- async dispatch(event: string, mask: number = STATUS_LEAF) {
62
- this.event = event;
63
- while (true) {
64
- if (this.status & STATUS_EXIT) {
65
- await this.state?._runHandler("onExit", this.state);
64
+ async dispatch(event: string): Promise<boolean> {
65
+ this.nextEvent = event;
66
+ if (!this.running && !(this.status & STATUS_FINISHED)) {
67
+ this.running = true;
68
+ try {
69
+ this.event = this.nextEvent;
70
+ this.nextEvent = undefined;
71
+ while (true) {
72
+ // ---
73
+ if (this.status & STATUS_EXIT) {
74
+ await this.state?._runHandler("onExit", this.state);
75
+ }
76
+ if (!this._update()) break;
77
+ if (this.status & STATUS_ENTER) {
78
+ await this.state?._runHandler("onEnter", this.state);
79
+ }
80
+ if (this.status & this.mask) break;
81
+ // ---
82
+ }
83
+ } finally {
84
+ this.running = false;
66
85
  }
67
- if (!this._update()) return false;
68
- if (this.status & STATUS_ENTER) {
69
- await this.state?._runHandler("onEnter", this.state);
86
+ if (this.nextEvent !== undefined) {
87
+ await new Promise((r) => setImmediate(r));
88
+ return this.dispatch(this.nextEvent);
70
89
  }
71
- if (this.status & mask) return true;
72
90
  }
91
+ return !(this.status & STATUS_FINISHED);
73
92
  }
74
93
 
75
94
  async dump(...args: unknown[]): Promise<FsmProcessDump> {
@@ -138,7 +157,7 @@ export class FsmProcess extends FsmBaseClass {
138
157
  descriptor: FsmStateDescriptor | undefined,
139
158
  ) {
140
159
  const state = new FsmState(this, parent, key, descriptor);
141
- this._runHandler("onStateCreate", state);
160
+ this._runHandlerParallel("onStateCreate", state);
142
161
  return state;
143
162
  }
144
163
 
@@ -1,4 +1,5 @@
1
1
  import { FsmState } from "../FsmState";
2
+ import { isStateTransitionEnabled } from "./transitions";
2
3
 
3
4
  export function newModuleLoader<T>(baseUrl: string) {
4
5
  const cache: Record<string, Promise<T>> = {};
@@ -23,15 +24,15 @@ export async function newHandlerLoader(
23
24
  baseUrl: string,
24
25
  loader: (
25
26
  path: string,
26
- ) => Promise<undefined | 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
  }