@hkdigital/lib-core 0.4.23 → 0.4.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/logging/internal/adapters/pino.d.ts +7 -3
  2. package/dist/logging/internal/adapters/pino.js +200 -67
  3. package/dist/logging/internal/transports/pretty-transport.d.ts +17 -0
  4. package/dist/logging/internal/transports/pretty-transport.js +104 -0
  5. package/dist/logging/internal/transports/test-transport.d.ts +19 -0
  6. package/dist/logging/internal/transports/test-transport.js +79 -0
  7. package/dist/network/loaders/audio/AudioScene.svelte.js +2 -2
  8. package/dist/network/loaders/image/ImageScene.svelte.js +18 -28
  9. package/dist/network/states/NetworkLoader.svelte.d.ts +7 -1
  10. package/dist/network/states/NetworkLoader.svelte.js +17 -4
  11. package/dist/services/README.md +23 -0
  12. package/dist/services/service-base/ServiceBase.d.ts +12 -8
  13. package/dist/services/service-base/ServiceBase.js +8 -6
  14. package/dist/state/classes.d.ts +0 -2
  15. package/dist/state/classes.js +0 -2
  16. package/dist/state/{classes → machines}/finite-state-machine/FiniteStateMachine.svelte.d.ts +13 -7
  17. package/dist/state/machines/finite-state-machine/FiniteStateMachine.svelte.js +181 -0
  18. package/dist/state/machines/finite-state-machine/README.md +547 -0
  19. package/dist/state/machines/finite-state-machine/constants.d.ts +13 -0
  20. package/dist/state/machines/finite-state-machine/constants.js +15 -0
  21. package/dist/state/{classes → machines}/finite-state-machine/index.d.ts +2 -1
  22. package/dist/state/{classes → machines}/finite-state-machine/index.js +2 -1
  23. package/dist/state/machines/finite-state-machine/typedef.d.ts +29 -0
  24. package/dist/state/machines/finite-state-machine/typedef.js +28 -0
  25. package/dist/state/machines/loading-state-machine/LoadingStateMachine.svelte.d.ts +22 -0
  26. package/dist/state/{classes → machines}/loading-state-machine/LoadingStateMachine.svelte.js +34 -29
  27. package/dist/state/machines/loading-state-machine/README.md +592 -0
  28. package/dist/state/{classes → machines}/loading-state-machine/constants.d.ts +2 -0
  29. package/dist/state/{classes → machines}/loading-state-machine/constants.js +2 -0
  30. package/dist/state/machines/typedef.d.ts +1 -0
  31. package/dist/state/machines/typedef.js +1 -0
  32. package/dist/state/machines.d.ts +2 -0
  33. package/dist/state/machines.js +2 -0
  34. package/dist/state/typedef.d.ts +1 -0
  35. package/dist/state/typedef.js +1 -0
  36. package/dist/ui/components/game-box/README.md +245 -0
  37. package/package.json +1 -1
  38. package/dist/logging/internal/adapters/pino.js__ +0 -260
  39. package/dist/state/classes/finite-state-machine/FiniteStateMachine.svelte.js +0 -133
  40. package/dist/state/classes/loading-state-machine/LoadingStateMachine.svelte.d.ts +0 -12
  41. /package/dist/state/{classes → machines}/loading-state-machine/index.d.ts +0 -0
  42. /package/dist/state/{classes → machines}/loading-state-machine/index.js +0 -0
