@uuxxx/fsm 1.2.1 → 1.2.3

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/README.md CHANGED
@@ -1 +1,313 @@
1
- # Finite state machine
1
+ # @uuxxx/fsm
2
+
3
+ [![npm version](https://badge.fury.io/js/@uuxxx%2Ffsm.svg)](https://badge.fury.io/js/@uuxxx%2Ffsm)
4
+
5
+ A lightweight, type-safe finite state machine library for JavaScript/TypeScript with plugin support and lifecycle hooks.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @uuxxx/fsm
11
+ # or
12
+ pnpm add @uuxxx/fsm
13
+ # or
14
+ yarn add @uuxxx/fsm
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```typescript
20
+ import { makeFsm } from '@uuxxx/fsm';
21
+
22
+ type State = 'idle' | 'loading' | 'success' | 'error';
23
+
24
+ const STATES: State[] = ['idle', 'loading', 'success', 'error']
25
+
26
+ const fsm = makeFsm({
27
+ init: 'idle',
28
+ states: STATES,
29
+ transitions: {
30
+ start: {
31
+ from: 'idle',
32
+ to: 'loading',
33
+ },
34
+ succeed: {
35
+ from: 'loading',
36
+ to: 'success',
37
+ },
38
+ fail: {
39
+ from: 'loading',
40
+ to: 'error',
41
+ },
42
+ reset: {
43
+ from: ['success', 'error'],
44
+ to: 'idle',
45
+ },
46
+ goto: {
47
+ from: '*',
48
+ to: (state: State) => state
49
+ }
50
+ },
51
+ });
52
+
53
+ // Check current state
54
+ console.log(fsm.state()); // 'idle'
55
+
56
+ // Perform transitions
57
+ fsm.start();
58
+ console.log(fsm.state()); // 'loading'
59
+
60
+ fsm.succeed();
61
+ console.log(fsm.state()); // 'success'
62
+
63
+ fsm.reset()
64
+ console.log(fsm.state()) // 'idle'
65
+
66
+ fsm.goto('error')
67
+ console.log(fsm.state()) // 'error'
68
+ ```
69
+
70
+ ## API Reference
71
+
72
+ ### `makeFsm(config)`
73
+
74
+ Creates a new finite state machine instance.
75
+
76
+ #### Parameters
77
+
78
+ - `config`: Configuration object with the following properties:
79
+ - `init`: Initial state
80
+ - `states`: Array of all possible states
81
+ - `transitions`: Object defining state transitions
82
+ - `methods?`: Optional lifecycle methods
83
+ - `plugins?`: Optional array of plugins
84
+
85
+ #### Returns
86
+
87
+ An FSM instance with transition methods, state methods, and plugin APIs.
88
+
89
+ ### State Methods
90
+
91
+ #### `fsm.state()`
92
+
93
+ Returns the current state.
94
+
95
+ ```typescript
96
+ const currentState = fsm.state();
97
+ ```
98
+
99
+ #### `fsm.allStates()`
100
+
101
+ Returns an array of all possible states.
102
+
103
+ ```typescript
104
+ const allStates = fsm.allStates();
105
+ ```
106
+
107
+ ### Transitions
108
+
109
+ Transitions are defined as objects with `from` and `to` properties:
110
+
111
+ ```typescript
112
+ type Transition<TState> = {
113
+ from: '*' | TState | TState[];
114
+ to: TState | ((...args: any[]) => TState | Promise<TState>);
115
+ };
116
+ ```
117
+
118
+ - `from`: The state(s) this transition can occur from
119
+ - Single state: `'idle'`
120
+ - Multiple states: `['loading', 'error']`
121
+ - Any state: `'*'`
122
+ - `to`: The target state or a function returning the target state
123
+ - Static: `'loading'`
124
+ - Dynamic: `(userId: string) => \`user_\${userId}\``
125
+ - Async: `async (data) => await apiCall(data)`
126
+
127
+ #### Examples
128
+
129
+ ```typescript
130
+ const transitions = {
131
+ // Simple transition
132
+ 'idle -> loading': {
133
+ from: 'idle',
134
+ to: 'loading',
135
+ },
136
+
137
+ // Multiple source states
138
+ reset: {
139
+ from: ['success', 'error'],
140
+ to: 'idle',
141
+ },
142
+
143
+ // Wildcard (from any state)
144
+ goto: {
145
+ from: '*',
146
+ to: (targetState: State) => targetState,
147
+ },
148
+
149
+ // Async transition
150
+ 'async fetch': {
151
+ from: 'idle',
152
+ to: async () => {
153
+ const result = await fetchData();
154
+ return result.success ? 'success' : 'error';
155
+ },
156
+ },
157
+ };
158
+ ```
159
+
160
+ ### Lifecycle Methods
161
+
162
+ Lifecycle methods can be attached to the FSM configuration:
163
+
164
+ ```typescript
165
+ const config = {
166
+ // ... other config
167
+ methods: {
168
+ onBeforeTransition: (event) => {
169
+ console.log('About to transition:', event);
170
+ // Return false to cancel the transition
171
+ return true;
172
+ },
173
+ onAfterTransition: (event) => {
174
+ console.log('Transition completed:', event);
175
+ },
176
+ },
177
+ };
178
+ ```
179
+
180
+ #### `onBeforeTransition(event)`
181
+
182
+ Called before a transition occurs. Return `false` to cancel the transition.
183
+
184
+ **Parameters:**
185
+ - `event`: Object with `transition`, `from`, `to`, and optional `args`
186
+
187
+ #### `onAfterTransition(event)`
188
+
189
+ Called after a successful transition.
190
+
191
+ **Parameters:**
192
+ - `event`: Object with `transition`, `from`, `to`, and optional `args`
193
+
194
+ ## Plugins
195
+
196
+ Plugins extend the FSM with additional functionality. Each plugin receives an API object and returns a plugin definition.
197
+
198
+ ### Plugin API
199
+
200
+ Plugins have access to:
201
+
202
+ - `api.state()`: Get current state
203
+ - `api.allStates()`: Get all states
204
+ - `api.init(callback)`: Register initialization callback
205
+ - `api.onBeforeTransition(callback)`: Register before transition callback
206
+ - `api.onAfterTransition(callback)`: Register after transition callback
207
+
208
+ ### Creating a Plugin
209
+
210
+ ```typescript
211
+ const myPlugin = (options) => (api) => {
212
+ // Plugin initialization
213
+ api.init((initialState) => {
214
+ console.log('FSM initialized with state:', initialState);
215
+ });
216
+
217
+ // Listen to transitions
218
+ api.onBeforeTransition((event) => {
219
+ console.log('Transition starting:', event);
220
+ });
221
+
222
+ // Return plugin definition
223
+ return {
224
+ name: 'my-plugin',
225
+ api: {
226
+ // Custom methods exposed on fsm['my-plugin']
227
+ doSomething: () => {
228
+ return api.state();
229
+ },
230
+ },
231
+ };
232
+ };
233
+ ```
234
+
235
+ ### Using Plugins
236
+
237
+ ```typescript
238
+ const config = {
239
+ // ... other config
240
+ plugins: [myPlugin({ someOption: true })],
241
+ };
242
+
243
+ const fsm = makeFsm(config);
244
+
245
+ // Access plugin API
246
+ const currentState = fsm['my-plugin'].doSomething();
247
+ ```
248
+
249
+ ## Built-in Plugins
250
+
251
+ ### History Plugin
252
+
253
+ Tracks state history and provides navigation methods.
254
+
255
+ ```typescript
256
+ import { makeFsm, historyPlugin } from '@uuxxx/fsm';
257
+
258
+ const config = {
259
+ // ... config
260
+ plugins: [historyPlugin()],
261
+ };
262
+
263
+ const fsm = makeFsm(config);
264
+
265
+ // Navigate
266
+ fsm.goto('state1');
267
+ fsm.goto('state2');
268
+
269
+ // History API
270
+ console.log(fsm.history.get()); // ['initial', 'state1', 'state2']
271
+
272
+ fsm.history.back(1); // Go back 1 step
273
+ fsm.history.forward(1); // Go forward 1 step
274
+ ```
275
+
276
+ #### History API Methods
277
+
278
+ - `fsm.history.get()`: Get the full history array
279
+ - `fsm.history.back(steps?)`: Go back N steps (default: 1)
280
+ - `fsm.history.forward(steps?)`: Go forward N steps (default: 1)
281
+
282
+ ## Error Handling
283
+
284
+ The FSM throws errors in the following cases:
285
+
286
+ - Invalid transition (current state doesn't match `from`)
287
+ - Pending async transition when starting a new sync transition
288
+ - Invalid target state
289
+ - Duplicate plugin names
290
+
291
+ ```typescript
292
+ try {
293
+ fsm.invalidTransition();
294
+ } catch (error) {
295
+ console.error(error.message); // [FSM]: Transition: "invalidTransition" is forbidden
296
+ }
297
+ ```
298
+
299
+ ## TypeScript Support
300
+
301
+ The library is fully typed. Type inference works automatically:
302
+
303
+ ```typescript
304
+ const fsm = makeFsm({
305
+ init: 'idle',
306
+ states: ['idle', 'running', 'stopped'],
307
+ transitions: {
308
+ start: { from: 'idle', to: 'running' },
309
+ stop: { from: 'running', to: 'stopped' },
310
+ },
311
+ });
312
+ // fsm is fully typed - autocomplete works for transitions and states
313
+ ```
@@ -1,10 +1,11 @@
1
- type AnyFn = (...args: any[]) => any;
2
- type Noop = () => void;
3
- type Rec<T = unknown> = Record<string, T>;
4
- type Key = string | number | symbol;
5
- type Vdx<T> = T | void;
6
- type KeyOf<T extends Rec> = keyof T;
7
- type Entries<T extends Rec> = { [K in KeyOf<T>]: [K, T[K]] }[KeyOf<T>];
1
+ type AnyFn = (...args: any[]) => any; //#endregion
2
+ type Rec<T = unknown> = Record<string, T>; //#endregion
3
+ type KeyOf<T extends Rec> = keyof T; //#endregion
4
+ type Noop = () => void; //#endregion
5
+ type Key = string | number | symbol; //#endregion
6
+ type Vdx<T> = T | void; //#endregion
7
+ type EmptyArray = []; //#endregion
8
+ type Entries<T extends Rec> = { [K in KeyOf<T>]: [K, T[K]] }[KeyOf<T>]; //#endregion
8
9
  type Label = string;
9
10
  type Transition<TState extends Label> = {
10
11
  from: '*' | TState | TState[];
@@ -34,4 +35,4 @@ type PluginApi = {
34
35
  api: Rec<AnyFn>;
35
36
  };
36
37
  type Plugin<TState extends Label = Label, TTransitions extends Rec<Transition<TState>> = Rec<Transition<TState>>> = (api: ApiForPlugin<TState, TTransitions>) => PluginApi;
37
- export { Transition as a, Rec as c, LifecycleMethods as i, Plugin as n, Label as o, StateMethods as r, KeyOf as s, ApiForPlugin as t };
38
+ export { Transition as a, KeyOf as c, LifecycleMethods as i, Rec as l, Plugin as n, Label as o, StateMethods as r, EmptyArray as s, ApiForPlugin as t };
@@ -1,10 +1,10 @@
1
- import { a as Transition, c as Rec, o as Label, t as ApiForPlugin } from "./Plugin-C8fnOXt5.js";
1
+ import { a as Transition, l as Rec, o as Label, t as ApiForPlugin } from "./Plugin-DGULTFg-.js";
2
2
  declare const historyPlugin: <TState extends Label, TTransitions extends Rec<Transition<TState>>>() => (api: ApiForPlugin<TState, TTransitions>) => {
3
3
  name: "history";
4
4
  api: {
5
- get(): Label[];
6
- back(steps: number): Label;
7
- forward(steps: number): Label;
5
+ get(): TState[];
6
+ back(steps: number): TState;
7
+ forward(steps: number): TState;
8
8
  };
9
9
  };
10
10
  export { historyPlugin as fsmHistoryPlugin };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { a as Transition, c as Rec, i as LifecycleMethods, n as Plugin, o as Label, r as StateMethods, s as KeyOf } from "./Plugin-C8fnOXt5.js";
2
- type EmptyArray = [];
1
+ import { a as Transition, c as KeyOf, i as LifecycleMethods, l as Rec, n as Plugin, o as Label, r as StateMethods, s as EmptyArray } from "./Plugin-DGULTFg-.js";
3
2
  type Config<TState extends Label, TTransitions extends Rec<Transition<TState>>, TPlugins extends Array<Plugin<TState, TTransitions>> = EmptyArray> = {
4
3
  init: TState;
5
4
  states: TState[];
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- let t={nlx:t=>null===t,ulx:t=>void 0===t,nil:e=>t.nlx(e)||t.ulx(e),not:{nlx:t=>null!==t,ulx:t=>void 0!==t,nil:e=>t.not.nlx(e)&&t.not.ulx(e)},array:t=>Array.isArray(t),string:t=>"string"==typeof t,function:t=>"function"==typeof t,promise:t=>t instanceof Promise,boolean:t=>"boolean"==typeof t,false:t=>!1===t,true:t=>!0===t};function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),r.push.apply(r,n)}return r}function n(t){for(var n=1;n<arguments.length;n++){var i=null!=arguments[n]?arguments[n]:{};n%2?r(Object(i),!0).forEach(function(r){!function(t,r,n){var i;(i=function(t,r){if("object"!=e(t)||!t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var i=n.call(t,r||"default");if("object"!=e(i))return i;throw TypeError("@@toPrimitive must return a primitive value.")}return("string"===r?String:Number)(t)}(r,"string"),(r="symbol"==e(i)?i:i+"")in t)?Object.defineProperty(t,r,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[r]=n}(t,r,i[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(i)):r(Object(i)).forEach(function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(i,e))})}return t}let i=e=>{var r,i,o,l;let s,a,u,f,c,m,b,p,y,g=e.init,v=(s=new Map,{listen(t,e){var r;return s.has(t)||s.set(t,[]),null==(r=s.get(t))||r.push(e),()=>{this.unlisten(t,e)}},unlisten(t,e){let r=s.get(t);if(!r)return;let n=r.filter(t=>t!==e);n.length?s.set(t,n):s.delete(t)},emit(e,...r){var n,i;return null!=(n=null==(i=s.get(e))?void 0:i.map(t=>t(...r)).filter(t.not.ulx))?n:[]},unlistenAll(t){s.delete(t)}});Object.entries(null!=(r=e.methods)?r:{}).forEach(([t,e])=>{v.listen(t,e)}),v.listen("onAfterTransition",({to:t})=>{g=t}),v.listen("error",t=>{throw Error(`[FSM]: ${t}`)});let O={state:()=>g,allStates:()=>[...e.states]},d=(i=e.transitions,o=O.state,u={},c=f={register(e,r){let n=r=>{r.to===r.from?v.emit("warn",`
2
- Transition: "${e}" is canceled because it's circular.
1
+ let t={nlx:t=>null===t,ulx:t=>void 0===t,nil:e=>t.nlx(e)||t.ulx(e),not:{nlx:t=>null!==t,ulx:t=>void 0!==t,nil:e=>t.not.nlx(e)&&t.not.ulx(e)},array:t=>Array.isArray(t),string:t=>"string"==typeof t,function:t=>"function"==typeof t,promise:t=>t instanceof Promise,boolean:t=>"boolean"==typeof t,false:t=>!1===t,true:t=>!0===t};function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),r.push.apply(r,n)}return r}function n(t){for(var n=1;n<arguments.length;n++){var i=null!=arguments[n]?arguments[n]:{};n%2?r(Object(i),!0).forEach(function(r){!function(t,r,n){var i;(i=function(t,r){if("object"!=e(t)||!t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var i=n.call(t,r||"default");if("object"!=e(i))return i;throw TypeError("@@toPrimitive must return a primitive value.")}return("string"===r?String:Number)(t)}(r,"string"),(r="symbol"==e(i)?i:i+"")in t)?Object.defineProperty(t,r,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[r]=n}(t,r,i[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(i)):r(Object(i)).forEach(function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(i,e))})}return t}let i=e=>{var r,i,o;let l,s,a,u,f,c,m=e.init,b=e.states.includes(e.init)?[...e.states]:[...e.states,e.init],p=(l=new Map,{listen(t,e){var r;return l.has(t)||l.set(t,[]),null==(r=l.get(t))||r.push(e),()=>{this.unlisten(t,e)}},unlisten(t,e){let r=l.get(t);if(!r)return;let n=r.filter(t=>t!==e);n.length?l.set(t,n):l.delete(t)},emit(e,...r){var n,i;return null!=(n=null==(i=l.get(e))?void 0:i.map(t=>t(...r)).filter(t.not.ulx))?n:[]},unlistenAll(t){l.delete(t)}});Object.entries(null!=(r=e.methods)?r:{}).forEach(([t,e])=>{p.listen(t,e)}),p.listen("onAfterTransition",({to:t})=>{m=t}),p.listen("error",t=>{throw Error(`[FSM]: ${t}`)});let y={state:()=>m,allStates:()=>b},g=(i=e.transitions,s=((e,{state:r,allStates:n})=>{let i,o={},l={register(s,a){let u=r=>{n().includes(r.to)?r.to===r.from?e.emit("warn",`
2
+ Transition: "${s}" is canceled because it's circular.
3
3
  Current state is ${r.from}. Transition target state is ${r.to}
4
- `):v.emit("onBeforeTransition",r).filter(t.boolean).every(t.true)&&v.emit("onAfterTransition",r)};return u[e]=(...i)=>{if(t.array(r.from)?!r.from.includes(o()):"*"!==r.from&&r.from!==o())return v.emit("error",`Transition: "${e}" is forbidden`),o();if(a)return v.emit("error",`Transition: "${e}" can't be made. Has pending transtion: "${a}"`),o();if(!t.function(r.to))return n({transition:e,from:o(),to:r.to}),o();let l=r.to(...i);return t.promise(l)?(a=e,l.then(t=>(a=void 0,n({transition:e,from:o(),to:t,args:i}),o()))):(n({transition:e,from:o(),to:l,args:i}),o())},f},make:()=>u},Object.entries(i).forEach(([t,e])=>c.register(t,e)),c.make()),h=(l=e.plugins,m={},b=n({init(t){v.listen("init",t)},onBeforeTransition:t=>v.listen("onBeforeTransition",t),onAfterTransition:t=>v.listen("onAfterTransition",t)},O),y=p={register(t){let{name:e,api:r}=t(b);return e in m&&v.emit("error",`There are at least two plugins with the same name: ${e}`),m[e]=r,p},make:()=>m},(null!=l?l:[]).forEach(y.register),y.make());return v.emit("init",g),n(n(n({},O),h),d)};export{i as makeFsm};
4
+ `):e.emit("onBeforeTransition",r).filter(t.boolean).every(t.true)&&e.emit("onAfterTransition",r):e.emit("error",`Transition: "${s}" can't be executed. It has invalid "to": "${r.to}"`)};return o[s]=(...n)=>{if(t.array(a.from)?!a.from.includes(r()):"*"!==a.from&&a.from!==r())return e.emit("error",`Transition: "${s}" is forbidden`),r();if(i)return e.emit("error",`Transition: "${s}" can't be made. Has pending transtion: "${i}"`),r();if(!t.function(a.to))return u({transition:s,from:r(),to:a.to}),r();let o=a.to(...n);return t.promise(o)?(i=s,o.then(t=>(i=void 0,u({transition:s,from:r(),to:t,args:n}),r()))):(u({transition:s,from:r(),to:o,args:n}),r())},l},make:()=>o};return l})(p,y),Object.entries(i).forEach(([t,e])=>s.register(t,e)),s.make()),d=(o=e.plugins,a={},u=n({init(t){p.listen("init",t)},onBeforeTransition:t=>p.listen("onBeforeTransition",t),onAfterTransition:t=>p.listen("onAfterTransition",t)},y),c=f={register(t){let{name:e,api:r}=t(u);return e in a&&p.emit("error",`There are at least two plugins with the same name: "${e}"`),a[e]=r,f},make:()=>a},(null!=o?o:[]).forEach(c.register),c.make());return p.emit("init",m),n(n(n({},y),d),g)};export{i as makeFsm};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uuxxx/fsm",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "author": "Artem Tryapichnikov <golysheeet@gmail.com>",
5
5
  "license": "MIT",
6
6
  "description": "Javascript library for creating finite state machine",
@@ -18,10 +18,8 @@
18
18
  "fsm"
19
19
  ],
20
20
  "engines": {
21
- "node": " >= 20",
22
- "pnpm": ">= 10",
23
- "npm": "Please use pnpm instead of yarn to install dependencies",
24
- "yarn": "Please use pnpm instead of yarn to install dependencies"
21
+ "node": ">= 20",
22
+ "pnpm": ">= 10"
25
23
  },
26
24
  "files": [
27
25
  "dist",
@@ -44,26 +42,25 @@
44
42
  "@babel/preset-typescript": "^7.27.1",
45
43
  "@changesets/cli": "^2.29.7",
46
44
  "@swc/core": "^1.13.5",
47
- "@types/jest": "^30.0.0",
48
- "babel-jest": "^30.2.0",
49
45
  "eslint": "^9.37.0",
50
46
  "eslint-config-xo-typescript": "^9.0.0",
51
- "jest": "^30.2.0",
52
- "jiti": "^2.6.1",
53
47
  "lefthook": "^2.0.2",
54
48
  "rolldown": "^1.0.0-beta.41",
55
49
  "rolldown-plugin-dts": "^0.22.1",
56
50
  "rollup-plugin-swc3": "^0.12.1",
57
- "ts-jest": "^29.4.4",
58
51
  "ts-node": "^10.9.2",
59
52
  "tslib": "^2.8.1",
60
- "typescript": "^5.9.3"
53
+ "typescript": "^5.9.3",
54
+ "vitest": "^4.0.18"
55
+ },
56
+ "dependencies": {
57
+ "@uuxxx/utils": "^0.0.3"
61
58
  },
62
59
  "scripts": {
63
60
  "build": "rolldown -c rolldown.config.ts",
64
61
  "changeset:version": "changeset version && git add --all && git commit -m 'chore: bump'",
65
62
  "changeset:publish": "changeset publish",
66
- "test": "jest",
63
+ "test": "vitest run",
67
64
  "lint": "eslint",
68
65
  "lint:fix": "eslint --fix",
69
66
  "check:types": "tsc --noEmit"