@alwatr/fsm 9.26.0 → 9.29.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 +372 -212
- package/dist/facade.d.ts +6 -7
- package/dist/facade.d.ts.map +1 -1
- package/dist/fsm-service.d.ts +26 -6
- package/dist/fsm-service.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +4 -4
- package/dist/type.d.ts +40 -16
- package/dist/type.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/facade.ts +10 -9
- package/src/fsm-service.ts +141 -54
- package/src/type.ts +45 -27
package/README.md
CHANGED
|
@@ -1,306 +1,466 @@
|
|
|
1
|
-
# Alwatr FSM
|
|
1
|
+
# 🤖 Alwatr FSM
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://www.google.com/search?q=alwatr+fsm)
|
|
5
|
-
[](https://www.google.com/search?q=alwatr+signal)
|
|
6
|
-
[](https://www.google.com/search?q=alwatr)
|
|
7
|
-
[](https://www.npmjs.com/package/%40alwatr/flux)
|
|
8
|
-
[](https://www.npmjs.com/package/%40alwatr/fsm)
|
|
9
|
-
[](https://www.npmjs.com/package/%40alwatr/signal)
|
|
3
|
+
**Declarative, Reactive, and Type-Safe Statechart Engine for High-Performance Applications**
|
|
10
4
|
|
|
11
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@alwatr/fsm)
|
|
6
|
+
[](https://github.com/Alwatr/alwatr/blob/next/LICENSE)
|
|
12
7
|
|
|
13
|
-
|
|
8
|
+
> A tiny, production-grade, and highly reactive Finite State Machine (FSM) engine built on top of `@alwatr/signal`. It natively enforces Run-To-Completion (RTC) safety, state persistence, nested transitions, and dynamic state actors—allowing you to model complex user flows as deterministic, bulletproof statecharts.
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Managing state in complex applications can be challenging. As features grow, state transitions can become unpredictable, leading to bugs and difficult-to-maintain code. Finite State Machines provide a powerful model to solve this problem by formalizing application logic.
|
|
10
|
+
---
|
|
18
11
|
|
|
19
|
-
|
|
12
|
+
## 🎯 What is Alwatr FSM?
|
|
20
13
|
|
|
21
|
-
|
|
14
|
+
`@alwatr/fsm` is a type-safe, lightweight engine that replaces loose, error-prone boolean flags (`isLoading`, `isError`, etc.) with a **declarative, mathematically sound statechart**.
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
- **Type-Safe**: Leverage TypeScript to catch errors at compile time, not runtime.
|
|
25
|
-
- **Reactive**: Built around signals for seamless integration with modern UI frameworks and reactive codebases.
|
|
26
|
-
- **Resilient**: Guarantees Run-to-Completion (RTC) and handles errors in user-defined functions gracefully without crashing.
|
|
16
|
+
It serves as the core logic engine (the "Brain") for the **Actor Model** within unidirectional data flow architectures. By defining states, transitions, guards, and side-effects in a single structured configuration, you ensure your application can never enter an undefined or invalid state.
|
|
27
17
|
|
|
28
18
|
---
|
|
29
19
|
|
|
30
|
-
##
|
|
20
|
+
## 🏗️ Architecture & Actor Model Flow
|
|
31
21
|
|
|
32
|
-
|
|
22
|
+
Alwatr FSM acts as a self-contained Actor. It receives external messages via its private mailbox (the `dispatch` method), evaluates them atomically, mutates its internal context, triggers side-effects, and broadcasts state updates to the rest of the application.
|
|
33
23
|
|
|
34
|
-
|
|
24
|
+
```mermaid
|
|
25
|
+
graph TD
|
|
26
|
+
%% Styling
|
|
27
|
+
classDef signal fill:#E1F5FE,stroke:#03A9F4,stroke-width:2px,color:#01579B;
|
|
28
|
+
classDef core fill:#E8F5E9,stroke:#4CAF50,stroke-width:2px,color:#1B5E20;
|
|
29
|
+
classDef side fill:#FFF3E0,stroke:#FF9800,stroke-width:2px,color:#E65100;
|
|
30
|
+
classDef actor fill:#F3E5F5,stroke:#9C27B0,stroke-width:2px,color:#4A148C;
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
Mailbox[dispatch Event] -->|Inbound Queue| Queue[Run-to-Completion Loop]
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
subgraph Engine ["FsmService (Atomic Evaluation)"]
|
|
35
|
+
Queue -->|Evaluate Guard Predicates| Guards{Guard Met?}
|
|
36
|
+
Guards -->|Yes| Transition[Apply Transition]
|
|
37
|
+
Guards -->|No| Ignored[Ignore Event]
|
|
42
38
|
|
|
43
|
-
|
|
39
|
+
Transition -->|1. Run Exit Effects| Exit[State Exit Effects]
|
|
40
|
+
Transition -->|2. Pure Assigner Mutation| Context[(Compute Next Context)]
|
|
41
|
+
Transition -->|3. Run Entry Effects| Entry[State Entry Effects]
|
|
42
|
+
Transition -->|4. Update stateSignal| Sig[State Signal Update]
|
|
43
|
+
Transition -->|5. Spawn State Actors| Spawn[State Actors Active]
|
|
44
|
+
end
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
Sig -->|Reactivity| UI[View Layer / Subscribers]
|
|
47
|
+
Spawn -->|Async Actions| Mailbox
|
|
48
|
+
|
|
49
|
+
class Sig,UI signal;
|
|
50
|
+
class Queue,Guards,Transition,Context core;
|
|
51
|
+
class Exit,Entry side;
|
|
52
|
+
class Spawn actor;
|
|
47
53
|
```
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
---
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
| :------------- | :------------------------------------------------------------------------------------------------------------------ |
|
|
53
|
-
| **State** | The current finite state of the machine (e.g., `'idle'`, `'loading'`). |
|
|
54
|
-
| **Context** | An object holding the "extended state"—any quantitative or non-finite data (e.g., `{retries: 2, data: null}`). |
|
|
55
|
-
| **Event** | An object that triggers a potential state transition (e.g., `{type: 'FETCH', id: '123'}`). |
|
|
56
|
-
| **Transition** | A rule defining the path from a source state to a target state for a given event. It can be guarded by a condition. |
|
|
57
|
-
| **Assigner** | A **pure function** that synchronously updates the `context` during a transition. |
|
|
58
|
-
| **Effect** | A function for **side effects** (e.g., API calls, logging) that runs upon entering or exiting a state. |
|
|
59
|
-
| **Condition** | A **predicate function** that must return `true` for its associated transition to be taken. |
|
|
57
|
+
## 🛡️ Core Philosophy
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
| **رویداد (Event)** | آبجکتی که یک گذار وضعیت بالقوه را آغاز میکند (مثلاً `{type: 'FETCH', id: '123'}`). |
|
|
66
|
-
| **گذار (Transition)** | قانونی که مسیر از یک وضعیت مبدأ به یک وضعیت مقصد را برای یک رویداد خاص تعریف میکند. این گذار میتواند توسط یک شرط محافظت شود. |
|
|
67
|
-
| **تخصیصدهنده (Assigner)** | یک **تابع خالص** که به صورت همزمان (synchronously) `context` را در طول یک گذار بهروزرسانی میکند. |
|
|
68
|
-
| **اثر جانبی (Effect)** | تابعی برای **عملیاتهای جانبی** (مانند فراخوانی API یا لاگ کردن) که هنگام ورود یا خروج از یک وضعیت اجرا میشود. |
|
|
69
|
-
| **شرط (Condition)** | یک **تابع گزارهای** که باید `true` برگرداند تا گذار مرتبط با آن انجام شود. |
|
|
59
|
+
- **Declarative Topology**: Describe your entire state machine logic in a single configuration matrix. Avoid nested `if/else` checks and scattered state variables.
|
|
60
|
+
- **Run-To-Completion (RTC)**: Transitions are evaluated synchronously in microtasks. Concurrent inputs are queued and evaluated sequentially to prevent state-sync bugs and race conditions.
|
|
61
|
+
- **Deep Compile-Time Safety**: Leverage TypeScript's advanced type inference to validate event payloads, contextual mutations, and conditional transitions at build time.
|
|
62
|
+
- **Resilient & Guarded Boundaries**: Assigner, guard, and effect closures run inside try/catch blocks. If a user function throws an error, the machine catches it, logs a diagnostic accident, and safely reverts context updates.
|
|
70
63
|
|
|
71
|
-
|
|
64
|
+
---
|
|
72
65
|
|
|
73
|
-
|
|
66
|
+
## 🧠 Glossary
|
|
74
67
|
|
|
75
|
-
|
|
68
|
+
| Term | Domain Scope | Description |
|
|
69
|
+
| :------------- | :-------------- | :-------------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| **State** | Finite | A discrete behavioral mode of the system (e.g., `'idle'`, `'uploading'`). |
|
|
71
|
+
| **Context** | Infinite | The extended quantitative state holding quantitative data (e.g., `{ retries: 0 }`). |
|
|
72
|
+
| **Event** | Message | An object with a unique `type` dispatched to trigger state machine evaluation. |
|
|
73
|
+
| **Transition** | Structural Rule | A predefined path establishing how the machine moves from state A to B on receiving an event. |
|
|
74
|
+
| **Assigner** | Pure Mutation | A synchronous, deterministic function that updates a slice of the machine's extended `context`. |
|
|
75
|
+
| **Effect** | Side-Effect | A fire-and-forget synchronous or asynchronous side-effect run on entering or leaving a state. |
|
|
76
|
+
| **Actor** | Spawned Process | An async lifecycle process spawned on state entry that can dispatch events back and runs cleanup on exit. |
|
|
77
|
+
| **Guard** | Gatekeeper | A boolean evaluator that must return `true` for a transition branch to be authorized. |
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
ابتدا انواع مربوط به وضعیتها، رویدادها و زمینه را تعریف میکنیم.
|
|
79
|
+
---
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
## 📦 Installation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# npm
|
|
85
|
+
npm install @alwatr/fsm
|
|
86
|
+
|
|
87
|
+
# bun
|
|
88
|
+
bun add @alwatr/fsm
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🚀 Quick Start Blueprint
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
type LightContext = {brightness: number};
|
|
95
|
+
### 1. Define Typed Schemas
|
|
85
96
|
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
```typescript
|
|
98
|
+
// types.ts
|
|
99
|
+
import type {MachineEvent} from '@alwatr/fsm';
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
export type FileState = 'idle' | 'uploading' | 'success' | 'failed';
|
|
102
|
+
|
|
103
|
+
export interface FileContext {
|
|
104
|
+
fileId: string | null;
|
|
105
|
+
progress: number;
|
|
106
|
+
errorMessage: string | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type FileEvent =
|
|
110
|
+
| {type: 'START_UPLOAD'; fileId: string}
|
|
111
|
+
| {type: 'PROGRESS_UPDATE'; percent: number}
|
|
112
|
+
| {type: 'UPLOAD_SUCCESS'}
|
|
113
|
+
| {type: 'UPLOAD_FAILURE'; error: string}
|
|
114
|
+
| {type: 'RETRY'};
|
|
91
115
|
```
|
|
92
116
|
|
|
93
|
-
###
|
|
117
|
+
### 2. Configure the State Machine
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
```typescript
|
|
120
|
+
// config.ts
|
|
121
|
+
import type {StateMachineConfig} from '@alwatr/fsm';
|
|
122
|
+
import type {FileState, FileEvent, FileContext} from './types.js';
|
|
97
123
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
context: {brightness: 0},
|
|
124
|
+
export const fileUploadConfig: StateMachineConfig<FileState, FileEvent, FileContext> = {
|
|
125
|
+
name: 'file-upload-lifecycle',
|
|
126
|
+
initial: 'idle',
|
|
127
|
+
context: {fileId: null, progress: 0, errorMessage: null},
|
|
103
128
|
states: {
|
|
104
|
-
|
|
129
|
+
idle: {
|
|
105
130
|
on: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
assigners: [() => ({
|
|
131
|
+
START_UPLOAD: {
|
|
132
|
+
target: 'uploading',
|
|
133
|
+
// Pure assigner updates context slice
|
|
134
|
+
assigners: [({context, event}) => ({...context, fileId: event.fileId, progress: 0, errorMessage: null})],
|
|
110
135
|
},
|
|
111
136
|
},
|
|
112
137
|
},
|
|
113
|
-
|
|
138
|
+
uploading: {
|
|
114
139
|
on: {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
140
|
+
PROGRESS_UPDATE: {
|
|
141
|
+
// No 'target' means an internal transition: context changes, state remains 'uploading'
|
|
142
|
+
assigners: [({context, event}) => ({...context, progress: event.percent})],
|
|
143
|
+
},
|
|
144
|
+
UPLOAD_SUCCESS: {target: 'success'},
|
|
145
|
+
UPLOAD_FAILURE: {
|
|
146
|
+
target: 'failed',
|
|
147
|
+
assigners: [({context, event}) => ({...context, errorMessage: event.error})],
|
|
119
148
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
failed: {
|
|
152
|
+
on: {
|
|
153
|
+
RETRY: {
|
|
154
|
+
target: 'uploading',
|
|
155
|
+
guard: ({context}) => context.fileId !== null, // Guard evaluates transition authorization
|
|
124
156
|
},
|
|
125
157
|
},
|
|
126
158
|
},
|
|
159
|
+
success: {},
|
|
127
160
|
},
|
|
128
161
|
};
|
|
129
162
|
```
|
|
130
163
|
|
|
131
|
-
###
|
|
164
|
+
### 3. Initialize & Use
|
|
132
165
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
```ts
|
|
166
|
+
```typescript
|
|
167
|
+
// main.ts
|
|
137
168
|
import {createFsmService} from '@alwatr/fsm';
|
|
169
|
+
import {fileUploadConfig} from './config.js';
|
|
138
170
|
|
|
139
|
-
//
|
|
140
|
-
const
|
|
171
|
+
// Instantiate the FSM service using the factory function
|
|
172
|
+
export const fileUploadService = createFsmService(fileUploadConfig);
|
|
141
173
|
|
|
142
|
-
// Subscribe to state changes
|
|
143
|
-
|
|
144
|
-
console.log(`
|
|
174
|
+
// Subscribe to state and context changes (fine-grained reactivity)
|
|
175
|
+
fileUploadService.stateSignal.subscribe((state) => {
|
|
176
|
+
console.log(`Current FSM State: ${state.name}`);
|
|
177
|
+
console.log(`Context Progress: ${state.context.progress}%`);
|
|
145
178
|
});
|
|
146
179
|
|
|
147
|
-
// Dispatch events
|
|
148
|
-
|
|
149
|
-
// Logs: Light is on with brightness 100
|
|
150
|
-
|
|
151
|
-
lightService.eventSignal.dispatch({type: 'SET_BRIGHTNESS', level: 50});
|
|
152
|
-
// Logs: Light is on with brightness 50
|
|
153
|
-
|
|
154
|
-
lightService.eventSignal.dispatch({type: 'TOGGLE'});
|
|
155
|
-
// Logs: Light is off with brightness 0
|
|
180
|
+
// Dispatch events into the machine mailbox
|
|
181
|
+
fileUploadService.dispatch({type: 'START_UPLOAD', fileId: 'doc_102'});
|
|
156
182
|
```
|
|
157
183
|
|
|
158
|
-
|
|
184
|
+
---
|
|
159
185
|
|
|
160
|
-
|
|
186
|
+
## ⚡ Advanced Features
|
|
161
187
|
|
|
162
|
-
|
|
163
|
-
import {createFsmService} from '@alwatr/fsm';
|
|
164
|
-
import type {StateMachineConfig} from '@alwatr/fsm';
|
|
188
|
+
### 1. Local / Session State Persistence
|
|
165
189
|
|
|
166
|
-
|
|
167
|
-
type User = {id: string; name: string};
|
|
168
|
-
type FetchContext = {user: User | null; error: string | null};
|
|
169
|
-
type FetchState = 'idle' | 'pending' | 'success' | 'error';
|
|
170
|
-
type FetchEvent = {type: 'FETCH'; id: string} | {type: 'RESOLVE'; user: User} | {type: 'REJECT'; error: string} | {type: 'RETRY'};
|
|
190
|
+
Make your state machine crash-resilient by syncing both state and context automatically with `localStorage` or `sessionStorage`.
|
|
171
191
|
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
name: '
|
|
192
|
+
```typescript
|
|
193
|
+
export const fileUploadConfig: StateMachineConfig<FileState, FileEvent, FileContext> = {
|
|
194
|
+
name: 'file-upload-lifecycle',
|
|
175
195
|
initial: 'idle',
|
|
176
|
-
context: {
|
|
177
|
-
|
|
178
|
-
|
|
196
|
+
context: {fileId: null, progress: 0, errorMessage: null},
|
|
197
|
+
persistent: {
|
|
198
|
+
schemaVersion: 1, // Automatic clear and reset if version bumps
|
|
199
|
+
storageKey: 'file-upload-state', // LocalStorage item key
|
|
179
200
|
},
|
|
180
201
|
states: {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
202
|
+
// ...
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### 2. Nested Transitions (Guards & Fallbacks)
|
|
208
|
+
|
|
209
|
+
You can define an array of transitions for a single event. The engine evaluates guards in order and executes the first valid one. A transition without a guard acts as a fallback.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
states: {
|
|
213
|
+
idle: {
|
|
214
|
+
on: {
|
|
215
|
+
VERIFY: [
|
|
216
|
+
{target: 'high_priority', guard: ({context}) => context.score > 90},
|
|
217
|
+
{target: 'medium_priority', guard: ({context}) => context.score > 50},
|
|
218
|
+
{target: 'low_priority'}, // Fallback path
|
|
219
|
+
],
|
|
185
220
|
},
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 3. Multiple Entry/Exit Effects
|
|
226
|
+
|
|
227
|
+
You can run multiple side-effects (synchronous or asynchronous fire-and-forget) in order when entering or leaving a state by specifying an array of effects. Note that these are executed synchronously by the FSM (the FSM does not wait for any returned Promises to resolve).
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
states: {
|
|
231
|
+
active: {
|
|
232
|
+
entry: [
|
|
233
|
+
({event}) => console.log('First entry effect', event),
|
|
234
|
+
async ({context}) => {
|
|
235
|
+
await saveProgress(context);
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
exit: [
|
|
239
|
+
() => console.log('Exit effect'),
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
> [!WARNING]
|
|
246
|
+
> **Initial State Entry Events**:
|
|
247
|
+
> When the FSM is initialized, the entry effects and actors of the initial state are executed with a mock event `{ type: '__init__' }` (casted to `TEvent`).
|
|
248
|
+
> If your initial state's entry effects or actors expect specific custom payload properties on the triggering event, they might throw runtime errors. Ensure your initial state's entry logic checks the event type or handles `{ type: '__init__' }` safely.
|
|
249
|
+
|
|
250
|
+
### 4. State Actors (Invoked Actors)
|
|
251
|
+
|
|
252
|
+
State Actors are async lifecycle processes spawned automatically when entering a state. In contrast to **Effects** (which are fire-and-forget synchronous actions), an **Actor**:
|
|
253
|
+
|
|
254
|
+
1. Is instantiated dynamically upon state entry.
|
|
255
|
+
2. Receives a `dispatch(event)` callback to asynchronously send events back to the parent FSM.
|
|
256
|
+
3. Can return a synchronous cleanup/teardown function that is executed automatically when the FSM exits the state or is destroyed.
|
|
257
|
+
|
|
258
|
+
This conforms to XState v5 patterns and is extremely useful for running side-effects with a defined lifecycle, such as polling intervals, websocket listeners, or async fetch requests.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
states: {
|
|
262
|
+
uploading: {
|
|
263
|
+
actors: [
|
|
264
|
+
({context, dispatch}) => {
|
|
265
|
+
console.log('Spawning polling actor...');
|
|
266
|
+
|
|
267
|
+
const intervalId = setInterval(async () => {
|
|
191
268
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
269
|
+
const status = await getStatus(context.fileId);
|
|
270
|
+
if (status.done) {
|
|
271
|
+
dispatch({type: 'UPLOAD_SUCCESS'});
|
|
272
|
+
} else {
|
|
273
|
+
dispatch({type: 'PROGRESS_UPDATE', percent: status.progress});
|
|
274
|
+
}
|
|
198
275
|
} catch (err) {
|
|
199
|
-
|
|
200
|
-
return {type: 'REJECT', error: (err as Error).message};
|
|
276
|
+
dispatch({type: 'UPLOAD_FAILURE', error: err.message});
|
|
201
277
|
}
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
REJECT: {
|
|
210
|
-
target: 'error',
|
|
211
|
-
assigners: [(event) => ({error: event.error})],
|
|
212
|
-
},
|
|
278
|
+
}, 1000);
|
|
279
|
+
|
|
280
|
+
// Return a cleanup callback run automatically on state exit
|
|
281
|
+
return () => {
|
|
282
|
+
console.log('Cleaning up polling actor...');
|
|
283
|
+
clearInterval(intervalId);
|
|
284
|
+
};
|
|
213
285
|
},
|
|
286
|
+
],
|
|
287
|
+
on: {
|
|
288
|
+
PROGRESS_UPDATE: {assigners: [({context, event}) => ({...context, progress: event.percent})]},
|
|
289
|
+
UPLOAD_SUCCESS: {target: 'success'},
|
|
290
|
+
UPLOAD_FAILURE: {target: 'failed'},
|
|
214
291
|
},
|
|
215
|
-
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 5. Internal vs. External Transitions (Self-Transitions)
|
|
297
|
+
|
|
298
|
+
Transitions are categorized into two types based on the presence of the `target` property:
|
|
299
|
+
|
|
300
|
+
1. **Internal Transitions** (No `target` specified):
|
|
301
|
+
- Triggered when a transition updates the machine's `context` (via `assigners`) but keeps the machine in the current state.
|
|
302
|
+
- **Behavior**: State entry/exit effects are **not** executed, and active state actors are **not** restarted.
|
|
303
|
+
- **Use Case**: Simple data updates (e.g., updating progress percentages or input values).
|
|
304
|
+
|
|
305
|
+
2. **External Transitions** (With `target` specified):
|
|
306
|
+
- Triggered when transitioning to a target state, **even if the target state is the same as the current state** (a self-transition).
|
|
307
|
+
- **Behavior**: Exits the current state (running `exit` effects and cleaning up active actors) and re-enters the state (running `entry` effects and spawning actors).
|
|
308
|
+
- **Use Case**: Resetting state-scoped processes, restarting animations, or refetching initial state data upon self-referential triggers.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## FSM as an Actor Model
|
|
313
|
+
|
|
314
|
+
In highly scalable architectures, complex user interface elements or backend workflows are best structured using the **Actor Model**. Under this paradigm, every specialized system is treated as an isolated, sovereign entity called an **Actor**.
|
|
315
|
+
|
|
316
|
+
An Actor complies strictly with three architectural rules:
|
|
317
|
+
|
|
318
|
+
1. It maintains isolated local state that nobody else can mutate directly.
|
|
319
|
+
2. It processes events via an inbound mailbox sequentially.
|
|
320
|
+
3. It sends asynchronous messages to other Actors to notify them of shifts in reality.
|
|
321
|
+
|
|
322
|
+
### The Alwatr Synergy: FSM + Flux Action Bus
|
|
323
|
+
|
|
324
|
+
`@alwatr/fsm` acts as the perfect structural core for an Actor.
|
|
325
|
+
|
|
326
|
+
- **The Brain**: `FsmService` encapsulates your local state (`stateSignal`) and contextual calculations.
|
|
327
|
+
- **The Mailbox**: The FSM's internal `eventSignal` serves as the private inbound message queue, exposed via the `dispatch` method.
|
|
328
|
+
- **The Global Nervous System**: The global `@alwatr/flux` Action Bus (`actionService.dispatch` / `actionService.on`) handles asynchronous messaging between different dockets of the system.
|
|
329
|
+
|
|
330
|
+
### Concrete Actor Workflow Implementation
|
|
331
|
+
|
|
332
|
+
By combining an Input Controller, an FsmService, and Output Effects, you construct a pure Actor that is completely decoupled from other business domains.
|
|
333
|
+
|
|
334
|
+
#### Step A: The Input Gate (The Controller Box)
|
|
335
|
+
|
|
336
|
+
The Input Controller intercepts generic global intents from the `@alwatr/flux` ecosystem, confirms target context keys, and queues them into the Actor's private FSM mailbox.
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// account-actor-controller.ts
|
|
340
|
+
import {actionService} from '@alwatr/flux';
|
|
341
|
+
import {accountFsmService} from './account-actor-service.js';
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @context AI: Input Mailbox Router for the Account Actor.
|
|
345
|
+
* Intercepts generic UI actions and converts them into specific internal FSM events.
|
|
346
|
+
*/
|
|
347
|
+
export function setupAccountActorController() {
|
|
348
|
+
// Catching global dialog resolutions without importing the Dialog Service directly
|
|
349
|
+
actionService.on('ui_confirm_dialog_resolved', (action) => {
|
|
350
|
+
// Structural Guard Boundary check: Ensure this confirmation belongs to this actor
|
|
351
|
+
if (action.context !== 'account_purging_flow') return;
|
|
352
|
+
|
|
353
|
+
const isApproved = action.payload === 'approve';
|
|
354
|
+
|
|
355
|
+
// Map the external system intent safely to the internal actor machine
|
|
356
|
+
accountFsmService.dispatch(isApproved ? {type: 'CONFIRM'} : {type: 'ABORT'});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
#### Step B: The Core Logic (The Actor Brain Config)
|
|
362
|
+
|
|
363
|
+
The internal machine processes the transition cleanly. When it shifts state, it uses entry/exit array hooks to broadcast outgoing signals to foreign actors.
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// account-actor-config.ts
|
|
367
|
+
import type {StateMachineConfig} from '@alwatr/fsm';
|
|
368
|
+
import {actionService} from '@alwatr/flux';
|
|
369
|
+
|
|
370
|
+
export const accountActorConfig: StateMachineConfig<any, any, any> = {
|
|
371
|
+
name: 'account-actor-core',
|
|
372
|
+
initial: 'active',
|
|
373
|
+
context: {userId: 'usr_882'},
|
|
374
|
+
states: {
|
|
375
|
+
active: {
|
|
216
376
|
on: {
|
|
217
|
-
|
|
377
|
+
TRIGGER_DELETE: {target: 'awaiting_confirmation'},
|
|
218
378
|
},
|
|
219
379
|
},
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
380
|
+
awaiting_confirmation: {
|
|
381
|
+
entry: [
|
|
382
|
+
// AI Note: Outbound Messaging. The Actor sends an instruction out over the nervous system.
|
|
383
|
+
() => {
|
|
384
|
+
actionService.dispatch({
|
|
385
|
+
type: 'confirm_dialog_requested',
|
|
386
|
+
payload: {
|
|
387
|
+
targetContext: 'account_purging_flow',
|
|
388
|
+
title: 'Purge Records',
|
|
389
|
+
message: 'Are you sure you want to completely erase your historical profile?',
|
|
390
|
+
},
|
|
391
|
+
});
|
|
230
392
|
},
|
|
393
|
+
],
|
|
394
|
+
on: {
|
|
395
|
+
CONFIRM: {target: 'wiping_data'},
|
|
396
|
+
ABORT: {target: 'active'},
|
|
231
397
|
},
|
|
232
398
|
},
|
|
399
|
+
wiping_data: {
|
|
400
|
+
entry: [
|
|
401
|
+
async () => {
|
|
402
|
+
// Perform isolated backend calls...
|
|
403
|
+
actionService.dispatch({type: 'confirm_dialog_close_requested'});
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
},
|
|
233
407
|
},
|
|
234
408
|
};
|
|
409
|
+
```
|
|
235
410
|
|
|
236
|
-
|
|
237
|
-
const fetchService = createFsmService(fetchMachineConfig);
|
|
411
|
+
### Strategic Benefits of the Actor Pattern
|
|
238
412
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (state.name === 'error') console.log('Error:', state.context.error);
|
|
243
|
-
});
|
|
413
|
+
1. **Absolute Multi-Domain Decoupling**: Services never invoke each other directly. They speak exclusively via event packets, guaranteeing that any single domain can be refactored or swapped out without causing regressions.
|
|
414
|
+
2. **Deterministic Parallelism**: Race conditions are structurally impossible. State transitions happen inside isolated microtasks while network responses queue outside the gate.
|
|
415
|
+
3. **AI-Agent Optimization**: The explicit segregation of Input Routing (Controllers), Context Transitions (FSM), and Outbound Signals (Effects) provides rigid, understandable boundaries that LLMs can parse and write code for with near-perfect reliability.
|
|
244
416
|
|
|
245
|
-
|
|
246
|
-
```
|
|
417
|
+
---
|
|
247
418
|
|
|
248
|
-
##
|
|
419
|
+
## API Reference
|
|
249
420
|
|
|
250
|
-
|
|
251
|
-
<br>
|
|
252
|
-
**قابل پیشبینی و ترسیم**: با محدود کردن زمان و چگونگی تغییر وضعیت، منطق برنامه شما قطعی و قابل درک میشود. ماهیت اعلانی پیکربندی، امکان ترسیم و مشاهده تمام وضعیتها و گذارهای ممکن را فراهم میکند.
|
|
421
|
+
### `createFsmService(config)`
|
|
253
422
|
|
|
254
|
-
|
|
255
|
-
<br>
|
|
256
|
-
**ذاتاً تایپ-سیف**: این کتابخانه از پایه با TypeScript ساخته شده است. این امر تضمین میکند که پارامترهای رویدادها در هر گذار به درستی تایپدهی شده و `assigner`ها زمینه (`context`) را با دادههای معتبر بهروزرسانی میکنند، که از بروز دسته وسیعی از باگها در زمان کامپایل جلوگیری میکند.
|
|
423
|
+
The primary factory utility to initiate a reactive FSM.
|
|
257
424
|
|
|
258
|
-
-
|
|
259
|
-
|
|
260
|
-
|
|
425
|
+
- **`config`**: `StateMachineConfig<TState, TEvent, TContext>` — Declarative system matrix detailing properties and state hooks.
|
|
426
|
+
- **Returns**: `FsmService<TState, TEvent, TContext>` — The assigned runtime manager.
|
|
427
|
+
- `stateSignal`: An `IReadonlySignal` broadcasting atomic state shifts.
|
|
428
|
+
- `dispatch(event)`: Dispatches a typed event to the state machine.
|
|
429
|
+
- `destroy()`: Cleans up local allocations to completely prevent memory retention issues.
|
|
261
430
|
|
|
262
|
-
|
|
263
|
-
<br>
|
|
264
|
-
**مستحکم و انعطافپذیر**: ماشین بر اساس مدل اجرای کامل تا انتها (RTC) عمل میکند، که تضمین میکند هر رویداد به طور کامل پردازش شده و سپس رویداد بعدی آغاز میشود. این از بروز race condition جلوگیری میکند. علاوه بر این، هرگونه خطای ایجاد شده در توابع تعریفشده توسط کاربر (`condition`, `assigner`, `effect`) مدیریت و لاگ میشود و از کرش کردن کل ماشین جلوگیری میکند.
|
|
431
|
+
### Key Types
|
|
265
432
|
|
|
266
|
-
|
|
433
|
+
| Type | Description |
|
|
434
|
+
| :------------------------- | :--------------------------------------------------------------------------------------------------------------- |
|
|
435
|
+
| **`MachineState<S, C>`** | Represents the complete state, containing `name: S` and `context: C`. |
|
|
436
|
+
| **`MachineEvent<T>`** | The base interface for events. Must have a `type: T` property. |
|
|
437
|
+
| **`StateMachineConfig`** | The main configuration object defining `initial`, `context`, and `states`. |
|
|
438
|
+
| **`FsmPersistenceConfig`** | Configuration options for FSM state persistence (`schemaVersion`, `storageKey`). |
|
|
439
|
+
| **`Transition`** | Defines a transition with an optional `target`, `guard`, and `assigners`. |
|
|
440
|
+
| **`Assigner<E, C>`** | A synchronous function that returns the next context object or void (if mutated directly). |
|
|
441
|
+
| **`Effect<E, C>`** | A synchronous or asynchronous fire-and-forget function executed on state entry/exit (returns `Awaitable<void>`). |
|
|
442
|
+
| **`Actor<E, C>`** | A function spawned on state entry that can call `dispatch(event)` and return a cleanup function. |
|
|
443
|
+
| **`Guard<E, C>`** | A boolean guard function that must return `true` for a transition branch to be taken. |
|
|
267
444
|
|
|
268
|
-
|
|
445
|
+
---
|
|
269
446
|
|
|
270
|
-
|
|
271
|
-
تابع اصلی برای ساخت یک سرویس FSM جدید.
|
|
447
|
+
## 🏛️ Part of the Alwatr Ecosystem
|
|
272
448
|
|
|
273
|
-
|
|
274
|
-
- The declarative configuration object for the state machine.
|
|
275
|
-
- آبجکت پیکربندی اعلانی برای ماشین حالت.
|
|
276
|
-
- **Returns**: `FsmService<TState, TEvent, TContext>`
|
|
277
|
-
- An instance of the FSM service with two main properties:
|
|
278
|
-
- `stateSignal`: A readable signal that emits the current `MachineState`.
|
|
279
|
-
- `eventSignal`: A signal to `dispatch` new events to the machine.
|
|
280
|
-
- یک نمونه از سرویس FSM با دو پراپرتی اصلی:
|
|
281
|
-
- `stateSignal`: یک سیگنال خواندنی که `MachineState` فعلی را منتشر میکند.
|
|
282
|
-
- `eventSignal`: سیگنالی برای ارسال (`dispatch`) رویدادهای جدید به ماشین.
|
|
449
|
+
`@alwatr/fsm` is part of the **Alwatr Developer Kit**, a monorepo of lightweight TypeScript libraries designed for modularity and performance.
|
|
283
450
|
|
|
284
|
-
|
|
451
|
+
- **[@alwatr/flux](https://github.com/Alwatr/alwatr/tree/next/pkg/flux)** — UI and reactive bundle (aggregates signals, actions, and FSM).
|
|
452
|
+
- **[@alwatr/signal](https://github.com/Alwatr/alwatr/tree/next/pkg/nanolib/signal)** — Fine-grained reactive signal bus.
|
|
453
|
+
- **[@alwatr/action](https://github.com/Alwatr/alwatr/tree/next/pkg/nanolib/action)** — Global event delegation action bus.
|
|
454
|
+
- **[@alwatr/directive](https://github.com/Alwatr/alwatr/tree/next/pkg/nanolib/directive)** — Attribute-based DOM directives.
|
|
285
455
|
|
|
286
|
-
|
|
287
|
-
| :----------------------- | :---------------------------------------------------------------------------- |
|
|
288
|
-
| **`MachineState<S, C>`** | Represents the complete state, containing `name: S` and `context: C`. |
|
|
289
|
-
| **`MachineEvent<T>`** | The base interface for events. Must have a `type: T` property. |
|
|
290
|
-
| **`StateMachineConfig`** | The main configuration object defining `initial`, `context`, and `states`. |
|
|
291
|
-
| **`Transition`** | Defines a transition with an optional `target`, `condition`, and `assigners`. |
|
|
456
|
+
---
|
|
292
457
|
|
|
293
|
-
|
|
294
|
-
| :----------------------- | :------------------------------------------------------------------------------------------------------------- |
|
|
295
|
-
| **`MachineState<S, C>`** | وضعیت کامل ماشین را نمایش میدهد که شامل `name: S` (نام وضعیت) و `context: C` (زمینه) است. |
|
|
296
|
-
| **`MachineEvent<T>`** | اینترفیس پایه برای رویدادها. باید یک پراپرتی `type: T` داشته باشد. |
|
|
297
|
-
| **`StateMachineConfig`** | آبجکت اصلی پیکربندی که `initial` (وضعیت اولیه)، `context` (زمینه اولیه) و `states` (وضعیتها) را تعریف میکند. |
|
|
298
|
-
| **`Transition`** | یک گذار را با `target` (مقصد)، `condition` (شرط) و `assigners` (تخصیصدهندهها)ی اختیاری تعریف میکند. |
|
|
458
|
+
## 🤝 Contributing
|
|
299
459
|
|
|
300
|
-
|
|
460
|
+
We welcome contributions! Please see our [Contributing Guide](https://github.com/Alwatr/alwatr/blob/next/CONTRIBUTING.md).
|
|
301
461
|
|
|
302
|
-
|
|
462
|
+
---
|
|
303
463
|
|
|
304
|
-
##
|
|
464
|
+
## 📄 License
|
|
305
465
|
|
|
306
|
-
|
|
466
|
+
[MPL-2.0](https://github.com/Alwatr/alwatr/blob/next/LICENSE) © [S. Ali Mihandoost](https://ali.mihandoost.com)
|