@@ -10,6 +10,7 @@ import {
10
10
  STATE_LOADED,
11
11
  STATE_CANCELLED,
12
12
  STATE_ERROR,
13
+ STATE_TIMEOUT,
13
14
 
14
15
  // > Signals
15
16
  INITIAL,
@@ -17,7 +18,8 @@ import {
17
18
  CANCEL,
18
19
  ERROR,
19
20
  LOADED,
20
- UNLOAD
21
+ UNLOAD,
22
+ TIMEOUT
21
23
  } from './constants.js';
22
24
 
23
25
  /**
@@ -30,49 +32,36 @@ export default class LoadingStateMachine extends FiniteStateMachine {
30
32
  /** @type {Error|null} */
31
33
  #error = null;
32
34
 
33
- /** @type {(( state: string )=>void)|null} */
34
- onenter = null;
35
-
36
35
  constructor() {
37
- let superCalled = false;
38
36
  super(STATE_INITIAL, {
39
37
  [STATE_INITIAL]: {
40
- _enter: () => {
41
- if (superCalled) {
42
- this.onenter?.(STATE_INITIAL);
43
- }
44
- superCalled = true;
45
- },
46
38
  [LOAD]: STATE_LOADING
47
39
  },
48
40
  [STATE_LOADING]: {
49
- _enter: () => {
50
- // console.log('LoadingStateMachine: enter LOADING');
51
- this.onenter?.(STATE_LOADING);
52
- },
41
+ // _enter: () => {
42
+ // console.log('LoadingStateMachine: enter LOADING');
43
+ // },
53
44
  [CANCEL]: STATE_CANCELLED,
54
45
  [ERROR]: STATE_ERROR,
55
- [LOADED]: STATE_LOADED
46
+ [LOADED]: STATE_LOADED,
47
+ [TIMEOUT]: STATE_TIMEOUT
56
48
  },
57
49
  [STATE_LOADED]: {
58
- _enter: () => {
59
- // console.log('LoadingStateMachine: enter LOADED');
60
- this.onenter?.(STATE_LOADED);
61
- },
50
+ // _enter: () => {
51
+ // console.log('LoadingStateMachine: enter LOADED');
52
+ // },
62
53
  [LOAD]: STATE_LOADING,
63
54
  [UNLOAD]: STATE_UNLOADING
64
55
  },
65
56
  [STATE_UNLOADING]: {
66
- _enter: () => {
67
- this.onenter?.(STATE_UNLOADING);
68
- },
69
57
  [ERROR]: STATE_ERROR,
70
58
  [INITIAL]: STATE_INITIAL
71
59
  },
72
60
  [STATE_CANCELLED]: {
73
- _enter: () => {
74
- this.onenter?.(STATE_CANCELLED);
75
- },
61
+ [LOAD]: STATE_LOADING,
62
+ [UNLOAD]: STATE_UNLOADING
63
+ },
64
+ [STATE_TIMEOUT]: {
76
65
  [LOAD]: STATE_LOADING,
77
66
  [UNLOAD]: STATE_UNLOADING
78
67
  },
@@ -90,10 +79,8 @@ export default class LoadingStateMachine extends FiniteStateMachine {
90
79
  this.#error = new Error('The state machine entered STATE_ERROR');
91
80
  }
92
81
  }
93
-
94
- this.onenter?.(STATE_CANCELLED);
95
82
  },
96
- _leave: () => {
83
+ _exit: () => {
97
84
  this.#error = null;
98
85
  },
99
86
  [LOAD]: STATE_LOADING,
@@ -106,4 +93,22 @@ export default class LoadingStateMachine extends FiniteStateMachine {
106
93
  get error() {
107
94
  return this.#error;
108
95
  }
96
+
97
+ /**
98
+ * Transition to timeout state
99
+ * - Only valid when currently loading
100
+ * - Useful for external timeout management
101
+ */
102
+ doTimeout() {
103
+ this.send(TIMEOUT);
104
+ }
105
+
106
+ /**
107
+ * Transition to cancelled state
108
+ * - Only valid when currently loading
109
+ * - Useful for external cancellation management
110
+ */
111
+ doCancel() {
112
+ this.send(CANCEL);
113
+ }
109
114
  }
@@ -0,0 +1,592 @@
1
+ # LoadingStateMachine
2
+
3
+ A specialized finite state machine designed for managing loading operations with comprehensive state tracking and error handling.
4
+
5
+ ## Overview
6
+
7
+ `LoadingStateMachine` extends `FiniteStateMachine` to provide a standardized way to handle loading workflows. It includes predefined states for initial, loading, loaded, unloading, cancelled, and error conditions.
8
+
9
+ ## States
10
+
11
+ The machine defines seven primary states:
12
+
13
+ - **`INITIAL`** - Starting state, ready to begin loading
14
+ - **`LOADING`** - Currently loading data or resources
15
+ - **`LOADED`** - Successfully loaded, data available
16
+ - **`UNLOADING`** - Cleaning up or releasing resources
17
+ - **`CANCELLED`** - Loading operation was cancelled
18
+ - **`ERROR`** - An error occurred during loading
19
+ - **`TIMEOUT`** - Loading operation exceeded time limit
20
+
21
+ ## Events
22
+
23
+ Available events to trigger state transitions:
24
+
25
+ - **`LOAD`** - Start loading operation
26
+ - **`LOADED`** - Mark loading as complete
27
+ - **`UNLOAD`** - Begin cleanup/unloading
28
+ - **`CANCEL`** - Cancel the loading operation
29
+ - **`ERROR`** - Signal an error occurred
30
+ - **`TIMEOUT`** - Signal operation has timed out
31
+ - **`INITIAL`** - Return to initial state
32
+
33
+ ## Basic Usage
34
+
35
+ ```javascript
36
+ import { LoadingStateMachine } from '$lib/state/machines.js';
37
+ import {
38
+ // States
39
+ STATE_INITIAL,
40
+ STATE_LOADING,
41
+ STATE_LOADED,
42
+ STATE_ERROR,
43
+ // Events
44
+ LOAD,
45
+ LOADED,
46
+ ERROR,
47
+ CANCEL
48
+ } from '$lib/state/machines.js';
49
+
50
+ const loader = new LoadingStateMachine();
51
+
52
+ // Check initial state
53
+ console.log(loader.current); // STATE_INITIAL
54
+
55
+ // Start loading
56
+ loader.send(LOAD);
57
+ console.log(loader.current); // STATE_LOADING
58
+
59
+ // Complete successfully
60
+ loader.send(LOADED);
61
+ console.log(loader.current); // STATE_LOADED
62
+ ```
63
+
64
+ ## State Transitions
65
+
66
+ ### Valid Transitions
67
+
68
+ ```
69
+ INITIAL → LOADING
70
+ LOADING → LOADED | CANCELLED | ERROR | TIMEOUT
71
+ LOADED → LOADING | UNLOADING
72
+ UNLOADING → INITIAL | ERROR
73
+ CANCELLED → LOADING | UNLOADING
74
+ ERROR → LOADING | UNLOADING
75
+ TIMEOUT → LOADING | UNLOADING
76
+ ```
77
+
78
+ ### Transition Diagram
79
+
80
+ ```
81
+ ┌─────────┐
82
+ │ INITIAL │
83
+ └────┬────┘
84
+ │ LOAD
85
+
86
+ ┌─────────┐ CANCEL ┌───────────┐
87
+ │ LOADING │──────────────→│ CANCELLED │
88
+ └────┬────┘ └─────┬─────┘
89
+ │ │
90
+ LOADED│ ERROR TIMEOUT LOAD │ UNLOAD
91
+ │ │ │ │ │
92
+ ▼ ▼ ▼ ▼ ▼
93
+ ┌────────┐ ┌───────┐ ┌─────────┐ ┌──────────┐
94
+ │ LOADED │ │ ERROR │ │ TIMEOUT │ │UNLOADING │
95
+ └────┬───┘ └───┬───┘ └────┬────┘ └────┬─────┘
96
+ │ │ │ │
97
+ UNLOAD│ LOAD │ UNLOAD LOAD│ UNLOAD INITIAL│ ERROR
98
+ ▼ ▼ ▼ ▼
99
+ ┌──────────┐◄──────────────────────────┘
100
+ │UNLOADING │
101
+ └──────────┘
102
+ ```
103
+
104
+ ## onenter Callback
105
+
106
+ The `onenter` callback provides a reliable way to react to state changes, designed to work around Svelte's reactive batching behavior.
107
+
108
+ ```javascript
109
+ const loader = new LoadingStateMachine();
110
+
111
+ loader.onenter = (state) => {
112
+ switch (state) {
113
+ case STATE_LOADING:
114
+ console.log('Started loading...');
115
+ showSpinner();
116
+ break;
117
+ case STATE_LOADED:
118
+ console.log('Loading complete!');
119
+ hideSpinner();
120
+ break;
121
+ case STATE_ERROR:
122
+ console.log('Loading failed');
123
+ showError();
124
+ break;
125
+ case STATE_TIMEOUT:
126
+ console.log('Loading timed out');
127
+ showRetryOption();
128
+ break;
129
+ }
130
+ };
131
+ ```
132
+
133
+ ### Why onenter?
134
+
135
+ The `onenter` callback was added because there might be issues when the stats machine is combined with Svelte's `$effect`. This callback ensures you can reliably respond to every state change.
136
+
137
+ ```javascript
138
+ // Svelte $effect might miss rapid transitions
139
+ $effect(() => {
140
+ console.log(loader.current); // May only see final state
141
+ });
142
+
143
+ // onenter catches every transition
144
+ loader.onenter = (state) => {
145
+ console.log(state); // Sees every state change
146
+ };
147
+ ```
148
+
149
+ ## Error Handling
150
+
151
+ The machine includes sophisticated error handling with automatic error object creation:
152
+
153
+ ```javascript
154
+ const loader = new LoadingStateMachine();
155
+
156
+ // Send error with Error object
157
+ loader.send(ERROR, new Error('Network failed'));
158
+ console.log(loader.error.message); // 'Network failed'
159
+
160
+ // Send error with error details
161
+ loader.send(ERROR, { error: 'Timeout occurred' });
162
+ console.log(loader.error.message); // 'Timeout occurred'
163
+
164
+ // Send error with just a string
165
+ loader.send(ERROR, { error: new Error('Parse error') });
166
+ console.log(loader.error.message); // 'Parse error'
167
+ ```
168
+
169
+ The `error` getter provides access to the last error:
170
+
171
+ ```javascript
172
+ if (loader.current === 'error') {
173
+ console.error('Loading failed:', loader.error);
174
+ }
175
+ ```
176
+
177
+ ## Timeout Handling
178
+
179
+ The machine supports timeout functionality for managing loading operations that exceed expected duration:
180
+
181
+ ```javascript
182
+ const loader = new LoadingStateMachine();
183
+
184
+ // External timeout management
185
+ const timeoutId = setTimeout(() => {
186
+ if (loader.current === STATE_LOADING) {
187
+ loader.doTimeout(); // Transitions to STATE_TIMEOUT
188
+ }
189
+ }, 10000); // 10 second timeout
190
+
191
+ loader.onenter = (state) => {
192
+ switch (state) {
193
+ case STATE_TIMEOUT:
194
+ console.log('Loading timed out');
195
+ showRetryButton();
196
+ break;
197
+ case STATE_LOADED:
198
+ clearTimeout(timeoutId); // Cancel timeout on success
199
+ break;
200
+ }
201
+ };
202
+ ```
203
+
204
+ ### doTimeout Method
205
+
206
+ Use `doTimeout()` to manually trigger a timeout transition:
207
+
208
+ ```javascript
209
+ // Only works when in STATE_LOADING
210
+ loader.doTimeout(); // Sends TIMEOUT signal
211
+ ```
212
+
213
+ ## Integration with Svelte Reactivity
214
+
215
+ LoadingStateMachine inherits the `onenter` callback and Svelte reactivity integration patterns from [FiniteStateMachine](../finite-state-machine/README.md#integration-with-svelte-reactivity).
216
+
217
+ ### Quick Reference: Loading State Pattern
218
+
219
+ **✅ Use `onenter` for immediate loading actions:**
220
+ ```javascript
221
+ const loader = new LoadingStateMachine();
222
+
223
+ loader.onenter = (state) => {
224
+ switch (state) {
225
+ case STATE_LOADING:
226
+ this.#startLoading(); // Start async process immediately
227
+ break;
228
+ case STATE_LOADED:
229
+ this.#cleanup(); // Cleanup when complete
230
+ break;
231
+ case STATE_ERROR:
232
+ this.#handleError(); // Handle error state
233
+ break;
234
+ }
235
+ this.state = state; // Update public state
236
+ };
237
+ ```
238
+
239
+ **✅ Use `$effect` for reactive completion monitoring:**
240
+ ```javascript
241
+ // Monitor derived/computed values and trigger transitions when conditions are met
242
+ $effect(() => {
243
+ if (loader.current === STATE_LOADING) {
244
+ // Check completion based on reactive derived values
245
+ if (this.#sourcesLoaded === this.#numberOfSources && this.#numberOfSources > 0) {
246
+ loader.send(LOADED); // Trigger transition when condition met
247
+ }
248
+ }
249
+ });
250
+ ```
251
+
252
+ ### Loading-Specific Example
253
+
254
+ ```javascript
255
+ export default class MultiSourceLoader {
256
+ #loader = new LoadingStateMachine();
257
+ #sources = $state([]);
258
+
259
+ // Derived progress calculation
260
+ #progress = $derived.by(() => {
261
+ let completed = 0;
262
+ for (const source of this.#sources) {
263
+ if (source.loaded) completed++;
264
+ }
265
+ return { completed, total: this.#sources.length };
266
+ });
267
+
268
+ constructor() {
269
+ // Handle immediate loading state actions
270
+ this.#loader.onenter = (state) => {
271
+ switch (state) {
272
+ case STATE_LOADING:
273
+ this.#startAllSources();
274
+ break;
275
+ case STATE_LOADED:
276
+ this.#notifyComplete();
277
+ break;
278
+ case STATE_ERROR:
279
+ this.#handleLoadError();
280
+ break;
281
+ case STATE_TIMEOUT:
282
+ this.#handleTimeout();
283
+ break;
284
+ }
285
+ this.state = state;
286
+ };
287
+
288
+ // Monitor reactive completion
289
+ $effect(() => {
290
+ if (this.#loader.current === STATE_LOADING) {
291
+ const { completed, total } = this.#progress;
292
+ if (completed === total && total > 0) {
293
+ this.#loader.send(LOADED);
294
+ }
295
+ }
296
+ });
297
+ }
298
+ }
299
+ ```
300
+
301
+ **📖 For complete documentation** of the `onenter` callback and Svelte reactivity patterns, see the [FiniteStateMachine README](../finite-state-machine/README.md#integration-with-svelte-reactivity).
302
+
303
+ ### Basic Component Integration
304
+
305
+ ```javascript
306
+ // LoadingComponent.svelte
307
+ <script>
308
+ import { LoadingStateMachine } from '$lib/state/machines.js';
309
+ import {
310
+ STATE_LOADING,
311
+ STATE_LOADED,
312
+ STATE_ERROR,
313
+ LOAD,
314
+ LOADED,
315
+ ERROR,
316
+ CANCEL
317
+ } from '$lib/state/machines.js';
318
+
319
+ const loader = new LoadingStateMachine();
320
+ let data = $state(null);
321
+
322
+ loader.onenter = (state) => {
323
+ if (state === STATE_LOADING) {
324
+ loadData();
325
+ }
326
+ };
327
+
328
+ async function loadData() {
329
+ try {
330
+ const response = await fetch('/api/data');
331
+ data = await response.json();
332
+ loader.send(LOADED);
333
+ } catch (error) {
334
+ loader.send(ERROR, error);
335
+ }
336
+ }
337
+ </script>
338
+
339
+ <button onclick={() => loader.send(LOAD)} disabled={loader.current === STATE_LOADING}>
340
+ {#if loader.current === STATE_LOADING}
341
+ Loading...
342
+ {:else}
343
+ Load Data
344
+ {/if}
345
+ </button>
346
+
347
+ {#if loader.current === STATE_LOADED}
348
+ <div>Data loaded successfully!</div>
349
+ {:else if loader.current === STATE_ERROR}
350
+ <div>Error: {loader.error.message}</div>
351
+ {/if}
352
+ ```
353
+
354
+ ### Advanced Component with Cancellation
355
+
356
+ ```javascript
357
+ <script>
358
+ import { LoadingStateMachine } from '$lib/state/machines.js';
359
+ import {
360
+ STATE_INITIAL,
361
+ STATE_LOADING,
362
+ STATE_LOADED,
363
+ STATE_CANCELLED,
364
+ STATE_ERROR,
365
+ LOAD,
366
+ LOADED,
367
+ ERROR,
368
+ CANCEL
369
+ } from '$lib/state/machines.js';
370
+
371
+ const loader = new LoadingStateMachine();
372
+ let abortController = null;
373
+
374
+ loader.onenter = (state) => {
375
+ switch (state) {
376
+ case STATE_LOADING:
377
+ startLoad();
378
+ break;
379
+ case STATE_CANCELLED:
380
+ abortController?.abort();
381
+ break;
382
+ }
383
+ };
384
+
385
+ async function startLoad() {
386
+ abortController = new AbortController();
387
+
388
+ try {
389
+ const response = await fetch('/api/slow-data', {
390
+ signal: abortController.signal
391
+ });
392
+ const data = await response.json();
393
+ loader.send(LOADED);
394
+ } catch (error) {
395
+ if (error.name === 'AbortError') {
396
+ // Request was cancelled, machine already in cancelled state
397
+ } else {
398
+ loader.send(ERROR, error);
399
+ }
400
+ }
401
+ }
402
+ </script>
403
+
404
+ <div>
405
+ {#if loader.current === STATE_INITIAL}
406
+ <button onclick={() => loader.send(LOAD)}>Start Loading</button>
407
+ {:else if loader.current === STATE_LOADING}
408
+ <button onclick={() => loader.send(CANCEL)}>Cancel Loading</button>
409
+ <div>Loading data...</div>
410
+ {:else if loader.current === STATE_LOADED}
411
+ <div>Data loaded successfully!</div>
412
+ <button onclick={() => loader.send(LOAD)}>Reload</button>
413
+ {:else if loader.current === STATE_CANCELLED}
414
+ <div>Loading cancelled</div>
415
+ <button onclick={() => loader.send(LOAD)}>Try Again</button>
416
+ {:else if loader.current === STATE_ERROR}
417
+ <div>Error: {loader.error.message}</div>
418
+ <button onclick={() => loader.send(LOAD)}>Retry</button>
419
+ {/if}
420
+ </div>
421
+ ```
422
+
423
+ ## Best Practices
424
+
425
+ ### 1. Use onenter for Side Effects
426
+
427
+ ```javascript
428
+ // ✅ Good - use onenter for reliable side effects
429
+ loader.onenter = (state) => {
430
+ if (state === STATE_LOADING) {
431
+ analytics.track('loading_started');
432
+ }
433
+ };
434
+
435
+ // ❌ Avoid - $effect may miss rapid transitions
436
+ $effect(() => {
437
+ if (loader.current === STATE_LOADING) {
438
+ analytics.track('loading_started'); // May not fire
439
+ }
440
+ });
441
+ ```
442
+
443
+ ### 2. Handle All Error States
444
+
445
+ ```javascript
446
+ loader.onenter = (state) => {
447
+ switch (state) {
448
+ case STATE_ERROR:
449
+ showErrorToast(loader.error.message);
450
+ logError(loader.error);
451
+ break;
452
+ case STATE_CANCELLED:
453
+ showMessage('Operation cancelled');
454
+ break;
455
+ }
456
+ };
457
+ ```
458
+
459
+ ### 3. Implement Proper Cleanup
460
+
461
+ ```javascript
462
+ loader.onenter = (state) => {
463
+ switch (state) {
464
+ case STATE_LOADING:
465
+ showProgressBar();
466
+ break;
467
+ case STATE_LOADED:
468
+ case STATE_ERROR:
469
+ case STATE_CANCELLED:
470
+ hideProgressBar();
471
+ break;
472
+ case STATE_UNLOADING:
473
+ cleanup();
474
+ break;
475
+ }
476
+ };
477
+ ```
478
+
479
+ ### 4. Use Constants for Events
480
+
481
+ ```javascript
482
+ import {
483
+ LOAD,
484
+ LOADED,
485
+ ERROR
486
+ } from '$lib/state/machines.js';
487
+
488
+ // ✅ Good - type safe and refactor friendly
489
+ loader.send(LOAD);
490
+
491
+ // ❌ Avoid - prone to typos
492
+ loader.send('load');
493
+ ```
494
+
495
+ ## Common Patterns
496
+
497
+ ### Resource Loading with Cleanup
498
+
499
+ ```javascript
500
+ const resourceLoader = new LoadingStateMachine();
501
+ let resource = null;
502
+
503
+ resourceLoader.onenter = async (state) => {
504
+ switch (state) {
505
+ case STATE_LOADING:
506
+ try {
507
+ resource = await loadResource();
508
+ resourceLoader.send(LOADED);
509
+ } catch (error) {
510
+ resourceLoader.send(ERROR, error);
511
+ }
512
+ break;
513
+
514
+ case STATE_UNLOADING:
515
+ if (resource) {
516
+ await resource.cleanup();
517
+ resource = null;
518
+ }
519
+ resourceLoader.send(INITIAL);
520
+ break;
521
+ }
522
+ };
523
+ ```
524
+
525
+ ### Retry Logic
526
+
527
+ ```javascript
528
+ const retryLoader = new LoadingStateMachine();
529
+ let retryCount = 0;
530
+ const maxRetries = 3;
531
+
532
+ retryLoader.onenter = async (state) => {
533
+ switch (state) {
534
+ case STATE_LOADING:
535
+ try {
536
+ await performLoad();
537
+ retryCount = 0; // Reset on success
538
+ retryLoader.send(LOADED);
539
+ } catch (error) {
540
+ retryLoader.send(ERROR, error);
541
+ }
542
+ break;
543
+
544
+ case STATE_ERROR:
545
+ if (retryCount < maxRetries) {
546
+ retryCount++;
547
+ setTimeout(() => {
548
+ retryLoader.send(LOAD);
549
+ }, 1000 * retryCount); // Exponential backoff
550
+ }
551
+ break;
552
+ }
553
+ };
554
+ ```
555
+
556
+ ## Testing
557
+
558
+ The LoadingStateMachine includes comprehensive tests covering:
559
+
560
+ - All state transitions
561
+ - onenter callback functionality
562
+ - Error handling with different error types
563
+ - Null safety when no callback is set
564
+ - Dynamic callback changes
565
+
566
+ See `LoadingStateMachine.test.js` for detailed examples.
567
+
568
+ ## Constants
569
+
570
+ Import state and event constants from the constants file:
571
+
572
+ ```javascript
573
+ import {
574
+ // States
575
+ STATE_INITIAL,
576
+ STATE_LOADING,
577
+ STATE_LOADED,
578
+ STATE_UNLOADING,
579
+ STATE_CANCELLED,
580
+ STATE_ERROR,
581
+ STATE_TIMEOUT,
582
+
583
+ // Events
584
+ LOAD,
585
+ LOADED,
586
+ UNLOAD,
587
+ CANCEL,
588
+ ERROR,
589
+ INITIAL,
590
+ TIMEOUT
591
+ } from './constants.js';
592
+ ```
@@ -4,9 +4,11 @@ export const STATE_UNLOADING: "unloading";
4
4
  export const STATE_LOADED: "loaded";
5
5
  export const STATE_CANCELLED: "cancelled";
6
6
  export const STATE_ERROR: "error";
7
+ export const STATE_TIMEOUT: "timeout";
7
8
  export const INITIAL: "initial";
8
9
  export const LOAD: "load";
9
10
  export const CANCEL: "cancel";
10
11
  export const ERROR: "error";
11
12
  export const LOADED: "loaded";
12
13
  export const UNLOAD: "unload";
14
+ export const TIMEOUT: "timeout";