@real-router/fsm 0.2.1 → 0.2.2

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.
Files changed (2) hide show
  1. package/README.md +34 -120
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,23 +1,18 @@
1
1
  # @real-router/fsm
2
2
 
3
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
3
+ > Synchronous finite state machine engine.
5
4
 
6
- Universal synchronous FSM engine for Real-Router. Zero dependencies, full TypeScript generics, O(1) transition lookup.
5
+ **Internal package** consumed by `@real-router/core`. Published to npm by historical accident do not depend on it directly.
7
6
 
8
- ## Installation
7
+ ## Purpose
9
8
 
10
- ```bash
11
- npm install @real-router/fsm
12
- # or
13
- pnpm add @real-router/fsm
14
- # or
15
- yarn add @real-router/fsm
16
- # or
17
- bun add @real-router/fsm
18
- ```
9
+ Drives the router's lifecycle state machine (`STOPPED → STARTED → DISPOSED`). Synchronous, zero-dependency, O(1) transition lookup.
10
+
11
+ ## Consumer
12
+
13
+ - `@real-router/core` — router lifecycle management
19
14
 
20
- ## Quick Start
15
+ ## Public API
21
16
 
22
17
  ```typescript
23
18
  import { FSM } from "@real-router/fsm";
@@ -32,128 +27,47 @@ const fsm = new FSM({
32
27
  },
33
28
  });
34
29
 
35
- fsm.send("TIMER"); // "yellow"
36
- fsm.send("TIMER"); // "red"
37
- fsm.send("RESET"); // "green"
38
- fsm.getState(); // "green"
39
- fsm.getContext(); // { count: 0 }
30
+ fsm.send("TIMER"); // "yellow"
31
+ fsm.send("TIMER"); // "red"
32
+ fsm.getState(); // "red"
33
+ fsm.getContext(); // { count: 0 }
40
34
  ```
41
35
 
42
- ---
43
-
44
- ## API
45
-
46
- ### `new FSM(config: FSMConfig<TStates, TEvents, TContext>)`
47
-
48
- Creates a new FSM instance.\
49
- `config.initial: TStates` — initial state\
50
- `config.context: TContext` — shared context object\
51
- `config.transitions: Record<TStates, Partial<Record<TEvents, TStates>>>` — transition table
36
+ | Method | Description |
37
+ |--------|-------------|
38
+ | `send(event, payload?)` | Trigger transition, return current state. No-op if no transition defined |
39
+ | `getState()` | Current state |
40
+ | `getContext()` | Context object (same reference as config) |
41
+ | `onTransition(listener)` | Subscribe to transitions, returns unsubscribe |
42
+ | `on(from, event, action)` | Register action for specific `(from, event)` pair |
43
+ | `forceState(state)` | Direct state update no actions, no listeners |
52
44
 
53
- ### `fsm.send(event, payload?): TStates`
54
-
55
- Sends an event to trigger a transition. Returns the current state after processing.\
56
- If no transition exists for the event in the current state, returns the current state (no-op).\
57
- State is updated before listeners fire (reentrancy-safe).
45
+ ### Type-Safe Payloads
58
46
 
59
47
  ```typescript
60
- fsm.send("TIMER"); // transitions and returns new state
61
- fsm.send("RESET"); // no-op if no transition defined, returns current state
62
- ```
63
-
64
- ### `fsm.getState(): TStates`
65
-
66
- Returns the current state.
67
-
68
- ### `fsm.getContext(): TContext`
69
-
70
- Returns the context object (same reference as provided in config).
71
-
72
- ### `fsm.onTransition(listener): () => void`
73
-
74
- Subscribes a listener to state transitions. Returns an unsubscribe function.\
75
- Listener receives `TransitionInfo` with `from`, `to`, `event`, and `payload` fields.\
76
- Not called on no-op sends (when no transition exists).
77
-
78
- ```typescript
79
- const unsub = fsm.onTransition((info) => {
80
- console.log(`${info.from} -> ${info.to} via ${info.event}`);
81
- });
82
-
83
- fsm.send("TIMER"); // logs: "green -> yellow via TIMER"
84
- unsub();
85
- ```
86
-
87
- ---
88
-
89
- ## Type-Safe Payloads
90
-
91
- Use `TPayloadMap` to require payloads for specific events:
92
-
93
- ```typescript
94
- import type { FSMConfig } from "@real-router/fsm";
95
-
96
- type State = "idle" | "loading" | "done";
97
- type Event = "FETCH" | "RESOLVE";
98
-
99
48
  interface PayloadMap {
100
49
  FETCH: { url: string };
101
50
  }
102
51
 
103
- const config: FSMConfig<State, Event, null> = {
104
- initial: "idle",
105
- context: null,
106
- transitions: {
107
- idle: { FETCH: "loading" },
108
- loading: { RESOLVE: "done" },
109
- done: {},
110
- },
111
- };
112
-
113
52
  const fsm = new FSM<State, Event, null, PayloadMap>(config);
114
-
115
- fsm.send("FETCH", { url: "/api" }); // OK payload required
116
- fsm.send("RESOLVE"); // OK — no payload needed
117
- fsm.send("FETCH"); // TypeScript error — missing payload
118
- ```
119
-
120
- ---
121
-
122
- ## Types
123
-
124
- ```typescript
125
- import type { FSMConfig, TransitionInfo } from "@real-router/fsm";
126
-
127
- interface FSMConfig<TStates, TEvents, TContext> {
128
- initial: TStates;
129
- context: TContext;
130
- transitions: Record<TStates, Partial<Record<TEvents, TStates>>>;
131
- }
132
-
133
- interface TransitionInfo<TStates, TEvents, TPayloadMap> {
134
- from: TStates;
135
- to: TStates;
136
- event: TEvents;
137
- payload: TPayloadMap[TEvents] | undefined;
138
- }
53
+ fsm.send("FETCH", { url: "/api" }); // required
54
+ fsm.send("RESOLVE"); // no payload
55
+ fsm.send("FETCH"); // TS error
139
56
  ```
140
57
 
141
- ---
142
-
143
- ## Design
58
+ ## Key Design Decisions
144
59
 
145
60
  - **Synchronous** — no async, no promises, no microtasks
146
- - **O(1) transitions** — cached current-state lookup, single property access per `send()`
147
- - **Zero-alloc hot path** — when no listeners are registered, `send()` allocates nothing
148
- - **Null-slot listener array** — `onTransition` reuses slots from unsubscribed listeners, preventing unbounded array growth
149
- - **Reentrancy-safe** — `send()` inside a listener sees the updated state; callers are responsible for preventing infinite recursion
150
-
151
- ---
61
+ - **`#currentTransitions` cache** — avoids double lookup `transitions[state][event]`
62
+ - **Zero-alloc hot path** — skips `TransitionInfo` allocation when no listeners
63
+ - **Null-slot listener array** — reuses slots from unsubscribed listeners
64
+ - **Reentrancy-safe** — state updated before listeners fire; callers responsible for preventing loops
65
+ - **`forceState()`** — bypasses `send()` overhead (~30ns saved per call) for router's navigate hot path
152
66
 
153
- ## Related Packages
67
+ ## Dependencies
154
68
 
155
- - [@real-router/core](https://www.npmjs.com/package/@real-router/core) — Core router
69
+ None (zero dependencies).
156
70
 
157
71
  ## License
158
72
 
159
- MIT © [Oleg Ivanov](https://github.com/greydragon888)
73
+ [MIT](../../LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/fsm",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "commonjs",
5
5
  "description": "Universal synchronous FSM engine for Real-Router",
6
6
  "main": "./dist/cjs/index.js",