@statewalker/fsm 0.15.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/fsm",
3
- "version": "0.15.2",
3
+ "version": "0.20.0",
4
4
  "description": "HFSM Implementation",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/statewalker/statewalker-fsm",
@@ -11,41 +11,44 @@
11
11
  "license": "MIT",
12
12
  "type": "module",
13
13
  "files": [
14
- "dist/**/package.json",
15
- "dist/**/*.js",
16
- "src/**/*.js",
17
- "index.js"
14
+ "dist",
15
+ "src"
18
16
  ],
19
- "module": "src/index.js",
20
- "main": "src/index.js",
21
- "jsdelivr": "dist/index.js",
22
- "unpkg": "dist/index.js",
17
+ "module": "./dist/index.js",
18
+ "main": "./dist/index.js",
19
+ "jsdelivr": "./dist/index.js",
20
+ "unpkg": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
23
22
  "exports": {
24
- "umd": "./dist/index-umd.min.js",
25
- "default": "./src/index.js"
23
+ ".": {
24
+ "import": "./src/index.ts"
25
+ }
26
26
  },
27
27
  "dependencies": {
28
28
  "@statewalker/tree": "0.10.2"
29
29
  },
30
30
  "devDependencies": {
31
- "@statewalker/rollup": "^0.1.6",
32
- "eslint": "^8",
33
- "expect.js": "^0.3",
34
- "mocha": "^10",
35
- "rollup": "^3"
31
+ "@statewalker/eslint-config": "*",
32
+ "@statewalker/typescript-config": "*",
33
+ "eslint": "^9.0.0",
34
+ "tsup": "^8.0.2",
35
+ "typescript": "^5.4.4",
36
+ "vitest": "^1.4.0"
36
37
  },
37
38
  "repository": {
38
39
  "type": "git",
39
40
  "url": "git@github.com:statewalker/statewalker-fsm.git"
40
41
  },
41
42
  "scripts": {
42
- "eslint": "eslint src",
43
- "rollup": "rollup -c",
44
- "test": "mocha -R spec ./test/index.js && yarn eslint",
45
- "prepublishOnly": "rm -rf dist && yarn test && yarn rollup"
43
+ "build": "yarn test && tsup",
44
+ "watch": "tsup --watch",
45
+ "clean": "rm -rf dist",
46
+ "lint": "eslint \"**/*.(js|ts)\"",
47
+ "test": "vitest --run",
48
+ "test:watch": "vitest"
46
49
  },
47
50
  "sideEffects": false,
48
51
  "publishConfig": {
49
52
  "access": "public"
50
53
  }
