@hkdigital/lib-core 0.4.22 → 0.4.24
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/dist/auth/jwt/util.js +35 -41
- package/dist/network/loaders/audio/AudioScene.svelte.js +2 -2
- package/dist/network/loaders/image/ImageScene.svelte.js +18 -28
- package/dist/network/states/NetworkLoader.svelte.d.ts +1 -1
- package/dist/network/states/NetworkLoader.svelte.js +2 -2
- package/dist/services/README.md +23 -0
- package/dist/state/classes.d.ts +0 -2
- package/dist/state/classes.js +0 -2
- package/dist/state/{classes → machines}/finite-state-machine/FiniteStateMachine.svelte.d.ts +10 -0
- package/dist/state/{classes → machines}/finite-state-machine/FiniteStateMachine.svelte.js +19 -1
- package/dist/state/machines/finite-state-machine/README.md +545 -0
- package/dist/state/{classes → machines}/finite-state-machine/index.d.ts +1 -1
- package/dist/state/{classes → machines}/finite-state-machine/index.js +1 -1
- package/dist/state/machines/finite-state-machine/typedef.d.ts +29 -0
- package/dist/state/machines/finite-state-machine/typedef.js +28 -0
- package/dist/state/{classes → machines}/loading-state-machine/LoadingStateMachine.svelte.d.ts +0 -2
- package/dist/state/{classes → machines}/loading-state-machine/LoadingStateMachine.svelte.js +7 -27
- package/dist/state/machines/loading-state-machine/README.md +544 -0
- package/dist/state/machines/typedef.d.ts +1 -0
- package/dist/state/machines/typedef.js +1 -0
- package/dist/state/machines.d.ts +2 -0
- package/dist/state/machines.js +2 -0
- package/dist/state/typedef.d.ts +1 -0
- package/dist/state/typedef.js +1 -0
- package/dist/ui/components/game-box/README.md +245 -0
- package/package.json +1 -1
- /package/dist/state/{classes → machines}/loading-state-machine/constants.d.ts +0 -0
- /package/dist/state/{classes → machines}/loading-state-machine/constants.js +0 -0
- /package/dist/state/{classes → machines}/loading-state-machine/index.d.ts +0 -0
- /package/dist/state/{classes → machines}/loading-state-machine/index.js +0 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# FiniteStateMachine
|
|
2
|
+
|
|
3
|
+
A lightweight finite state machine implementation for JavaScript applications, designed to work seamlessly with Svelte 5 runes.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `FiniteStateMachine` class provides a simple yet powerful way to manage application state through well-defined states and transitions. It supports enter/exit callbacks, event-driven transitions, and debounced events.
|
|
8
|
+
|
|
9
|
+
## Basic Usage
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
import { FiniteStateMachine } from '$lib/state/classes.js';
|
|
13
|
+
|
|
14
|
+
const machine = new FiniteStateMachine('idle', {
|
|
15
|
+
idle: {
|
|
16
|
+
start: 'running'
|
|
17
|
+
},
|
|
18
|
+
running: {
|
|
19
|
+
pause: 'paused',
|
|
20
|
+
stop: 'idle'
|
|
21
|
+
},
|
|
22
|
+
paused: {
|
|
23
|
+
resume: 'running',
|
|
24
|
+
stop: 'idle'
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Check current state
|
|
29
|
+
console.log(machine.current); // 'idle'
|
|
30
|
+
|
|
31
|
+
// Send events to trigger transitions
|
|
32
|
+
machine.send('start');
|
|
33
|
+
console.log(machine.current); // 'running'
|
|
34
|
+
|
|
35
|
+
machine.send('pause');
|
|
36
|
+
console.log(machine.current); // 'paused'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Constructor
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
new FiniteStateMachine(initialState, states)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `initialState`: The starting state (string)
|
|
46
|
+
- `states`: Object defining available states and their transitions
|
|
47
|
+
|
|
48
|
+
## State Definition
|
|
49
|
+
|
|
50
|
+
Each state can define:
|
|
51
|
+
|
|
52
|
+
- **Transitions**: Event name → target state
|
|
53
|
+
- **Enter callback**: `_enter` function called when entering the state
|
|
54
|
+
- **Exit callback**: `_exit` function called when leaving the state
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
const machine = new FiniteStateMachine('idle', {
|
|
58
|
+
idle: {
|
|
59
|
+
_enter: (metadata) => {
|
|
60
|
+
console.log('Entered idle state');
|
|
61
|
+
},
|
|
62
|
+
_exit: (metadata) => {
|
|
63
|
+
console.log('Leaving idle state');
|
|
64
|
+
},
|
|
65
|
+
start: 'running'
|
|
66
|
+
},
|
|
67
|
+
running: {
|
|
68
|
+
_enter: (metadata) => {
|
|
69
|
+
console.log('Started running');
|
|
70
|
+
},
|
|
71
|
+
stop: 'idle'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Callback Metadata
|
|
77
|
+
|
|
78
|
+
Enter and exit callbacks receive metadata about the transition:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
/** @typedef {import('./typedef.js').StateTransitionMetadata} StateTransitionMetadata */
|
|
82
|
+
|
|
83
|
+
// Metadata structure:
|
|
84
|
+
{
|
|
85
|
+
from: 'previousState', // State being exited
|
|
86
|
+
to: 'newState', // State being entered
|
|
87
|
+
event: 'eventName', // Event that triggered transition
|
|
88
|
+
args: [] // Arguments passed to send()
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### TypeScript/JSDoc Integration
|
|
93
|
+
|
|
94
|
+
For better type safety, import the type definitions:
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
/** @typedef {import('./typedef.js').StateTransitionMetadata} StateTransitionMetadata */
|
|
98
|
+
/** @typedef {import('./typedef.js').OnEnterCallback} OnEnterCallback */
|
|
99
|
+
/** @typedef {import('./typedef.js').OnExitCallback} OnExitCallback */
|
|
100
|
+
|
|
101
|
+
/** @type {OnEnterCallback} */
|
|
102
|
+
const handleEnter = (state, metadata) => {
|
|
103
|
+
console.log(`Entering ${state} from ${metadata.from}`);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** @type {OnExitCallback} */
|
|
107
|
+
const handleExit = (state, metadata) => {
|
|
108
|
+
console.log(`Leaving ${state} to ${metadata.to}`);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
machine.onenter = handleEnter;
|
|
112
|
+
machine.onexit = handleExit;
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Methods
|
|
116
|
+
|
|
117
|
+
### `send(event, ...args)`
|
|
118
|
+
|
|
119
|
+
Triggers a state transition based on the event.
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
machine.send('start');
|
|
123
|
+
machine.send('error', new Error('Something went wrong'));
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Returns the current state after processing the event.
|
|
127
|
+
|
|
128
|
+
### `debounce(wait, event, ...args)`
|
|
129
|
+
|
|
130
|
+
Debounces event sending to prevent rapid-fire transitions.
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
// Will only execute after 500ms of inactivity
|
|
134
|
+
await machine.debounce(500, 'search', query);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `current` (getter)
|
|
138
|
+
|
|
139
|
+
Returns the current state as a string.
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
console.log(machine.current); // 'idle'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Advanced Features
|
|
146
|
+
|
|
147
|
+
### Wildcard States
|
|
148
|
+
|
|
149
|
+
Use `*` to define transitions available from any state:
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
const machine = new FiniteStateMachine('idle', {
|
|
153
|
+
idle: {
|
|
154
|
+
start: 'running'
|
|
155
|
+
},
|
|
156
|
+
running: {
|
|
157
|
+
pause: 'paused'
|
|
158
|
+
},
|
|
159
|
+
paused: {
|
|
160
|
+
resume: 'running'
|
|
161
|
+
},
|
|
162
|
+
'*': {
|
|
163
|
+
reset: 'idle', // Available from any state
|
|
164
|
+
error: 'error'
|
|
165
|
+
},
|
|
166
|
+
error: {
|
|
167
|
+
reset: 'idle'
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Same-State Transitions
|
|
173
|
+
|
|
174
|
+
Same-state transitions (e.g., `idle → idle`) do NOT trigger enter/exit
|
|
175
|
+
callbacks. The state remains unchanged.
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
machine.send('reset'); // If already in 'idle', no callbacks are called
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Function Actions
|
|
182
|
+
|
|
183
|
+
Instead of target states, you can define function actions:
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
const machine = new FiniteStateMachine('idle', {
|
|
187
|
+
idle: {
|
|
188
|
+
log: () => {
|
|
189
|
+
console.log('Logging from idle state');
|
|
190
|
+
return 'idle'; // Stay in same state
|
|
191
|
+
},
|
|
192
|
+
start: 'running'
|
|
193
|
+
},
|
|
194
|
+
running: {
|
|
195
|
+
stop: 'idle'
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## onenter and onexit Callbacks
|
|
201
|
+
|
|
202
|
+
The `onenter` and `onexit` callbacks provide a unified way to react to all state changes, designed to work reliably with Svelte's reactivity system:
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
const machine = new FiniteStateMachine('idle', {
|
|
206
|
+
idle: { start: 'loading' },
|
|
207
|
+
loading: { complete: 'loaded', error: 'error' },
|
|
208
|
+
loaded: { reset: 'idle' },
|
|
209
|
+
error: { retry: 'loading', reset: 'idle' }
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
machine.onexit = (state, metadata) => {
|
|
213
|
+
switch (state) {
|
|
214
|
+
case 'loading':
|
|
215
|
+
console.log('Leaving loading state...');
|
|
216
|
+
// Cancel ongoing requests
|
|
217
|
+
abortController?.abort();
|
|
218
|
+
break;
|
|
219
|
+
case 'loaded':
|
|
220
|
+
console.log('Leaving loaded state...');
|
|
221
|
+
// Cleanup resources
|
|
222
|
+
releaseResources();
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
machine.onenter = (state, metadata) => {
|
|
228
|
+
switch (state) {
|
|
229
|
+
case 'loading':
|
|
230
|
+
console.log('Started loading...');
|
|
231
|
+
showSpinner();
|
|
232
|
+
break;
|
|
233
|
+
case 'loaded':
|
|
234
|
+
console.log('Loading complete!');
|
|
235
|
+
hideSpinner();
|
|
236
|
+
break;
|
|
237
|
+
case 'error':
|
|
238
|
+
console.log('Loading failed');
|
|
239
|
+
showError(metadata.args[0]);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Callback Execution Order
|
|
246
|
+
|
|
247
|
+
The callbacks are executed in this specific order during state transitions:
|
|
248
|
+
|
|
249
|
+
1. **`onexit`** - Called before leaving current state
|
|
250
|
+
2. **`_exit`** - Individual state exit callback
|
|
251
|
+
3. **`_enter`** - Individual state enter callback
|
|
252
|
+
4. **`onenter`** - Called after entering new state
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
const machine = new FiniteStateMachine('idle', {
|
|
256
|
+
idle: {
|
|
257
|
+
_enter: () => console.log('2. idle _enter'),
|
|
258
|
+
_exit: () => console.log('4. idle _exit'),
|
|
259
|
+
start: 'loading'
|
|
260
|
+
},
|
|
261
|
+
loading: {
|
|
262
|
+
_enter: () => console.log('5. loading _enter'),
|
|
263
|
+
complete: 'loaded'
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
machine.onexit = (state) => console.log(`3. onexit ${state}`);
|
|
268
|
+
machine.onenter = (state) => console.log(`6. onenter ${state}`);
|
|
269
|
+
|
|
270
|
+
// Initial state triggers _enter and onenter
|
|
271
|
+
// Output:
|
|
272
|
+
// 2. idle _enter
|
|
273
|
+
// 6. onenter idle
|
|
274
|
+
|
|
275
|
+
machine.send('start');
|
|
276
|
+
// Output:
|
|
277
|
+
// 3. onexit idle
|
|
278
|
+
// 4. idle _exit
|
|
279
|
+
// 5. loading _enter
|
|
280
|
+
// 6. onenter loading
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### onexit vs onenter
|
|
284
|
+
|
|
285
|
+
- **`onexit`**: Called when leaving a state - perfect for cleanup, cancellation, resource release
|
|
286
|
+
- **`onenter`**: Called when entering a state - perfect for initialization, setup, starting processes
|
|
287
|
+
- **`_exit`/`_enter`**: Individual per-state callbacks, called during transition
|
|
288
|
+
- **All callbacks are optional**: Set to null if not needed
|
|
289
|
+
|
|
290
|
+
## Integration with Svelte Reactivity
|
|
291
|
+
|
|
292
|
+
When using FiniteStateMachine with Svelte's reactive derived state, use this pattern to avoid timing issues and ensure reliable state monitoring:
|
|
293
|
+
|
|
294
|
+
### Pattern: Separate onenter from Reactive Monitoring
|
|
295
|
+
|
|
296
|
+
**✅ Use `onexit` and `onenter` for immediate state actions:**
|
|
297
|
+
```javascript
|
|
298
|
+
const machine = new FiniteStateMachine('idle', {
|
|
299
|
+
idle: { start: 'loading' },
|
|
300
|
+
loading: { complete: 'loaded' },
|
|
301
|
+
loaded: { reset: 'idle' }
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
machine.onexit = (state) => {
|
|
305
|
+
switch (state) {
|
|
306
|
+
case 'loading':
|
|
307
|
+
this.#cancelProcess(); // Cancel ongoing operations
|
|
308
|
+
break;
|
|
309
|
+
case 'loaded':
|
|
310
|
+
this.#releaseResources(); // Cleanup when leaving
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
machine.onenter = (state) => {
|
|
316
|
+
switch (state) {
|
|
317
|
+
case 'loading':
|
|
318
|
+
this.#startProcess(); // Start async process immediately
|
|
319
|
+
break;
|
|
320
|
+
case 'loaded':
|
|
321
|
+
this.#notifyComplete(); // Notify when complete
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
this.state = state; // Update public state
|
|
325
|
+
};
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**✅ Use `$effect` for reactive state monitoring:**
|
|
329
|
+
```javascript
|
|
330
|
+
// Monitor derived/computed values and trigger transitions when conditions are met
|
|
331
|
+
$effect(() => {
|
|
332
|
+
if (machine.current === 'loading') {
|
|
333
|
+
// Check completion based on reactive derived values
|
|
334
|
+
if (this.#itemsCompleted === this.#totalItems && this.#totalItems > 0) {
|
|
335
|
+
machine.send('complete'); // Trigger transition when condition met
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Why This Pattern?
|
|
342
|
+
|
|
343
|
+
- **`onexit`**: Handles cleanup and teardown when leaving states
|
|
344
|
+
- **`onenter`**: Handles setup and initialization when entering states
|
|
345
|
+
- **`$effect`**: Handles reactive monitoring of derived/computed values over time
|
|
346
|
+
- **Avoids timing issues**: Doesn't check completion status immediately on state entry
|
|
347
|
+
- **Leverages Svelte reactivity**: Automatically responds to changes in reactive variables
|
|
348
|
+
- **Clean separation**: State machine handles discrete transitions, effects handle continuous monitoring
|
|
349
|
+
- **Complete lifecycle**: Full control over state entry and exit
|
|
350
|
+
|
|
351
|
+
### Use Cases for This Pattern:
|
|
352
|
+
|
|
353
|
+
- **Progress monitoring**: File uploads, multi-step processes
|
|
354
|
+
- **Derived state transitions**: Validation completion, multi-source loading
|
|
355
|
+
- **Real-time condition checking**: Monitoring reactive computed properties
|
|
356
|
+
- **Complex completion logic**: When completion depends on multiple reactive values
|
|
357
|
+
|
|
358
|
+
### Example: Multi-Task Processor
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
export default class TaskProcessor {
|
|
362
|
+
#machine = new FiniteStateMachine('idle', {
|
|
363
|
+
idle: { start: 'processing' },
|
|
364
|
+
processing: { complete: 'finished', error: 'failed' },
|
|
365
|
+
finished: { reset: 'idle' },
|
|
366
|
+
failed: { retry: 'processing', reset: 'idle' }
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
#tasks = $state([]);
|
|
370
|
+
|
|
371
|
+
// Derived progress calculation
|
|
372
|
+
#progress = $derived.by(() => {
|
|
373
|
+
let completed = 0;
|
|
374
|
+
for (const task of this.#tasks) {
|
|
375
|
+
if (task.completed) completed++;
|
|
376
|
+
}
|
|
377
|
+
return { completed, total: this.#tasks.length };
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
constructor() {
|
|
381
|
+
// onexit: Handle cleanup when leaving states
|
|
382
|
+
this.#machine.onexit = (state) => {
|
|
383
|
+
switch (state) {
|
|
384
|
+
case 'processing':
|
|
385
|
+
this.#cancelTasks(); // Cancel ongoing tasks if interrupted
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// onenter: Handle immediate state actions
|
|
391
|
+
this.#machine.onenter = (state) => {
|
|
392
|
+
switch (state) {
|
|
393
|
+
case 'processing':
|
|
394
|
+
this.#startAllTasks(); // Start processing immediately
|
|
395
|
+
break;
|
|
396
|
+
case 'finished':
|
|
397
|
+
this.#notifyComplete(); // Cleanup/notify when done
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
this.state = state;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// $effect: Monitor reactive completion
|
|
404
|
+
$effect(() => {
|
|
405
|
+
if (this.#machine.current === 'processing') {
|
|
406
|
+
const { completed, total } = this.#progress;
|
|
407
|
+
if (completed === total && total > 0) {
|
|
408
|
+
this.#machine.send('complete');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Basic Component Integration
|
|
417
|
+
|
|
418
|
+
```javascript
|
|
419
|
+
// Component.svelte
|
|
420
|
+
<script>
|
|
421
|
+
import { FiniteStateMachine } from '$lib/state/classes.js';
|
|
422
|
+
|
|
423
|
+
const machine = new FiniteStateMachine('idle', {
|
|
424
|
+
idle: { start: 'loading' },
|
|
425
|
+
loading: { complete: 'loaded', error: 'error' },
|
|
426
|
+
loaded: { reset: 'idle' },
|
|
427
|
+
error: { retry: 'loading', reset: 'idle' }
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Reactive state updates
|
|
431
|
+
$effect(() => {
|
|
432
|
+
console.log('State changed to:', machine.current);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Handle state-specific actions
|
|
436
|
+
machine.onexit = (state) => {
|
|
437
|
+
switch (state) {
|
|
438
|
+
case 'loading':
|
|
439
|
+
// Cancel any ongoing requests
|
|
440
|
+
cancelRequests();
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
machine.onenter = (state) => {
|
|
446
|
+
switch (state) {
|
|
447
|
+
case 'loading':
|
|
448
|
+
loadData();
|
|
449
|
+
break;
|
|
450
|
+
case 'error':
|
|
451
|
+
showErrorMessage();
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
</script>
|
|
456
|
+
|
|
457
|
+
<button onclick={() => machine.send('start')}>
|
|
458
|
+
{machine.current === 'loading' ? 'Loading...' : 'Start'}
|
|
459
|
+
</button>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
## Error Handling
|
|
463
|
+
|
|
464
|
+
Invalid transitions are handled gracefully with console warnings:
|
|
465
|
+
|
|
466
|
+
```javascript
|
|
467
|
+
const machine = new FiniteStateMachine('idle', {
|
|
468
|
+
idle: { start: 'running' },
|
|
469
|
+
running: { stop: 'idle' }
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
machine.send('invalidEvent'); // Logs warning, stays in current state
|
|
473
|
+
console.log(machine.current); // Still 'idle'
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Best Practices
|
|
477
|
+
|
|
478
|
+
1. **Clear state names**: Use descriptive state names like `loading`, `error`,
|
|
479
|
+
`authenticated` rather than generic ones
|
|
480
|
+
2. **Minimal state count**: Keep the number of states manageable
|
|
481
|
+
3. **Explicit transitions**: Define all valid transitions explicitly
|
|
482
|
+
4. **Error states**: Include error states and recovery paths
|
|
483
|
+
5. **Callback cleanup**: Use exit callbacks to clean up resources
|
|
484
|
+
|
|
485
|
+
## Examples
|
|
486
|
+
|
|
487
|
+
### Loading State Machine
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
const loader = new FiniteStateMachine('idle', {
|
|
491
|
+
idle: {
|
|
492
|
+
_enter: () => console.log('Ready to load'),
|
|
493
|
+
load: 'loading'
|
|
494
|
+
},
|
|
495
|
+
loading: {
|
|
496
|
+
_enter: () => console.log('Loading started'),
|
|
497
|
+
_exit: () => console.log('Loading finished'),
|
|
498
|
+
success: 'loaded',
|
|
499
|
+
error: 'error'
|
|
500
|
+
},
|
|
501
|
+
loaded: {
|
|
502
|
+
_enter: () => console.log('Data loaded successfully'),
|
|
503
|
+
reload: 'loading',
|
|
504
|
+
reset: 'idle'
|
|
505
|
+
},
|
|
506
|
+
error: {
|
|
507
|
+
_enter: ({ args }) => console.error('Load failed:', args[0]),
|
|
508
|
+
retry: 'loading',
|
|
509
|
+
reset: 'idle'
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Authentication State Machine
|
|
515
|
+
|
|
516
|
+
```javascript
|
|
517
|
+
const auth = new FiniteStateMachine('anonymous', {
|
|
518
|
+
anonymous: {
|
|
519
|
+
login: 'authenticating'
|
|
520
|
+
},
|
|
521
|
+
authenticating: {
|
|
522
|
+
_enter: () => console.log('Checking credentials...'),
|
|
523
|
+
success: 'authenticated',
|
|
524
|
+
failure: 'anonymous'
|
|
525
|
+
},
|
|
526
|
+
authenticated: {
|
|
527
|
+
_enter: (meta) => console.log('Welcome!'),
|
|
528
|
+
logout: 'anonymous',
|
|
529
|
+
expire: 'anonymous'
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## Testing
|
|
535
|
+
|
|
536
|
+
The state machine includes comprehensive tests covering:
|
|
537
|
+
|
|
538
|
+
- Basic state transitions
|
|
539
|
+
- Enter/exit callbacks
|
|
540
|
+
- Invalid transitions
|
|
541
|
+
- Same-state transitions
|
|
542
|
+
- Immediate state access
|
|
543
|
+
- Callback execution order
|
|
544
|
+
|
|
545
|
+
See `FiniteStateMachine.test.js` for detailed examples.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default as FiniteStateMachine } from "./FiniteStateMachine.svelte";
|
|
1
|
+
export { default as FiniteStateMachine } from "./FiniteStateMachine.svelte.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default as FiniteStateMachine } from './FiniteStateMachine.svelte';
|
|
1
|
+
export { default as FiniteStateMachine } from './FiniteStateMachine.svelte.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata object passed to state transition callbacks
|
|
3
|
+
*/
|
|
4
|
+
export type StateTransitionMetadata = {
|
|
5
|
+
/**
|
|
6
|
+
* - The state being exited
|
|
7
|
+
*/
|
|
8
|
+
from: string;
|
|
9
|
+
/**
|
|
10
|
+
* - The state being entered
|
|
11
|
+
*/
|
|
12
|
+
to: string;
|
|
13
|
+
/**
|
|
14
|
+
* - The event that triggered the transition
|
|
15
|
+
*/
|
|
16
|
+
event: string;
|
|
17
|
+
/**
|
|
18
|
+
* - Arguments passed to the send() method
|
|
19
|
+
*/
|
|
20
|
+
args: any[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Callback function called when entering a state
|
|
24
|
+
*/
|
|
25
|
+
export type OnEnterCallback = (arg0: string, arg1: StateTransitionMetadata) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Callback function called when exiting a state
|
|
28
|
+
*/
|
|
29
|
+
export type OnExitCallback = (arg0: string, arg1: StateTransitionMetadata) => void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Type definitions for FiniteStateMachine
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Metadata object passed to state transition callbacks
|
|
7
|
+
*
|
|
8
|
+
* @typedef {object} StateTransitionMetadata
|
|
9
|
+
* @property {string} from - The state being exited
|
|
10
|
+
* @property {string} to - The state being entered
|
|
11
|
+
* @property {string} event - The event that triggered the transition
|
|
12
|
+
* @property {any[]} args - Arguments passed to the send() method
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Callback function called when entering a state
|
|
17
|
+
*
|
|
18
|
+
* @typedef {function(string, StateTransitionMetadata): void} OnEnterCallback
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Callback function called when exiting a state
|
|
23
|
+
*
|
|
24
|
+
* @typedef {function(string, StateTransitionMetadata): void} OnExitCallback
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Export types for external use (this is just for JSDoc, no actual exports needed)
|
|
28
|
+
export {};
|
|
@@ -30,49 +30,31 @@ export default class LoadingStateMachine extends FiniteStateMachine {
|
|
|
30
30
|
/** @type {Error|null} */
|
|
31
31
|
#error = null;
|
|
32
32
|
|
|
33
|
-
/** @type {(( state: string )=>void)|null} */
|
|
34
|
-
onenter = null;
|
|
35
|
-
|
|
36
33
|
constructor() {
|
|
37
|
-
let superCalled = false;
|
|
38
34
|
super(STATE_INITIAL, {
|
|
39
35
|
[STATE_INITIAL]: {
|
|
40
|
-
_enter: () => {
|
|
41
|
-
if (superCalled) {
|
|
42
|
-
this.onenter?.(STATE_INITIAL);
|
|
43
|
-
}
|
|
44
|
-
superCalled = true;
|
|
45
|
-
},
|
|
46
36
|
[LOAD]: STATE_LOADING
|
|
47
37
|
},
|
|
48
38
|
[STATE_LOADING]: {
|
|
49
|
-
_enter: () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
39
|
+
// _enter: () => {
|
|
40
|
+
// console.log('LoadingStateMachine: enter LOADING');
|
|
41
|
+
// },
|
|
53
42
|
[CANCEL]: STATE_CANCELLED,
|
|
54
43
|
[ERROR]: STATE_ERROR,
|
|
55
44
|
[LOADED]: STATE_LOADED
|
|
56
45
|
},
|
|
57
46
|
[STATE_LOADED]: {
|
|
58
|
-
_enter: () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
},
|
|
47
|
+
// _enter: () => {
|
|
48
|
+
// console.log('LoadingStateMachine: enter LOADED');
|
|
49
|
+
// },
|
|
62
50
|
[LOAD]: STATE_LOADING,
|
|
63
51
|
[UNLOAD]: STATE_UNLOADING
|
|
64
52
|
},
|
|
65
53
|
[STATE_UNLOADING]: {
|
|
66
|
-
_enter: () => {
|
|
67
|
-
this.onenter?.(STATE_UNLOADING);
|
|
68
|
-
},
|
|
69
54
|
[ERROR]: STATE_ERROR,
|
|
70
55
|
[INITIAL]: STATE_INITIAL
|
|
71
56
|
},
|
|
72
57
|
[STATE_CANCELLED]: {
|
|
73
|
-
_enter: () => {
|
|
74
|
-
this.onenter?.(STATE_CANCELLED);
|
|
75
|
-
},
|
|
76
58
|
[LOAD]: STATE_LOADING,
|
|
77
59
|
[UNLOAD]: STATE_UNLOADING
|
|
78
60
|
},
|
|
@@ -90,10 +72,8 @@ export default class LoadingStateMachine extends FiniteStateMachine {
|
|
|
90
72
|
this.#error = new Error('The state machine entered STATE_ERROR');
|
|
91
73
|
}
|
|
92
74
|
}
|
|
93
|
-
|
|
94
|
-
this.onenter?.(STATE_CANCELLED);
|
|
95
75
|
},
|
|
96
|
-
|
|
76
|
+
_exit: () => {
|
|
97
77
|
this.#error = null;
|
|
98
78
|
},
|
|
99
79
|
[LOAD]: STATE_LOADING,
|