@duyquangnvx/state-machine 0.1.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/README.md +240 -0
- package/dist/index.cjs +202 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +118 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# state-machine
|
|
2
|
+
|
|
3
|
+
A generic, type-safe finite state machine library for TypeScript with synchronous lifecycle hooks, transition guards, hierarchical states, event history, and a tick-based update loop.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Fully generic** — `StateMachine<TContext, TStateId>` works with any context and state ID types
|
|
8
|
+
- **Synchronous lifecycle** — `onEnter` / `onExit` / `onUpdate` are all sync for deterministic state at every moment
|
|
9
|
+
- **Transition guards** — `canTransitionTo(target, ctx)` lets each state control allowed transitions
|
|
10
|
+
- **Tick-based updates** — `onUpdate(ctx, dt)` runs every frame/tick; return a state ID to auto-transition
|
|
11
|
+
- **Hierarchical states** — `HierarchicalState` embeds a nested state machine inside a parent state
|
|
12
|
+
- **Event system** — Subscribe to state changes with `on(listener)`, unsubscribe with the returned function
|
|
13
|
+
- **Bounded history** — Configurable history buffer for debugging and replay
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm run build
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Define states
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { BaseState } from "state-machine";
|
|
26
|
+
|
|
27
|
+
type MyStateId = "idle" | "loading" | "ready";
|
|
28
|
+
|
|
29
|
+
interface MyContext {
|
|
30
|
+
data: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class IdleState extends BaseState<MyContext, MyStateId> {
|
|
34
|
+
readonly id = "idle" as const;
|
|
35
|
+
|
|
36
|
+
override onEnter(ctx: MyContext, prevState: MyStateId | null): void {
|
|
37
|
+
console.log("Entered idle");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override onUpdate(ctx: MyContext, dt: number): MyStateId | undefined {
|
|
41
|
+
return "loading"; // auto-transition on next tick
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class LoadingState extends BaseState<MyContext, MyStateId> {
|
|
46
|
+
readonly id = "loading" as const;
|
|
47
|
+
|
|
48
|
+
override onUpdate(): MyStateId | undefined {
|
|
49
|
+
return "ready";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class ReadyState extends BaseState<MyContext, MyStateId> {
|
|
54
|
+
readonly id = "ready" as const;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Create and run the machine
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { StateMachine } from "state-machine";
|
|
62
|
+
|
|
63
|
+
const sm = new StateMachine<MyContext, MyStateId>({
|
|
64
|
+
states: [new IdleState(), new LoadingState(), new ReadyState()],
|
|
65
|
+
initialState: "idle",
|
|
66
|
+
context: { data: null },
|
|
67
|
+
historySize: 50, // optional, defaults to 100
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
sm.start(); // enters "idle", calls onEnter
|
|
71
|
+
sm.update(0.016); // calls onUpdate, may auto-transition
|
|
72
|
+
sm.transitionTo("ready"); // explicit transition
|
|
73
|
+
sm.stop(); // exits current state, calls onExit
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Transition guards
|
|
77
|
+
|
|
78
|
+
Override `canTransitionTo` to block transitions conditionally:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
class IdleState extends BaseState<MyContext, MyStateId> {
|
|
82
|
+
readonly id = "idle" as const;
|
|
83
|
+
|
|
84
|
+
override canTransitionTo(target: MyStateId, ctx: MyContext): boolean {
|
|
85
|
+
return ctx.data !== null; // only allow transitions when data is loaded
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Blocked transitions throw `TransitionDeniedError`.
|
|
91
|
+
|
|
92
|
+
### Events and history
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const unsub = sm.on((event) => {
|
|
96
|
+
console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
sm.getHistory(); // ReadonlyArray<StateChangeEvent<MyStateId>>
|
|
100
|
+
unsub(); // stop listening
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Async work as a state
|
|
104
|
+
|
|
105
|
+
The state machine is fully synchronous — `onEnter`, `onExit`, and `onUpdate` all return `void`. This ensures the machine is always in exactly one definite state at any moment, which is critical for game loops and real-time systems.
|
|
106
|
+
|
|
107
|
+
To handle async operations (API calls, loading, etc.), **model the async work as its own state** that polls for completion:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
type MyStateId = "IDLE" | "LOADING" | "READY";
|
|
111
|
+
|
|
112
|
+
interface MyContext {
|
|
113
|
+
loading: boolean;
|
|
114
|
+
data: string | null;
|
|
115
|
+
fetchData: () => Promise<string>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class LoadingState extends BaseState<MyContext, MyStateId> {
|
|
119
|
+
readonly id = "LOADING" as const;
|
|
120
|
+
|
|
121
|
+
override onEnter(ctx: MyContext): void {
|
|
122
|
+
ctx.loading = true;
|
|
123
|
+
ctx.fetchData().then((data) => {
|
|
124
|
+
ctx.data = data;
|
|
125
|
+
ctx.loading = false;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override onUpdate(ctx: MyContext): MyStateId | undefined {
|
|
130
|
+
if (!ctx.loading) return "READY";
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This pattern keeps the state machine synchronous while still supporting async operations. The state machine ticks on each frame/update, and the loading state simply polls until the async work completes.
|
|
137
|
+
|
|
138
|
+
## API
|
|
139
|
+
|
|
140
|
+
### `StateMachine<TContext, TStateId>`
|
|
141
|
+
|
|
142
|
+
| Method / Property | Description |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `start(): void` | Enter the initial state. Idempotent. |
|
|
145
|
+
| `stop(): void` | Exit the current state and shut down. |
|
|
146
|
+
| `transitionTo(stateId): void` | Transition to a specific state (checks guard). |
|
|
147
|
+
| `update(dt): void` | Tick the current state; auto-transitions if `onUpdate` returns a state ID. |
|
|
148
|
+
| `on(listener)` | Subscribe to state changes. Returns unsubscribe function. |
|
|
149
|
+
| `getHistory()` | Get the bounded transition history. |
|
|
150
|
+
| `currentStateId` | The active state's ID. Throws if not started. |
|
|
151
|
+
| `isStarted` | Whether the machine is running. |
|
|
152
|
+
| `context` | The shared mutable context object. |
|
|
153
|
+
|
|
154
|
+
### `BaseState<TContext, TStateId>` (lifecycle hooks)
|
|
155
|
+
|
|
156
|
+
| Hook | Signature | Notes |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `canTransitionTo` | `(target, ctx) => boolean` | Sync guard. Default: `true`. |
|
|
159
|
+
| `onEnter` | `(ctx, prevState) => void` | Called when entering. `prevState` is `null` on `start()`. |
|
|
160
|
+
| `onUpdate` | `(ctx, dt) => TStateId \| undefined` | Return a state ID to auto-transition. |
|
|
161
|
+
| `onExit` | `(ctx, nextState) => void` | Called when leaving. `nextState` is `null` on `stop()`. |
|
|
162
|
+
|
|
163
|
+
### `HierarchicalState<TContext, TParentId, TChildId>`
|
|
164
|
+
|
|
165
|
+
A composite state that runs a nested `StateMachine`. Override `createChildConfig(ctx)` to define the child machine. The child starts/stops automatically with the parent state.
|
|
166
|
+
|
|
167
|
+
### Errors
|
|
168
|
+
|
|
169
|
+
| Error | Thrown when |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `StateNotFoundError` | Transitioning to an unknown state ID. |
|
|
172
|
+
| `MachineNotStartedError` | Calling `transitionTo`, `update`, or `currentStateId` before `start()`. |
|
|
173
|
+
| `TransitionDeniedError` | `canTransitionTo` returns `false`. |
|
|
174
|
+
|
|
175
|
+
## Demos
|
|
176
|
+
|
|
177
|
+
### Tower Defense
|
|
178
|
+
|
|
179
|
+
Three interlocking state machines (tower, enemy, wave) simulating a tower defense game loop.
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
Tower: BUILDING -> IDLE -> TARGETING -> ATTACKING -> IDLE
|
|
183
|
+
Enemy: SPAWNING -> MOVING -> ATTACKING -> DYING -> DEAD
|
|
184
|
+
Wave: PREPARING -> WAVE_ACTIVE -> WAVE_COMPLETE -> ... -> GAME_OVER
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npm run build && npm run demo
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Slot Machine
|
|
192
|
+
|
|
193
|
+
Demonstrates the "async work as a state" pattern — API calls (bet deduction, payout crediting) are modeled as dedicated states that poll for completion.
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
IDLE -> DEDUCTING_BET -> SPINNING -> STOPPING -> EVALUATING -> CREDITING_WIN -> IDLE
|
|
197
|
+
|
|
|
198
|
+
+--> IDLE (no win)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npm run build && npm run demo:slot
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Testing
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npm test
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
67 tests covering the core library (construction, start/stop, transitions, guards, updates, events, history, hierarchical states) and both demos.
|
|
212
|
+
|
|
213
|
+
## Project structure
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
src/
|
|
217
|
+
lib/ # Core library
|
|
218
|
+
State.ts # BaseState abstract class
|
|
219
|
+
StateMachine.ts # StateMachine engine
|
|
220
|
+
HierarchicalState.ts # Composite state with nested machine
|
|
221
|
+
StateEvent.ts # Event emitter with bounded history
|
|
222
|
+
interfaces.ts # IState, IStateMachine, config types
|
|
223
|
+
errors.ts # StateNotFoundError, MachineNotStartedError, TransitionDeniedError
|
|
224
|
+
index.ts # Public exports
|
|
225
|
+
demo/
|
|
226
|
+
tower/ # Tower defense FSM (tower states)
|
|
227
|
+
enemy/ # Tower defense FSM (enemy states)
|
|
228
|
+
wave/ # Tower defense FSM (wave management)
|
|
229
|
+
slot/ # Slot machine FSM
|
|
230
|
+
main.ts # Tower defense entry point
|
|
231
|
+
slot-main.ts # Slot machine entry point
|
|
232
|
+
GameLoop.ts # Tick-based game loop utility
|
|
233
|
+
tests/
|
|
234
|
+
lib/ # Core library tests
|
|
235
|
+
demo/ # Demo-specific tests
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
ISC
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/lib/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BaseState: () => BaseState,
|
|
24
|
+
HierarchicalState: () => HierarchicalState,
|
|
25
|
+
MachineNotStartedError: () => MachineNotStartedError,
|
|
26
|
+
StateEventEmitter: () => StateEventEmitter,
|
|
27
|
+
StateMachine: () => StateMachine,
|
|
28
|
+
StateNotFoundError: () => StateNotFoundError,
|
|
29
|
+
TransitionDeniedError: () => TransitionDeniedError
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/lib/State.ts
|
|
34
|
+
var BaseState = class {
|
|
35
|
+
canTransitionTo(_targetState, _ctx) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
onEnter(_ctx, _prevState) {
|
|
39
|
+
}
|
|
40
|
+
onUpdate(_ctx, _dt) {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
onExit(_ctx, _nextState) {
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/lib/StateEvent.ts
|
|
48
|
+
var StateEventEmitter = class {
|
|
49
|
+
listeners = [];
|
|
50
|
+
history = [];
|
|
51
|
+
maxHistory;
|
|
52
|
+
constructor(maxHistory = 100) {
|
|
53
|
+
this.maxHistory = maxHistory;
|
|
54
|
+
}
|
|
55
|
+
on(listener) {
|
|
56
|
+
this.listeners.push(listener);
|
|
57
|
+
return () => {
|
|
58
|
+
const idx = this.listeners.indexOf(listener);
|
|
59
|
+
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
emit(change) {
|
|
63
|
+
this.history.push(change);
|
|
64
|
+
if (this.history.length > this.maxHistory) {
|
|
65
|
+
this.history.shift();
|
|
66
|
+
}
|
|
67
|
+
for (const listener of this.listeners) {
|
|
68
|
+
listener(change);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
getHistory() {
|
|
72
|
+
return [...this.history];
|
|
73
|
+
}
|
|
74
|
+
clear() {
|
|
75
|
+
this.history.length = 0;
|
|
76
|
+
this.listeners.length = 0;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/lib/errors.ts
|
|
81
|
+
var StateNotFoundError = class extends Error {
|
|
82
|
+
constructor(stateId) {
|
|
83
|
+
super(`State not found: "${stateId}"`);
|
|
84
|
+
this.name = "StateNotFoundError";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var MachineNotStartedError = class extends Error {
|
|
88
|
+
constructor() {
|
|
89
|
+
super("State machine has not been started. Call start() first.");
|
|
90
|
+
this.name = "MachineNotStartedError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var TransitionDeniedError = class extends Error {
|
|
94
|
+
constructor(from, to) {
|
|
95
|
+
super(`Transition denied: "${from}" -> "${to}"`);
|
|
96
|
+
this.name = "TransitionDeniedError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/lib/StateMachine.ts
|
|
101
|
+
var StateMachine = class {
|
|
102
|
+
stateMap = /* @__PURE__ */ new Map();
|
|
103
|
+
emitter;
|
|
104
|
+
currentState = null;
|
|
105
|
+
initialStateId;
|
|
106
|
+
_isStarted = false;
|
|
107
|
+
context;
|
|
108
|
+
constructor(config) {
|
|
109
|
+
this.context = config.context;
|
|
110
|
+
this.initialStateId = config.initialState;
|
|
111
|
+
this.emitter = new StateEventEmitter(config.historySize ?? 100);
|
|
112
|
+
for (const state of config.states) {
|
|
113
|
+
if (this.stateMap.has(state.id)) {
|
|
114
|
+
throw new Error(`Duplicate state id: "${state.id}"`);
|
|
115
|
+
}
|
|
116
|
+
this.stateMap.set(state.id, state);
|
|
117
|
+
}
|
|
118
|
+
if (!this.stateMap.has(config.initialState)) {
|
|
119
|
+
throw new StateNotFoundError(config.initialState);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
get currentStateId() {
|
|
123
|
+
if (!this.currentState) throw new MachineNotStartedError();
|
|
124
|
+
return this.currentState.id;
|
|
125
|
+
}
|
|
126
|
+
get isStarted() {
|
|
127
|
+
return this._isStarted;
|
|
128
|
+
}
|
|
129
|
+
start() {
|
|
130
|
+
if (this._isStarted) return;
|
|
131
|
+
const state = this.stateMap.get(this.initialStateId);
|
|
132
|
+
if (!state) throw new StateNotFoundError(this.initialStateId);
|
|
133
|
+
this.currentState = state;
|
|
134
|
+
this._isStarted = true;
|
|
135
|
+
this.currentState.onEnter(this.context, null);
|
|
136
|
+
}
|
|
137
|
+
stop() {
|
|
138
|
+
if (!this._isStarted || !this.currentState) return;
|
|
139
|
+
this.currentState.onExit(this.context, null);
|
|
140
|
+
this.currentState = null;
|
|
141
|
+
this._isStarted = false;
|
|
142
|
+
}
|
|
143
|
+
transitionTo(stateId) {
|
|
144
|
+
if (!this.currentState) throw new MachineNotStartedError();
|
|
145
|
+
const to = this.stateMap.get(stateId);
|
|
146
|
+
if (!to) throw new StateNotFoundError(stateId);
|
|
147
|
+
const from = this.currentState;
|
|
148
|
+
if (!from.canTransitionTo(stateId, this.context)) {
|
|
149
|
+
throw new TransitionDeniedError(from.id, stateId);
|
|
150
|
+
}
|
|
151
|
+
const change = {
|
|
152
|
+
from: from.id,
|
|
153
|
+
to: to.id,
|
|
154
|
+
timestamp: Date.now()
|
|
155
|
+
};
|
|
156
|
+
from.onExit(this.context, to.id);
|
|
157
|
+
this.currentState = to;
|
|
158
|
+
this.emitter.emit(change);
|
|
159
|
+
to.onEnter(this.context, from.id);
|
|
160
|
+
}
|
|
161
|
+
update(dt) {
|
|
162
|
+
if (!this.currentState) throw new MachineNotStartedError();
|
|
163
|
+
const next = this.currentState.onUpdate(this.context, dt);
|
|
164
|
+
if (next) {
|
|
165
|
+
this.transitionTo(next);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
on(listener) {
|
|
169
|
+
return this.emitter.on(listener);
|
|
170
|
+
}
|
|
171
|
+
getHistory() {
|
|
172
|
+
return this.emitter.getHistory();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/lib/HierarchicalState.ts
|
|
177
|
+
var HierarchicalState = class extends BaseState {
|
|
178
|
+
childMachine = null;
|
|
179
|
+
onEnter(ctx, _prevState) {
|
|
180
|
+
const config = this.createChildConfig(ctx);
|
|
181
|
+
this.childMachine = new StateMachine(config);
|
|
182
|
+
this.childMachine.start();
|
|
183
|
+
}
|
|
184
|
+
onUpdate(_ctx, _dt) {
|
|
185
|
+
return void 0;
|
|
186
|
+
}
|
|
187
|
+
onExit(_ctx, _nextState) {
|
|
188
|
+
this.childMachine?.stop();
|
|
189
|
+
this.childMachine = null;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
193
|
+
0 && (module.exports = {
|
|
194
|
+
BaseState,
|
|
195
|
+
HierarchicalState,
|
|
196
|
+
MachineNotStartedError,
|
|
197
|
+
StateEventEmitter,
|
|
198
|
+
StateMachine,
|
|
199
|
+
StateNotFoundError,
|
|
200
|
+
TransitionDeniedError
|
|
201
|
+
});
|
|
202
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/index.ts","../src/lib/State.ts","../src/lib/StateEvent.ts","../src/lib/errors.ts","../src/lib/StateMachine.ts","../src/lib/HierarchicalState.ts"],"sourcesContent":["export { BaseState } from \"./State.js\";\nexport { StateMachine } from \"./StateMachine.js\";\nexport { StateEventEmitter } from \"./StateEvent.js\";\nexport { HierarchicalState } from \"./HierarchicalState.js\";\nexport {\n StateNotFoundError,\n MachineNotStartedError,\n TransitionDeniedError,\n} from \"./errors.js\";\nexport type {\n IState,\n IStateMachine,\n StateChangeEvent,\n StateMachineConfig,\n} from \"./interfaces.js\";\nexport type { StateEventListener } from \"./StateEvent.js\";\n","import type { IState } from \"./interfaces.js\";\n\n/**\n * Abstract base class providing default no-op lifecycle hooks.\n * Concrete states override only what they need.\n *\n * All hooks are synchronous. Model async work as its own state\n * that polls for completion in onUpdate.\n */\nexport abstract class BaseState<TContext, TStateId extends string>\n implements IState<TContext, TStateId>\n{\n abstract readonly id: TStateId;\n\n canTransitionTo(_targetState: TStateId, _ctx: TContext): boolean {\n return true;\n }\n\n onEnter(_ctx: TContext, _prevState: TStateId | null): void {\n // no-op\n }\n\n onUpdate(_ctx: TContext, _dt: number): TStateId | undefined {\n return undefined;\n }\n\n onExit(_ctx: TContext, _nextState: TStateId | null): void {\n // no-op\n }\n}\n","import type { StateChangeEvent } from \"./interfaces.js\";\n\nexport type StateEventListener<TStateId extends string> = (\n event: StateChangeEvent<TStateId>,\n) => void;\n\n/**\n * Typed event emitter for state changes with bounded history.\n */\nexport class StateEventEmitter<TStateId extends string> {\n private readonly listeners: StateEventListener<TStateId>[] = [];\n private readonly history: StateChangeEvent<TStateId>[] = [];\n private readonly maxHistory: number;\n\n constructor(maxHistory: number = 100) {\n this.maxHistory = maxHistory;\n }\n\n on(listener: StateEventListener<TStateId>): () => void {\n this.listeners.push(listener);\n return () => {\n const idx = this.listeners.indexOf(listener);\n if (idx !== -1) this.listeners.splice(idx, 1);\n };\n }\n\n emit(change: StateChangeEvent<TStateId>): void {\n this.history.push(change);\n if (this.history.length > this.maxHistory) {\n this.history.shift();\n }\n for (const listener of this.listeners) {\n listener(change);\n }\n }\n\n getHistory(): ReadonlyArray<StateChangeEvent<TStateId>> {\n return [...this.history];\n }\n\n clear(): void {\n this.history.length = 0;\n this.listeners.length = 0;\n }\n}\n","export class StateNotFoundError extends Error {\n constructor(stateId: string) {\n super(`State not found: \"${stateId}\"`);\n this.name = \"StateNotFoundError\";\n }\n}\n\nexport class MachineNotStartedError extends Error {\n constructor() {\n super(\"State machine has not been started. Call start() first.\");\n this.name = \"MachineNotStartedError\";\n }\n}\n\nexport class TransitionDeniedError extends Error {\n constructor(from: string, to: string) {\n super(`Transition denied: \"${from}\" -> \"${to}\"`);\n this.name = \"TransitionDeniedError\";\n }\n}\n","import type {\n IState,\n IStateMachine,\n StateMachineConfig,\n StateChangeEvent,\n} from \"./interfaces.js\";\nimport { StateEventEmitter, type StateEventListener } from \"./StateEvent.js\";\nimport {\n StateNotFoundError,\n MachineNotStartedError,\n TransitionDeniedError,\n} from \"./errors.js\";\n\nexport class StateMachine<TContext, TStateId extends string>\n implements IStateMachine<TContext, TStateId>\n{\n private readonly stateMap = new Map<TStateId, IState<TContext, TStateId>>();\n private readonly emitter: StateEventEmitter<TStateId>;\n private currentState: IState<TContext, TStateId> | null = null;\n private readonly initialStateId: TStateId;\n private _isStarted = false;\n\n readonly context: TContext;\n\n constructor(config: StateMachineConfig<TContext, TStateId>) {\n this.context = config.context;\n this.initialStateId = config.initialState;\n this.emitter = new StateEventEmitter(config.historySize ?? 100);\n\n for (const state of config.states) {\n if (this.stateMap.has(state.id)) {\n throw new Error(`Duplicate state id: \"${state.id}\"`);\n }\n this.stateMap.set(state.id, state);\n }\n\n if (!this.stateMap.has(config.initialState)) {\n throw new StateNotFoundError(config.initialState);\n }\n }\n\n get currentStateId(): TStateId {\n if (!this.currentState) throw new MachineNotStartedError();\n return this.currentState.id;\n }\n\n get isStarted(): boolean {\n return this._isStarted;\n }\n\n start(): void {\n if (this._isStarted) return;\n const state = this.stateMap.get(this.initialStateId);\n if (!state) throw new StateNotFoundError(this.initialStateId);\n this.currentState = state;\n this._isStarted = true;\n this.currentState.onEnter(this.context, null);\n }\n\n stop(): void {\n if (!this._isStarted || !this.currentState) return;\n this.currentState.onExit(this.context, null);\n this.currentState = null;\n this._isStarted = false;\n }\n\n transitionTo(stateId: TStateId): void {\n if (!this.currentState) throw new MachineNotStartedError();\n const to = this.stateMap.get(stateId);\n if (!to) throw new StateNotFoundError(stateId);\n\n const from = this.currentState;\n if (!from.canTransitionTo(stateId, this.context)) {\n throw new TransitionDeniedError(from.id, stateId);\n }\n\n const change: StateChangeEvent<TStateId> = {\n from: from.id,\n to: to.id,\n timestamp: Date.now(),\n };\n\n from.onExit(this.context, to.id);\n this.currentState = to;\n this.emitter.emit(change);\n to.onEnter(this.context, from.id);\n }\n\n update(dt: number): void {\n if (!this.currentState) throw new MachineNotStartedError();\n const next = this.currentState.onUpdate(this.context, dt);\n if (next) {\n this.transitionTo(next);\n }\n }\n\n on(listener: StateEventListener<TStateId>): () => void {\n return this.emitter.on(listener);\n }\n\n getHistory(): ReadonlyArray<StateChangeEvent<TStateId>> {\n return this.emitter.getHistory();\n }\n}\n","import { BaseState } from \"./State.js\";\nimport { StateMachine } from \"./StateMachine.js\";\nimport type { StateMachineConfig } from \"./interfaces.js\";\n\n/**\n * A composite state that contains a nested StateMachine.\n * When this state is entered, the nested machine starts.\n * When this state is exited, the nested machine stops.\n * On update, the nested machine is updated.\n */\nexport abstract class HierarchicalState<\n TContext,\n TStateId extends string,\n TChildStateId extends string,\n> extends BaseState<TContext, TStateId> {\n protected childMachine: StateMachine<TContext, TChildStateId> | null = null;\n\n protected abstract createChildConfig(\n ctx: TContext,\n ): StateMachineConfig<TContext, TChildStateId>;\n\n override onEnter(ctx: TContext, _prevState: TStateId | null): void {\n const config = this.createChildConfig(ctx);\n this.childMachine = new StateMachine(config);\n this.childMachine.start();\n }\n\n override onUpdate(_ctx: TContext, _dt: number): TStateId | undefined {\n // Child machine update must be called separately by the consumer\n // or override this method to call this.childMachine.update(dt).\n return undefined;\n }\n\n override onExit(_ctx: TContext, _nextState: TStateId | null): void {\n this.childMachine?.stop();\n this.childMachine = null;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single state in the machine.
|
|
3
|
+
* TContext is shared mutable data, TStateId identifies states.
|
|
4
|
+
*
|
|
5
|
+
* All lifecycle hooks are synchronous. Model async work as its own
|
|
6
|
+
* state that polls for completion in onUpdate.
|
|
7
|
+
*/
|
|
8
|
+
interface IState<TContext, TStateId extends string> {
|
|
9
|
+
readonly id: TStateId;
|
|
10
|
+
canTransitionTo(targetState: TStateId, ctx: TContext): boolean;
|
|
11
|
+
onEnter(ctx: TContext, prevState: TStateId | null): void;
|
|
12
|
+
onUpdate(ctx: TContext, dt: number): TStateId | undefined;
|
|
13
|
+
onExit(ctx: TContext, nextState: TStateId | null): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Recorded state change for history / debugging.
|
|
17
|
+
*/
|
|
18
|
+
interface StateChangeEvent<TStateId extends string> {
|
|
19
|
+
from: TStateId;
|
|
20
|
+
to: TStateId;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Configuration to construct a StateMachine.
|
|
25
|
+
*/
|
|
26
|
+
interface StateMachineConfig<TContext, TStateId extends string> {
|
|
27
|
+
states: IState<TContext, TStateId>[];
|
|
28
|
+
initialState: TStateId;
|
|
29
|
+
context: TContext;
|
|
30
|
+
historySize?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Public interface for a state machine.
|
|
34
|
+
*/
|
|
35
|
+
interface IStateMachine<TContext, TStateId extends string> {
|
|
36
|
+
readonly currentStateId: TStateId;
|
|
37
|
+
readonly context: TContext;
|
|
38
|
+
readonly isStarted: boolean;
|
|
39
|
+
start(): void;
|
|
40
|
+
stop(): void;
|
|
41
|
+
transitionTo(stateId: TStateId): void;
|
|
42
|
+
update(dt: number): void;
|
|
43
|
+
getHistory(): ReadonlyArray<StateChangeEvent<TStateId>>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Abstract base class providing default no-op lifecycle hooks.
|
|
48
|
+
* Concrete states override only what they need.
|
|
49
|
+
*
|
|
50
|
+
* All hooks are synchronous. Model async work as its own state
|
|
51
|
+
* that polls for completion in onUpdate.
|
|
52
|
+
*/
|
|
53
|
+
declare abstract class BaseState<TContext, TStateId extends string> implements IState<TContext, TStateId> {
|
|
54
|
+
abstract readonly id: TStateId;
|
|
55
|
+
canTransitionTo(_targetState: TStateId, _ctx: TContext): boolean;
|
|
56
|
+
onEnter(_ctx: TContext, _prevState: TStateId | null): void;
|
|
57
|
+
onUpdate(_ctx: TContext, _dt: number): TStateId | undefined;
|
|
58
|
+
onExit(_ctx: TContext, _nextState: TStateId | null): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type StateEventListener<TStateId extends string> = (event: StateChangeEvent<TStateId>) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Typed event emitter for state changes with bounded history.
|
|
64
|
+
*/
|
|
65
|
+
declare class StateEventEmitter<TStateId extends string> {
|
|
66
|
+
private readonly listeners;
|
|
67
|
+
private readonly history;
|
|
68
|
+
private readonly maxHistory;
|
|
69
|
+
constructor(maxHistory?: number);
|
|
70
|
+
on(listener: StateEventListener<TStateId>): () => void;
|
|
71
|
+
emit(change: StateChangeEvent<TStateId>): void;
|
|
72
|
+
getHistory(): ReadonlyArray<StateChangeEvent<TStateId>>;
|
|
73
|
+
clear(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare class StateMachine<TContext, TStateId extends string> implements IStateMachine<TContext, TStateId> {
|
|
77
|
+
private readonly stateMap;
|
|
78
|
+
private readonly emitter;
|
|
79
|
+
private currentState;
|
|
80
|
+
private readonly initialStateId;
|
|
81
|
+
private _isStarted;
|
|
82
|
+
readonly context: TContext;
|
|
83
|
+
constructor(config: StateMachineConfig<TContext, TStateId>);
|
|
84
|
+
get currentStateId(): TStateId;
|
|
85
|
+
get isStarted(): boolean;
|
|
86
|
+
start(): void;
|
|
87
|
+
stop(): void;
|
|
88
|
+
transitionTo(stateId: TStateId): void;
|
|
89
|
+
update(dt: number): void;
|
|
90
|
+
on(listener: StateEventListener<TStateId>): () => void;
|
|
91
|
+
getHistory(): ReadonlyArray<StateChangeEvent<TStateId>>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A composite state that contains a nested StateMachine.
|
|
96
|
+
* When this state is entered, the nested machine starts.
|
|
97
|
+
* When this state is exited, the nested machine stops.
|
|
98
|
+
* On update, the nested machine is updated.
|
|
99
|
+
*/
|
|
100
|
+
declare abstract class HierarchicalState<TContext, TStateId extends string, TChildStateId extends string> extends BaseState<TContext, TStateId> {
|
|
101
|
+
protected childMachine: StateMachine<TContext, TChildStateId> | null;
|
|
102
|
+
protected abstract createChildConfig(ctx: TContext): StateMachineConfig<TContext, TChildStateId>;
|
|
103
|
+
onEnter(ctx: TContext, _prevState: TStateId | null): void;
|
|
104
|
+
onUpdate(_ctx: TContext, _dt: number): TStateId | undefined;
|
|
105
|
+
onExit(_ctx: TContext, _nextState: TStateId | null): void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
declare class StateNotFoundError extends Error {
|
|
109
|
+
constructor(stateId: string);
|
|
110
|
+
}
|
|
111
|
+
declare class MachineNotStartedError extends Error {
|
|
112
|
+
constructor();
|
|
113
|
+
}
|
|
114
|
+
declare class TransitionDeniedError extends Error {
|
|
115
|
+
constructor(from: string, to: string);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { BaseState, HierarchicalState, type IState, type IStateMachine, MachineNotStartedError, type StateChangeEvent, StateEventEmitter, type StateEventListener, StateMachine, type StateMachineConfig, StateNotFoundError, TransitionDeniedError };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single state in the machine.
|
|
3
|
+
* TContext is shared mutable data, TStateId identifies states.
|
|
4
|
+
*
|
|
5
|
+
* All lifecycle hooks are synchronous. Model async work as its own
|
|
6
|
+
* state that polls for completion in onUpdate.
|
|
7
|
+
*/
|
|
8
|
+
interface IState<TContext, TStateId extends string> {
|
|
9
|
+
readonly id: TStateId;
|
|
10
|
+
canTransitionTo(targetState: TStateId, ctx: TContext): boolean;
|
|
11
|
+
onEnter(ctx: TContext, prevState: TStateId | null): void;
|
|
12
|
+
onUpdate(ctx: TContext, dt: number): TStateId | undefined;
|
|
13
|
+
onExit(ctx: TContext, nextState: TStateId | null): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Recorded state change for history / debugging.
|
|
17
|
+
*/
|
|
18
|
+
interface StateChangeEvent<TStateId extends string> {
|
|
19
|
+
from: TStateId;
|
|
20
|
+
to: TStateId;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Configuration to construct a StateMachine.
|
|
25
|
+
*/
|
|
26
|
+
interface StateMachineConfig<TContext, TStateId extends string> {
|
|
27
|
+
states: IState<TContext, TStateId>[];
|
|
28
|
+
initialState: TStateId;
|
|
29
|
+
context: TContext;
|
|
30
|
+
historySize?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Public interface for a state machine.
|
|
34
|
+
*/
|
|
35
|
+
interface IStateMachine<TContext, TStateId extends string> {
|
|
36
|
+
readonly currentStateId: TStateId;
|
|
37
|
+
readonly context: TContext;
|
|
38
|
+
readonly isStarted: boolean;
|
|
39
|
+
start(): void;
|
|
40
|
+
stop(): void;
|
|
41
|
+
transitionTo(stateId: TStateId): void;
|
|
42
|
+
update(dt: number): void;
|
|
43
|
+
getHistory(): ReadonlyArray<StateChangeEvent<TStateId>>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Abstract base class providing default no-op lifecycle hooks.
|
|
48
|
+
* Concrete states override only what they need.
|
|
49
|
+
*
|
|
50
|
+
* All hooks are synchronous. Model async work as its own state
|
|
51
|
+
* that polls for completion in onUpdate.
|
|
52
|
+
*/
|
|
53
|
+
declare abstract class BaseState<TContext, TStateId extends string> implements IState<TContext, TStateId> {
|
|
54
|
+
abstract readonly id: TStateId;
|
|
55
|
+
canTransitionTo(_targetState: TStateId, _ctx: TContext): boolean;
|
|
56
|
+
onEnter(_ctx: TContext, _prevState: TStateId | null): void;
|
|
57
|
+
onUpdate(_ctx: TContext, _dt: number): TStateId | undefined;
|
|
58
|
+
onExit(_ctx: TContext, _nextState: TStateId | null): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type StateEventListener<TStateId extends string> = (event: StateChangeEvent<TStateId>) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Typed event emitter for state changes with bounded history.
|
|
64
|
+
*/
|
|
65
|
+
declare class StateEventEmitter<TStateId extends string> {
|
|
66
|
+
private readonly listeners;
|
|
67
|
+
private readonly history;
|
|
68
|
+
private readonly maxHistory;
|
|
69
|
+
constructor(maxHistory?: number);
|
|
70
|
+
on(listener: StateEventListener<TStateId>): () => void;
|
|
71
|
+
emit(change: StateChangeEvent<TStateId>): void;
|
|
72
|
+
getHistory(): ReadonlyArray<StateChangeEvent<TStateId>>;
|
|
73
|
+
clear(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare class StateMachine<TContext, TStateId extends string> implements IStateMachine<TContext, TStateId> {
|
|
77
|
+
private readonly stateMap;
|
|
78
|
+
private readonly emitter;
|
|
79
|
+
private currentState;
|
|
80
|
+
private readonly initialStateId;
|
|
81
|
+
private _isStarted;
|
|
82
|
+
readonly context: TContext;
|
|
83
|
+
constructor(config: StateMachineConfig<TContext, TStateId>);
|
|
84
|
+
get currentStateId(): TStateId;
|
|
85
|
+
get isStarted(): boolean;
|
|
86
|
+
start(): void;
|
|
87
|
+
stop(): void;
|
|
88
|
+
transitionTo(stateId: TStateId): void;
|
|
89
|
+
update(dt: number): void;
|
|
90
|
+
on(listener: StateEventListener<TStateId>): () => void;
|
|
91
|
+
getHistory(): ReadonlyArray<StateChangeEvent<TStateId>>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A composite state that contains a nested StateMachine.
|
|
96
|
+
* When this state is entered, the nested machine starts.
|
|
97
|
+
* When this state is exited, the nested machine stops.
|
|
98
|
+
* On update, the nested machine is updated.
|
|
99
|
+
*/
|
|
100
|
+
declare abstract class HierarchicalState<TContext, TStateId extends string, TChildStateId extends string> extends BaseState<TContext, TStateId> {
|
|
101
|
+
protected childMachine: StateMachine<TContext, TChildStateId> | null;
|
|
102
|
+
protected abstract createChildConfig(ctx: TContext): StateMachineConfig<TContext, TChildStateId>;
|
|
103
|
+
onEnter(ctx: TContext, _prevState: TStateId | null): void;
|
|
104
|
+
onUpdate(_ctx: TContext, _dt: number): TStateId | undefined;
|
|
105
|
+
onExit(_ctx: TContext, _nextState: TStateId | null): void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
declare class StateNotFoundError extends Error {
|
|
109
|
+
constructor(stateId: string);
|
|
110
|
+
}
|
|
111
|
+
declare class MachineNotStartedError extends Error {
|
|
112
|
+
constructor();
|
|
113
|
+
}
|
|
114
|
+
declare class TransitionDeniedError extends Error {
|
|
115
|
+
constructor(from: string, to: string);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { BaseState, HierarchicalState, type IState, type IStateMachine, MachineNotStartedError, type StateChangeEvent, StateEventEmitter, type StateEventListener, StateMachine, type StateMachineConfig, StateNotFoundError, TransitionDeniedError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/lib/State.ts
|
|
2
|
+
var BaseState = class {
|
|
3
|
+
canTransitionTo(_targetState, _ctx) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
onEnter(_ctx, _prevState) {
|
|
7
|
+
}
|
|
8
|
+
onUpdate(_ctx, _dt) {
|
|
9
|
+
return void 0;
|
|
10
|
+
}
|
|
11
|
+
onExit(_ctx, _nextState) {
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/lib/StateEvent.ts
|
|
16
|
+
var StateEventEmitter = class {
|
|
17
|
+
listeners = [];
|
|
18
|
+
history = [];
|
|
19
|
+
maxHistory;
|
|
20
|
+
constructor(maxHistory = 100) {
|
|
21
|
+
this.maxHistory = maxHistory;
|
|
22
|
+
}
|
|
23
|
+
on(listener) {
|
|
24
|
+
this.listeners.push(listener);
|
|
25
|
+
return () => {
|
|
26
|
+
const idx = this.listeners.indexOf(listener);
|
|
27
|
+
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
emit(change) {
|
|
31
|
+
this.history.push(change);
|
|
32
|
+
if (this.history.length > this.maxHistory) {
|
|
33
|
+
this.history.shift();
|
|
34
|
+
}
|
|
35
|
+
for (const listener of this.listeners) {
|
|
36
|
+
listener(change);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
getHistory() {
|
|
40
|
+
return [...this.history];
|
|
41
|
+
}
|
|
42
|
+
clear() {
|
|
43
|
+
this.history.length = 0;
|
|
44
|
+
this.listeners.length = 0;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/lib/errors.ts
|
|
49
|
+
var StateNotFoundError = class extends Error {
|
|
50
|
+
constructor(stateId) {
|
|
51
|
+
super(`State not found: "${stateId}"`);
|
|
52
|
+
this.name = "StateNotFoundError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var MachineNotStartedError = class extends Error {
|
|
56
|
+
constructor() {
|
|
57
|
+
super("State machine has not been started. Call start() first.");
|
|
58
|
+
this.name = "MachineNotStartedError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var TransitionDeniedError = class extends Error {
|
|
62
|
+
constructor(from, to) {
|
|
63
|
+
super(`Transition denied: "${from}" -> "${to}"`);
|
|
64
|
+
this.name = "TransitionDeniedError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/lib/StateMachine.ts
|
|
69
|
+
var StateMachine = class {
|
|
70
|
+
stateMap = /* @__PURE__ */ new Map();
|
|
71
|
+
emitter;
|
|
72
|
+
currentState = null;
|
|
73
|
+
initialStateId;
|
|
74
|
+
_isStarted = false;
|
|
75
|
+
context;
|
|
76
|
+
constructor(config) {
|
|
77
|
+
this.context = config.context;
|
|
78
|
+
this.initialStateId = config.initialState;
|
|
79
|
+
this.emitter = new StateEventEmitter(config.historySize ?? 100);
|
|
80
|
+
for (const state of config.states) {
|
|
81
|
+
if (this.stateMap.has(state.id)) {
|
|
82
|
+
throw new Error(`Duplicate state id: "${state.id}"`);
|
|
83
|
+
}
|
|
84
|
+
this.stateMap.set(state.id, state);
|
|
85
|
+
}
|
|
86
|
+
if (!this.stateMap.has(config.initialState)) {
|
|
87
|
+
throw new StateNotFoundError(config.initialState);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
get currentStateId() {
|
|
91
|
+
if (!this.currentState) throw new MachineNotStartedError();
|
|
92
|
+
return this.currentState.id;
|
|
93
|
+
}
|
|
94
|
+
get isStarted() {
|
|
95
|
+
return this._isStarted;
|
|
96
|
+
}
|
|
97
|
+
start() {
|
|
98
|
+
if (this._isStarted) return;
|
|
99
|
+
const state = this.stateMap.get(this.initialStateId);
|
|
100
|
+
if (!state) throw new StateNotFoundError(this.initialStateId);
|
|
101
|
+
this.currentState = state;
|
|
102
|
+
this._isStarted = true;
|
|
103
|
+
this.currentState.onEnter(this.context, null);
|
|
104
|
+
}
|
|
105
|
+
stop() {
|
|
106
|
+
if (!this._isStarted || !this.currentState) return;
|
|
107
|
+
this.currentState.onExit(this.context, null);
|
|
108
|
+
this.currentState = null;
|
|
109
|
+
this._isStarted = false;
|
|
110
|
+
}
|
|
111
|
+
transitionTo(stateId) {
|
|
112
|
+
if (!this.currentState) throw new MachineNotStartedError();
|
|
113
|
+
const to = this.stateMap.get(stateId);
|
|
114
|
+
if (!to) throw new StateNotFoundError(stateId);
|
|
115
|
+
const from = this.currentState;
|
|
116
|
+
if (!from.canTransitionTo(stateId, this.context)) {
|
|
117
|
+
throw new TransitionDeniedError(from.id, stateId);
|
|
118
|
+
}
|
|
119
|
+
const change = {
|
|
120
|
+
from: from.id,
|
|
121
|
+
to: to.id,
|
|
122
|
+
timestamp: Date.now()
|
|
123
|
+
};
|
|
124
|
+
from.onExit(this.context, to.id);
|
|
125
|
+
this.currentState = to;
|
|
126
|
+
this.emitter.emit(change);
|
|
127
|
+
to.onEnter(this.context, from.id);
|
|
128
|
+
}
|
|
129
|
+
update(dt) {
|
|
130
|
+
if (!this.currentState) throw new MachineNotStartedError();
|
|
131
|
+
const next = this.currentState.onUpdate(this.context, dt);
|
|
132
|
+
if (next) {
|
|
133
|
+
this.transitionTo(next);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
on(listener) {
|
|
137
|
+
return this.emitter.on(listener);
|
|
138
|
+
}
|
|
139
|
+
getHistory() {
|
|
140
|
+
return this.emitter.getHistory();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/lib/HierarchicalState.ts
|
|
145
|
+
var HierarchicalState = class extends BaseState {
|
|
146
|
+
childMachine = null;
|
|
147
|
+
onEnter(ctx, _prevState) {
|
|
148
|
+
const config = this.createChildConfig(ctx);
|
|
149
|
+
this.childMachine = new StateMachine(config);
|
|
150
|
+
this.childMachine.start();
|
|
151
|
+
}
|
|
152
|
+
onUpdate(_ctx, _dt) {
|
|
153
|
+
return void 0;
|
|
154
|
+
}
|
|
155
|
+
onExit(_ctx, _nextState) {
|
|
156
|
+
this.childMachine?.stop();
|
|
157
|
+
this.childMachine = null;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
export {
|
|
161
|
+
BaseState,
|
|
162
|
+
HierarchicalState,
|
|
163
|
+
MachineNotStartedError,
|
|
164
|
+
StateEventEmitter,
|
|
165
|
+
StateMachine,
|
|
166
|
+
StateNotFoundError,
|
|
167
|
+
TransitionDeniedError
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/State.ts","../src/lib/StateEvent.ts","../src/lib/errors.ts","../src/lib/StateMachine.ts","../src/lib/HierarchicalState.ts"],"sourcesContent":["import type { IState } from \"./interfaces.js\";\n\n/**\n * Abstract base class providing default no-op lifecycle hooks.\n * Concrete states override only what they need.\n *\n * All hooks are synchronous. Model async work as its own state\n * that polls for completion in onUpdate.\n */\nexport abstract class BaseState<TContext, TStateId extends string>\n implements IState<TContext, TStateId>\n{\n abstract readonly id: TStateId;\n\n canTransitionTo(_targetState: TStateId, _ctx: TContext): boolean {\n return true;\n }\n\n onEnter(_ctx: TContext, _prevState: TStateId | null): void {\n // no-op\n }\n\n onUpdate(_ctx: TContext, _dt: number): TStateId | undefined {\n return undefined;\n }\n\n onExit(_ctx: TContext, _nextState: TStateId | null): void {\n // no-op\n }\n}\n","import type { StateChangeEvent } from \"./interfaces.js\";\n\nexport type StateEventListener<TStateId extends string> = (\n event: StateChangeEvent<TStateId>,\n) => void;\n\n/**\n * Typed event emitter for state changes with bounded history.\n */\nexport class StateEventEmitter<TStateId extends string> {\n private readonly listeners: StateEventListener<TStateId>[] = [];\n private readonly history: StateChangeEvent<TStateId>[] = [];\n private readonly maxHistory: number;\n\n constructor(maxHistory: number = 100) {\n this.maxHistory = maxHistory;\n }\n\n on(listener: StateEventListener<TStateId>): () => void {\n this.listeners.push(listener);\n return () => {\n const idx = this.listeners.indexOf(listener);\n if (idx !== -1) this.listeners.splice(idx, 1);\n };\n }\n\n emit(change: StateChangeEvent<TStateId>): void {\n this.history.push(change);\n if (this.history.length > this.maxHistory) {\n this.history.shift();\n }\n for (const listener of this.listeners) {\n listener(change);\n }\n }\n\n getHistory(): ReadonlyArray<StateChangeEvent<TStateId>> {\n return [...this.history];\n }\n\n clear(): void {\n this.history.length = 0;\n this.listeners.length = 0;\n }\n}\n","export class StateNotFoundError extends Error {\n constructor(stateId: string) {\n super(`State not found: \"${stateId}\"`);\n this.name = \"StateNotFoundError\";\n }\n}\n\nexport class MachineNotStartedError extends Error {\n constructor() {\n super(\"State machine has not been started. Call start() first.\");\n this.name = \"MachineNotStartedError\";\n }\n}\n\nexport class TransitionDeniedError extends Error {\n constructor(from: string, to: string) {\n super(`Transition denied: \"${from}\" -> \"${to}\"`);\n this.name = \"TransitionDeniedError\";\n }\n}\n","import type {\n IState,\n IStateMachine,\n StateMachineConfig,\n StateChangeEvent,\n} from \"./interfaces.js\";\nimport { StateEventEmitter, type StateEventListener } from \"./StateEvent.js\";\nimport {\n StateNotFoundError,\n MachineNotStartedError,\n TransitionDeniedError,\n} from \"./errors.js\";\n\nexport class StateMachine<TContext, TStateId extends string>\n implements IStateMachine<TContext, TStateId>\n{\n private readonly stateMap = new Map<TStateId, IState<TContext, TStateId>>();\n private readonly emitter: StateEventEmitter<TStateId>;\n private currentState: IState<TContext, TStateId> | null = null;\n private readonly initialStateId: TStateId;\n private _isStarted = false;\n\n readonly context: TContext;\n\n constructor(config: StateMachineConfig<TContext, TStateId>) {\n this.context = config.context;\n this.initialStateId = config.initialState;\n this.emitter = new StateEventEmitter(config.historySize ?? 100);\n\n for (const state of config.states) {\n if (this.stateMap.has(state.id)) {\n throw new Error(`Duplicate state id: \"${state.id}\"`);\n }\n this.stateMap.set(state.id, state);\n }\n\n if (!this.stateMap.has(config.initialState)) {\n throw new StateNotFoundError(config.initialState);\n }\n }\n\n get currentStateId(): TStateId {\n if (!this.currentState) throw new MachineNotStartedError();\n return this.currentState.id;\n }\n\n get isStarted(): boolean {\n return this._isStarted;\n }\n\n start(): void {\n if (this._isStarted) return;\n const state = this.stateMap.get(this.initialStateId);\n if (!state) throw new StateNotFoundError(this.initialStateId);\n this.currentState = state;\n this._isStarted = true;\n this.currentState.onEnter(this.context, null);\n }\n\n stop(): void {\n if (!this._isStarted || !this.currentState) return;\n this.currentState.onExit(this.context, null);\n this.currentState = null;\n this._isStarted = false;\n }\n\n transitionTo(stateId: TStateId): void {\n if (!this.currentState) throw new MachineNotStartedError();\n const to = this.stateMap.get(stateId);\n if (!to) throw new StateNotFoundError(stateId);\n\n const from = this.currentState;\n if (!from.canTransitionTo(stateId, this.context)) {\n throw new TransitionDeniedError(from.id, stateId);\n }\n\n const change: StateChangeEvent<TStateId> = {\n from: from.id,\n to: to.id,\n timestamp: Date.now(),\n };\n\n from.onExit(this.context, to.id);\n this.currentState = to;\n this.emitter.emit(change);\n to.onEnter(this.context, from.id);\n }\n\n update(dt: number): void {\n if (!this.currentState) throw new MachineNotStartedError();\n const next = this.currentState.onUpdate(this.context, dt);\n if (next) {\n this.transitionTo(next);\n }\n }\n\n on(listener: StateEventListener<TStateId>): () => void {\n return this.emitter.on(listener);\n }\n\n getHistory(): ReadonlyArray<StateChangeEvent<TStateId>> {\n return this.emitter.getHistory();\n }\n}\n","import { BaseState } from \"./State.js\";\nimport { StateMachine } from \"./StateMachine.js\";\nimport type { StateMachineConfig } from \"./interfaces.js\";\n\n/**\n * A composite state that contains a nested StateMachine.\n * When this state is entered, the nested machine starts.\n * When this state is exited, the nested machine stops.\n * On update, the nested machine is updated.\n */\nexport abstract class HierarchicalState<\n TContext,\n TStateId extends string,\n TChildStateId extends string,\n> extends BaseState<TContext, TStateId> {\n protected childMachine: StateMachine<TContext, TChildStateId> | null = null;\n\n protected abstract createChildConfig(\n ctx: TContext,\n ): StateMachineConfig<TContext, TChildStateId>;\n\n override onEnter(ctx: TContext, _prevState: TStateId | null): void {\n const config = this.createChildConfig(ctx);\n this.childMachine = new StateMachine(config);\n this.childMachine.start();\n }\n\n override onUpdate(_ctx: TContext, _dt: number): TStateId | undefined {\n // Child machine update must be called separately by the consumer\n // or override this method to call this.childMachine.update(dt).\n return undefined;\n }\n\n override onExit(_ctx: TContext, _nextState: TStateId | null): void {\n this.childMachine?.stop();\n this.childMachine = null;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@duyquangnvx/state-machine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generic, type-safe finite state machine library for TypeScript",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"require": {
|
|
13
|
+
"types": "./dist/index.d.cts",
|
|
14
|
+
"default": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"files": ["dist"],
|
|
22
|
+
"publishConfig": { "access": "public" },
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"keywords": ["state-machine", "fsm", "typescript", "game", "state"],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"build:demo": "tsc",
|
|
28
|
+
"demo": "npm run build:demo && node dist/demo/main.js",
|
|
29
|
+
"demo:slot": "npm run build:demo && node dist/demo/slot-main.js",
|
|
30
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
31
|
+
"clean": "rimraf dist",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/jest": "^29.5.0",
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"jest": "^29.7.0",
|
|
38
|
+
"ts-jest": "^29.2.0",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"typescript": "^5.7.0"
|
|
41
|
+
}
|
|
42
|
+
}
|