51
- }
54
+ }
@@ -0,0 +1,52 @@
1
+ import { bindMethods } from "./utils/bindMethods.ts";
2
+
3
+ export class FsmBaseClass {
4
+ handlers: Record<string, Function[]> = {};
5
+ data: Record<string, unknown> = {};
6
+ constructor() {
7
+ bindMethods(this, "setData", "getData");
8
+ }
9
+ setData<T>(key: string, value: T) {
10
+ this.data[key] = value;
11
+ return this;
12
+ }
13
+ getData<T>(key: string): T | undefined {
14
+ return this.data[key] as T;
15
+ }
16
+
17
+ // ----------------------------------------------
18
+ // internal methods
19
+ _addHandler(type: string, handler: Function, direct: boolean = true) {
20
+ const list = (this.handlers[type] = this.handlers[type] || []);
21
+ direct ? list.push(handler) : list.unshift(handler);
22
+ return () => this._removeHandler(type, handler);
23
+ }
24
+
25
+ _removeHandler(type: string, handler: Function) {
26
+ let list = this.handlers[type];
27
+ if (!list) return;
28
+ list = list.filter((h) => h !== handler);
29
+ if (list.length > 0) {
30
+ this.handlers[type] = list;
31
+ } else {
32
+ delete this.handlers[type];
33
+ }
34
+ }
35
+ _runHandlerSync(type: string, ...args: unknown[]) {
36
+ const list = this.handlers[type] || [];
37
+ return list.map((handler) => handler(...args));
38
+ }
39
+ async _runHandler(type: string, ...args: unknown[]) {
40
+ const promises = this._runHandlerSync(type, ...args);
41
+ for (const promise of promises) {
42
+ try {
43
+ await promise;
44
+ } catch (error) {
45
+ await this._handleError(error);
46
+ }
47
+ }
48
+ }
49
+ async _handleError(error: Error | unknown) {
50
+ console.error(error);
51
+ }
52
+ }
@@ -0,0 +1,184 @@
1
+ import { bindMethods } from "./utils/bindMethods.ts";
2
+ import { FsmBaseClass } from "./FsmBaseClass.ts";
3
+ import {
4
+ FsmState,
5
+ FsmStateDump,
6
+ FsmStateSyncHandler,
7
+ } from "./FsmState.ts";
8
+ import {
9
+ EVENT_EMPTY,
10
+ FsmStateConfig,
11
+ STATE_FINAL,
12
+ STATE_INITIAL,
13
+ } from "./FsmStateConfig.ts";
14
+ import { FsmStateDescriptor } from "./FsmStateDescriptor.ts";
15
+
16
+ export const STATUS_NONE = 0;
17
+ export const STATUS_FIRST = 1;
18
+ export const STATUS_NEXT = 2;
19
+ export const STATUS_LEAF = 4;
20
+ export const STATUS_LAST = 8;
21
+ export const STATUS_FINISHED = 16;
22
+
23
+ //
24
+ export const STATUS_ENTER = STATUS_FIRST | STATUS_NEXT;
25
+ export const STATUS_EXIT = STATUS_LEAF | STATUS_LAST;
26
+
27
+ export type FsmProcessHandler = (
28
+ process: FsmProcess,
29
+ ...args: any[]
30
+ ) => void | Promise<void>;
31
+
32
+ export type FsmProcessDump = Record<string, any> & {
33
+ status: number;
34
+ event?: string;
35
+ stack: FsmStateDump[];
36
+ };
37
+
38
+ export type FsmProcessDumpHandler = (
39
+ process: FsmProcess,
40
+ dump: FsmProcessDump
41
+ ) => void | Promise<void>;
42
+
43
+ export class FsmProcess extends FsmBaseClass {
44
+ state?: FsmState;
45
+ event?: string;
46
+ status: number = 0;
47
+ config: FsmStateConfig;
48
+ rootDescriptor: FsmStateDescriptor;
49
+
50
+ constructor(config: FsmStateConfig) {
51
+ super();
52
+ this.rootDescriptor = FsmStateDescriptor.build(config);
53
+ this.config = config;
54
+ bindMethods(this, "dispatch", "dump", "restore");
55
+ }
56
+
57
+ async dispatch(event: string, mask: number = STATUS_LEAF) {
58
+ this.event = event;
59
+ while (true) {
60
+ if (this.status & STATUS_EXIT) {
61
+ await this.state?._runHandler("onExit", this.state);
62
+ }
63
+ if (!this._update()) return false;
64
+ if (this.status & STATUS_ENTER) {
65
+ await this.state?._runHandler("onEnter", this.state);
66
+ }
67
+ if (this.status & mask) return true;
68
+ }
69
+ }
70
+
71
+ async dump(...args: unknown[]): Promise<FsmProcessDump> {
72
+ const dumpState = async (state: FsmState) => {
73
+ const stateDump: FsmStateDump = {
74
+ key: state.key,
75
+ data: {},
76
+ };
77
+ await state._runHandler("dump", state, stateDump.data, ...args);
78
+ return stateDump;
79
+ };
80
+ const dumpStates = async (
81
+ state: FsmState | undefined,
82
+ stack: FsmStateDump[] = []
83
+ ) => {
84
+ if (!state) return stack;
85
+ state.parent && (await dumpStates(state.parent, stack));
86
+ stack.push(await dumpState(state));
87
+ return stack;
88
+ };
89
+ const dump: FsmProcessDump = {
90
+ status: this.status,
91
+ event: this.event,
92
+ stack: await dumpStates(this.state),
93
+ };
94
+ return dump;
95
+ }
96
+
97
+ async restore(dump: FsmProcessDump, ...args: unknown[]) {
98
+ this.status = dump.status || 0;
99
+ this.event = dump.event;
100
+ this.state = undefined;
101
+ for (let i = 0; i < dump.stack.length; i++) {
102
+ const stateDump = dump.stack[i];
103
+ this.state = this.state
104
+ ? this._newSubstate(this.state, stateDump.key)
105
+ : this._newState(undefined, stateDump.key, this.rootDescriptor);
106
+ await this.state._runHandler(
107
+ "restore",
108
+ this.state,
109
+ stateDump.data,
110
+ ...args
111
+ );
112
+ }
113
+ return this;
114
+ }
115
+
116
+ onStateCreate(handler: FsmStateSyncHandler) {
117
+ return this._addHandler("onStateCreate", handler, true);
118
+ }
119
+
120
+ onStateError(handler: (state: FsmState, error: any) => void | Promise<void>) {
121
+ return this._addHandler("onStateError", handler);
122
+ }
123
+
124
+ async _handleStateError(state: FsmState, error: Error | unknown) {
125
+ await this._runHandler("onStateError", state, error);
126
+ this._handleError(error);
127
+ }
128
+
129
+ _newState(
130
+ parent: FsmState | undefined,
131
+ key: string,
132
+ descriptor: FsmStateDescriptor | undefined
133
+ ) {
134
+ const state = new FsmState(this, parent, key, descriptor);
135
+ this._runHandlerSync("onStateCreate", state);
136
+ return state;
137
+ }
138
+
139
+ _getSubstate(parent: FsmState | undefined, prevStateKey: string | undefined) {
140
+ if (!parent) return;
141
+ const toState =
142
+ parent.descriptor?.getTargetStateKey(
143
+ prevStateKey || STATE_INITIAL,
144
+ this.event || EVENT_EMPTY
145
+ ) || STATE_FINAL;
146
+ if (!toState) return;
147
+ return this._newSubstate(parent, toState);
148
+ }
149
+
150
+ _newSubstate(parent: FsmState | undefined, toState: string) {
151
+ let descriptor: FsmStateDescriptor | undefined;
152
+ for (
153
+ let state: FsmState | undefined = parent;
154
+ !descriptor && state;
155
+ state = state.parent
156
+ ) {
157
+ descriptor = state.descriptor?.states[toState];
158
+ }
159
+ return this._newState(parent, toState, descriptor);
160
+ }
161
+
162
+ _update() {
163
+ if (this.status & STATUS_FINISHED) return false;
164
+ const nextState =
165
+ this.status !== STATUS_NONE
166
+ ? this.status & STATUS_ENTER
167
+ ? this._getSubstate(this.state, STATE_INITIAL)
168
+ : this._getSubstate(this.state?.parent, this.state?.key)
169
+ : this._newState(undefined, this.config.key, this.rootDescriptor);
170
+ if (nextState !== undefined) {
171
+ this.state = nextState;
172
+ this.status = this.status & STATUS_EXIT ? STATUS_NEXT : STATUS_FIRST;
173
+ } else {
174
+ if (this.status & STATUS_EXIT) {
175
+ this.state = this.state?.parent;
176
+ this.status = STATUS_LAST;
177
+ } else {
178
+ this.status = STATUS_LEAF;
179
+ }
180
+ if (!this.state) this.status = STATUS_FINISHED;
181
+ }
182
+ return !(this.status & STATUS_FINISHED);
183
+ }
184
+ }
@@ -0,0 +1,87 @@
1
+ import { bindMethods } from "./utils/bindMethods.ts";
2
+ import { FsmBaseClass } from "./FsmBaseClass.ts";
3
+ import { FsmProcess } from "./FsmProcess.ts";
4
+ import { FsmStateDescriptor } from "./FsmStateDescriptor.ts";
5
+
6
+ export type FsmStateDump = Record<string, any> & {
7
+ key: string;
8
+ data: Record<string, unknown>;
9
+ };
10
+ export type FsmStateHandler = (
11
+ state: FsmState,
12
+ ...args: unknown[]
13
+ ) => void | Promise<void>;
14
+
15
+ export type FsmStateSyncHandler = (state: FsmState, ...args: unknown[]) => void;
16
+
17
+ export type FsmStateDumpHandler = (
18
+ state: FsmState,
19
+ dump: FsmStateDump
20
+ ) => void | Promise<void>;
21
+
22
+ export type FsmStateErrorHandler = (
23
+ state: FsmState,
24
+ error: any
25
+ ) => void | Promise<void>;
26
+
27
+ export class FsmState extends FsmBaseClass {
28
+ process: FsmProcess;
29
+ key: string;
30
+ parent?: FsmState;
31
+ descriptor?: FsmStateDescriptor;
32
+
33
+ constructor(
34
+ process: FsmProcess,
35
+ parent: FsmState | undefined,
36
+ key: string,
37
+ descriptor?: FsmStateDescriptor
38
+ ) {
39
+ super();
40
+ this.process = process;
41
+ this.key = key;
42
+ this.parent = parent;
43
+ this.descriptor = descriptor;
44
+ bindMethods(
45
+ this,
46
+ "onEnter",
47
+ "onExit",
48
+ "dump",
49
+ "restore",
50
+ "useData",
51
+ "onStateError"
52
+ );
53
+ }
54
+
55
+ onEnter(handler: FsmStateHandler) {
56
+ return this._addHandler("onEnter", handler, true);
57
+ }
58
+ onExit(handler: FsmStateHandler) {
59
+ return this._addHandler("onExit", handler, false);
60
+ }
61
+ onStateError(handler: FsmStateErrorHandler) {
62
+ return this._addHandler("onStateError", handler);
63
+ }
64
+ dump(handler: FsmStateDumpHandler) {
65
+ return this._addHandler("dump", handler, true);
66
+ }
67
+ restore(handler: FsmStateDumpHandler) {
68
+ return this._addHandler("restore", handler, true);
69
+ }
70
+ getData<T>(key: string, recursive: boolean = true): T | undefined {
71
+ return (
72
+ (this.data[key] as T) ??
73
+ (recursive ? this.parent?.getData<T>(key, recursive) : undefined)
74
+ );
75
+ }
76
+ useData<T>(key: string) {
77
+ return [
78
+ (recursive: boolean = true) => this.getData<T>(key, recursive),
79
+ (value: T) => this.setData(key, value),
80
+ ];
81
+ }
82
+
83
+ async _handleError(error: Error | unknown) {
84
+ await this._runHandler("onStateError", this, error);
85
+ await this.process._handleStateError(this, error);
86
+ }
87
+ }
@@ -0,0 +1,13 @@
1
+ export const STATE_ANY = "*";
2
+ export const STATE_INITIAL = "";
3
+ export const STATE_FINAL = "";
4
+ export const EVENT_ANY = "*";
5
+ export const EVENT_EMPTY = "";
6
+
7
+ export type FsmStateConfig = {
8
+ key: string;
9
+
10
+ transitions: [string, string, string][];
11
+
12
+ states?: FsmStateConfig[];
13
+ };
@@ -0,0 +1,47 @@
1
+ import {
2
+ type FsmStateConfig,
3
+ EVENT_ANY,
4
+ STATE_ANY,
5
+ STATE_FINAL,
6
+ } from "./FsmStateConfig.ts";
7
+
8
+ export class FsmStateDescriptor {
9
+ transitions: Record<string, Record<string, string>> = {};
10
+ states: Record<string, FsmStateDescriptor> = {};
11
+
12
+ static build(config: FsmStateConfig) {
13
+ const descriptor = new FsmStateDescriptor();
14
+ for (const [from, event, to] of config.transitions) {
15
+ const index = (descriptor.transitions[from] =
16
+ descriptor.transitions[from] || {});
17
+ index[event] = to;
18
+ }
19
+ if (config.states) {
20
+ for (const substateConfig of config.states) {
21
+ descriptor.states[substateConfig.key] = this.build(substateConfig);
22
+ }
23
+ }
24
+ return descriptor;
25
+ }
26
+
27
+ getTargetStateKey(stateKey: string, eventKey: string) {
28
+ const pairs = [
29
+ [stateKey, eventKey],
30
+ [STATE_ANY, eventKey],
31
+ [stateKey, EVENT_ANY],
32
+ [STATE_ANY, EVENT_ANY],
33
+ ];
34
+ let targetKey;
35
+ for (
36
+ let i = 0, len = pairs.length;
37
+ targetKey === undefined && i < len;
38
+ i++
39
+ ) {
40
+ const [stateKey, eventKey] = pairs[i];
41
+ const stateTransitions = this.transitions[stateKey];
42
+ if (!stateTransitions) continue;
43
+ targetKey = stateTransitions[eventKey];
44
+ }
45
+ return targetKey !== undefined ? targetKey : STATE_FINAL;
46
+ }
47
+ }
@@ -0,0 +1,30 @@
1
+ import { FsmState, FsmStateHandler } from "../FsmState.ts";
2
+
3
+ export const KEY_HANDLERS = "handlers";
4
+
5
+ export function callStateHandlers(state: FsmState) {
6
+ const key = state.key;
7
+ for (
8
+ let parent: FsmState | undefined = state.parent;
9
+ !!parent;
10
+ parent = parent?.parent
11
+ ) {
12
+ const handlers =
13
+ parent.getData<Record<string, FsmStateHandler[]>>(KEY_HANDLERS)?.[key];
14
+ handlers?.forEach((handler) => handler(state));
15
+ }
16
+ }
17
+
18
+ export function addSubstateHandlers(
19
+ state: FsmState,
20
+ handlers: Record<string, FsmStateHandler>
21
+ ) {
22
+ const oldIndex =
23
+ state.getData<Record<string, FsmStateHandler[]>>(KEY_HANDLERS);
24
+ const index = oldIndex ? { ...oldIndex } : {};
25
+ for (const [key, handler] of Object.entries(handlers)) {
26
+ const list = (index[key] = index[key] || []);
27
+ list.push(handler);
28
+ }
29
+ state.setData(KEY_HANDLERS, index);
30
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./handlers.ts";
2
+ export * from "./printer.ts";
3
+ export * from "./tracer.ts";
@@ -0,0 +1,48 @@
1
+ import { type FsmProcess } from "../FsmProcess.ts";
2
+ import { type FsmState } from "../FsmState.ts";
3
+ export type Printer = (...args: any[]) => void;
4
+ export type PrinterConfig = {
5
+ prefix?: string;
6
+ print?: (...args: any[]) => void;
7
+ lineNumbers?: boolean;
8
+ };
9
+
10
+ export const KEY_PRINTER = "printer";
11
+
12
+ export function preparePrinter(
13
+ process: FsmProcess,
14
+ { prefix = "", print = console.log, lineNumbers = false }: PrinterConfig
15
+ ): Printer {
16
+ let lineCounter = 0;
17
+ const shift = () => {
18
+ let prefix = "";
19
+ for (let s = process.state?.parent; !!s; s = s.parent) {
20
+ prefix += " ";
21
+ }
22
+ return prefix;
23
+ };
24
+ const getPrefix = lineNumbers ? () => `[${++lineCounter}]${shift()}` : shift;
25
+ const printer = (...args: string[]) => print(prefix, getPrefix(), ...args);
26
+ return printer;
27
+ }
28
+
29
+ export function setPrinter(state: FsmState, config: PrinterConfig = {}) {
30
+ const printer = preparePrinter(state.process, config);
31
+ state.setData(KEY_PRINTER, printer);
32
+ }
33
+
34
+ export function setProcessPrinter(
35
+ process: FsmProcess,
36
+ config: PrinterConfig = {}
37
+ ) {
38
+ const printer = preparePrinter(process, config);
39
+ process.setData(KEY_PRINTER, printer);
40
+ }
41
+
42
+ export function getPrinter(state: FsmState): Printer {
43
+ return (
44
+ state.getData(KEY_PRINTER, true) ||
45
+ state.process.getData(KEY_PRINTER) ||
46
+ console.log
47
+ );
48
+ }
@@ -0,0 +1,19 @@
1
+ import { FsmProcess } from "../FsmProcess.ts";
2
+ import { FsmState } from "../FsmState.ts";
3
+ import { getPrinter, type Printer } from "./printer.ts";
4
+
5
+ export function setProcessTracer(process: FsmProcess, print?: Printer) {
6
+ return process.onStateCreate((state) => {
7
+ setStateTracer(state, print);
8
+ });
9
+ }
10
+
11
+ export function setStateTracer(state: FsmState, print?: Printer) {
12
+ const printLine = print || getPrinter(state);
13
+ state.onEnter(() => {
14
+ printLine(`<${state?.key} event="${state.process.event}">`);
15
+ });
16
+ state.onExit(() => {
17
+ printLine(`</${state.key}> <!-- event="${state.process.event}" -->`);
18
+ });
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./FsmProcess.ts";
2
+ export * from "./FsmState.ts";
3
+ export * from "./FsmStateConfig.ts";
4
+ export * from "./FsmStateDescriptor.ts";
5
+ export * from "./context/index.ts";
@@ -0,0 +1,7 @@
1
+ export function bindMethods<T>(obj: T, ...methods: string[]) {
2
+ const o = obj as any;
3
+ methods.forEach((methodName) => {
4
+ o[methodName] = (o[methodName] as Function).bind(o);
5
+ });
6
+ return obj;
7
+ }