@hkdigital/lib-core 0.4.47 → 0.4.48

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.
@@ -4,7 +4,10 @@
4
4
  */
5
5
  export default class SceneBase {
6
6
  state: string;
7
+ initial: boolean;
7
8
  loaded: boolean;
9
+ /** @type {SceneLoadingProgress} */
10
+ progress: SceneLoadingProgress;
8
11
  /**
9
12
  * Get the array of sources managed by this scene
10
13
  *
@@ -19,17 +22,6 @@ export default class SceneBase {
19
22
  * @returns {import('../../states/index.js').NetworkLoader} loader
20
23
  */
21
24
  getLoaderFromSource(source: object): import("../../states/index.js").NetworkLoader;
22
- /**
23
- * Get scene loading progress
24
- */
25
- get progress(): import("./typedef.js").SceneLoadingProgress;
26
- /**
27
- * Get scene abort progress
28
- */
29
- get abortProgress(): {
30
- sourcesAborted: number;
31
- numberOfSources: number;
32
- };
33
25
  /**
34
26
  * Start loading all sources
35
27
  */
@@ -27,12 +27,22 @@ export default class SceneBase {
27
27
  // @note this exported state is set by onenter
28
28
  state = $state(STATE_INITIAL);
29
29
 
30
+ initial = $derived.by(() => {
31
+ return this.state === STATE_INITIAL;
32
+ });
33
+
30
34
  loaded = $derived.by(() => {
31
35
  return this.state === STATE_LOADED;
32
36
  });
33
37
 
38
+ // aborted = $derived.by(() => {
39
+ // return this.state === STATE_ABORTED;
40
+ // });
41
+
42
+
43
+
34
44
  /** @type {SceneLoadingProgress} */
35
- #progress = $derived.by(() => {
45
+ progress = $derived.by(() => {
36
46
  let totalSize = 0;
37
47
  let totalBytesLoaded = 0;
38
48
  let sourcesLoaded = 0;
@@ -62,36 +72,24 @@ export default class SceneBase {
62
72
  };
63
73
  });
64
74
 
65
- #abortProgress = $derived.by(() => {
66
- let sourcesAborted = 0;
67
- const sources = this.sources;
68
- const numberOfSources = sources.length;
69
-
70
- for (let j = 0; j < numberOfSources; j++) {
71
- const source = sources[j];
72
- const loader = this.getLoaderFromSource(source);
73
- const loaderState = loader.state;
74
-
75
- if (loaderState === STATE_ABORTED || loaderState === STATE_ERROR) {
76
- sourcesAborted++;
77
- }
78
- }
79
-
80
- return {
81
- sourcesAborted,
82
- numberOfSources
83
- };
84
- });
85
75
 
86
76
  /**
87
77
  * Construct SceneBase
88
78
  */
89
79
  constructor() {
90
- const state = this.#state;
80
+ this.#state.onenter = (currentState) => {
81
+ if (currentState === STATE_LOADING) {
82
+ this.#startLoading();
83
+ } else if (currentState === STATE_ABORTING) {
84
+ this.#startAbort();
85
+ }
86
+
87
+ this.state = currentState;
88
+ };
91
89
 
92
90
  $effect(() => {
93
91
  if (this.state === STATE_LOADING) {
94
- const { sourcesLoaded, numberOfSources } = this.#progress;
92
+ const { sourcesLoaded, numberOfSources } = this.progress;
95
93
 
96
94
  if (sourcesLoaded === numberOfSources && numberOfSources > 0) {
97
95
  this.#state.send(LOADED);
@@ -99,20 +97,13 @@ export default class SceneBase {
99
97
  }
100
98
  });
101
99
 
102
- $effect(() => {
103
- if (this.state === STATE_ABORTING) {
104
- const { sourcesAborted, numberOfSources } = this.#abortProgress;
105
-
106
- if (sourcesAborted === numberOfSources && numberOfSources > 0) {
107
- this.#state.send(ABORTED);
108
- }
109
- }
110
- });
111
100
 
112
101
  $effect(() => {
113
- if (this.#state.current === STATE_LOADING) {
102
+ if (this.state === STATE_LOADING) {
103
+
114
104
  // Check if any source failed during loading
115
105
  const sources = this.sources;
106
+
116
107
  for (const source of sources) {
117
108
  const loader = this.getLoaderFromSource(source);
118
109
  if (loader.state === STATE_ERROR) {
@@ -121,18 +112,9 @@ export default class SceneBase {
121
112
  }
122
113
  }
123
114
  }
124
- });
125
-
126
- state.onenter = (currentState) => {
127
- if (currentState === STATE_LOADING) {
128
- this.#startLoading();
129
- } else if (currentState === STATE_ABORTING) {
130
- this.#startAbort();
131
- }
132
115
 
133
- this.state = currentState;
134
- };
135
- }
116
+ });
117
+ } // end constructor
136
118
 
137
119
  /* ==== Abstract methods - must be implemented by subclasses */
138
120
 
@@ -159,20 +141,6 @@ export default class SceneBase {
159
141
 
160
142
  /* ==== Common loader interface */
161
143
 
162
- /**
163
- * Get scene loading progress
164
- */
165
- get progress() {
166
- return this.#progress;
167
- }
168
-
169
- /**
170
- * Get scene abort progress
171
- */
172
- get abortProgress() {
173
- return this.#abortProgress;
174
- }
175
-
176
144
  /**
177
145
  * Start loading all sources
178
146
  */
@@ -302,5 +270,13 @@ export default class SceneBase {
302
270
  const loader = this.getLoaderFromSource(source);
303
271
  loader.abort();
304
272
  }
273
+
274
+ // Defer ABORTED transition to avoid re-entrant state machine calls
275
+ setTimeout(() => {
276
+ // Only transition to ABORTED if still in ABORTING state
277
+ if (this.#state.current === STATE_ABORTING) {
278
+ this.#state.send(ABORTED);
279
+ }
280
+ }, 0);
305
281
  }
306
282
  }
@@ -13,8 +13,7 @@ export default class NetworkLoader {
13
13
  constructor({ url }: {
14
14
  url: string;
15
15
  });
16
- _state: LoadingStateMachine;
17
- state: any;
16
+ state: string;
18
17
  initial: boolean;
19
18
  loaded: boolean;
20
19
  /** @type {string|null} */
@@ -94,4 +93,3 @@ export default class NetworkLoader {
94
93
  getObjectURL(): string;
95
94
  #private;
96
95
  }
97
- import { LoadingStateMachine } from '../../state/machines.js';
@@ -29,20 +29,34 @@ import { ERROR_NOT_LOADED, ERROR_TRANSFERRED } from './constants.js';
29
29
  * - Loaded data can be transferred to an AudioBufferSourceNode
30
30
  */
31
31
  export default class NetworkLoader {
32
- _state = $state(new LoadingStateMachine());
32
+ #state = $state(new LoadingStateMachine());
33
33
 
34
- state = $derived.by(() => {
35
- return this._state.current;
36
- });
34
+ // state = $derived.by(() => {
35
+ // return this.#state.current;
36
+ // });
37
+
38
+ // initial = $derived.by(() => {
39
+ // return this.#state.current === STATE_INITIAL;
40
+ // });
41
+
42
+ // loaded = $derived.by(() => {
43
+ // return this.#state.current === STATE_LOADED;
44
+ // });
45
+
46
+ state = $state(STATE_INITIAL);
37
47
 
38
48
  initial = $derived.by(() => {
39
- return this._state.current === STATE_INITIAL;
49
+ return this.state === STATE_INITIAL;
40
50
  });
41
51
 
42
52
  loaded = $derived.by(() => {
43
- return this._state.current === STATE_LOADED;
53
+ return this.state === STATE_LOADED;
44
54
  });
45
55
 
56
+ // aborted = $derived.by(() => {
57
+ // return this.state === STATE_ABORTED;
58
+ // });
59
+
46
60
  /** @type {string|null} */
47
61
  _url = null;
48
62
 
@@ -86,11 +100,10 @@ export default class NetworkLoader {
86
100
 
87
101
  this._url = url;
88
102
 
89
- const state = this._state;
90
- // const progress = this.progress;
103
+ this.#state.onenter = (currentState) => {
104
+ this.state = currentState;
91
105
 
92
- this._state.onenter = (currentState) => {
93
- switch (state.current) {
106
+ switch (currentState) {
94
107
  case STATE_LOADING:
95
108
  {
96
109
  this.#load();
@@ -116,8 +129,17 @@ export default class NetworkLoader {
116
129
  this._abortLoading();
117
130
  this._abortLoading = null;
118
131
  }
119
- // Transition to aborted state after abort completes
120
- this._state.send(ABORTED);
132
+
133
+ //
134
+ // _abortLoading has been called (is set)
135
+ // => Transition to state ABORTED (deferred to avoid re-entrant call)
136
+ //
137
+ setTimeout(() => {
138
+ // Only transition to ABORTED if still in ABORTING state
139
+ if (this.#state.current === STATE_ABORTING) {
140
+ this.#state.send(ABORTED);
141
+ }
142
+ }, 0);
121
143
  }
122
144
  break;
123
145
 
@@ -134,14 +156,14 @@ export default class NetworkLoader {
134
156
  * Start loading all network data
135
157
  */
136
158
  load() {
137
- this._state.send(LOAD);
159
+ this.#state.send(LOAD);
138
160
  }
139
161
 
140
162
  /**
141
163
  * Unoad all network data
142
164
  */
143
165
  unload() {
144
- this._state.send(UNLOAD);
166
+ this.#state.send(UNLOAD);
145
167
  }
146
168
 
147
169
  /**
@@ -150,7 +172,7 @@ export default class NetworkLoader {
150
172
  * - Aborts network requests and transitions to STATE_ABORTING
151
173
  */
152
174
  abort() {
153
- this._state.send(ABORT);
175
+ this.#state.send(ABORT);
154
176
  }
155
177
 
156
178
  /**
@@ -303,9 +325,9 @@ export default class NetworkLoader {
303
325
  // this._size = this._buffer.byteLength;
304
326
  // }
305
327
 
306
- this._state.send(LOADED);
328
+ this.#state.send(LOADED);
307
329
  } catch (e) {
308
- this._state.send(ERROR, e);
330
+ this.#state.send(ERROR, e);
309
331
  }
310
332
  }
311
333
 
@@ -325,9 +347,9 @@ export default class NetworkLoader {
325
347
  this._headers = null;
326
348
  this._buffer = null;
327
349
 
328
- this._state.send(INITIAL);
350
+ this.#state.send(INITIAL);
329
351
  } catch (e) {
330
- this._state.send(ERROR, e);
352
+ this.#state.send(ERROR, e);
331
353
  }
332
354
  }
333
355
  } // end class
@@ -45,6 +45,9 @@ export default class FiniteStateMachine extends EventEmitter {
45
45
  /** @type {boolean} */
46
46
  #enableConsoleWarnings = !isTestEnv;
47
47
 
48
+ /** @type {boolean} */
49
+ #isTransitioning = false;
50
+
48
51
  /**
49
52
  * Constructor
50
53
  *
@@ -81,21 +84,27 @@ export default class FiniteStateMachine extends EventEmitter {
81
84
  /** @type {TransitionData} */
82
85
  const transition = { from: this.#current, to: newState, event, args };
83
86
 
84
- // Call onexit callback before leaving current state
85
- this.onexit?.(this.#current, transition);
87
+ this.#isTransitioning = true;
88
+
89
+ try {
90
+ // Call onexit callback before leaving current state
91
+ this.onexit?.(this.#current, transition);
86
92
 
87
- // Emit EXIT event for external listeners
88
- this.emit(EXIT, { state: this.#current, transition });
93
+ // Emit EXIT event for external listeners
94
+ this.emit(EXIT, { state: this.#current, transition });
89
95
 
90
- this.#executeAction('_exit', transition);
91
- this.#current = newState;
92
- this.#executeAction('_enter', transition);
96
+ this.#executeAction('_exit', transition);
97
+ this.#current = newState;
98
+ this.#executeAction('_enter', transition);
93
99
 
94
- // Emit ENTER event for external listeners
95
- this.emit(ENTER, { state: newState, transition });
100
+ // Emit ENTER event for external listeners
101
+ this.emit(ENTER, { state: newState, transition });
96
102
 
97
- // Call onenter callback after state change
98
- this.onenter?.(newState, transition);
103
+ // Call onenter callback after state change
104
+ this.onenter?.(newState, transition);
105
+ } finally {
106
+ this.#isTransitioning = false;
107
+ }
99
108
  }
100
109
 
101
110
  /**
@@ -147,6 +156,15 @@ export default class FiniteStateMachine extends EventEmitter {
147
156
  * @param {any[]} args
148
157
  */
149
158
  send(event, ...args) {
159
+ if (this.#isTransitioning) {
160
+ throw new Error(
161
+ `Cannot send event '${event}' while state machine is transitioning. ` +
162
+ `This indicates a re-entrant call from within onenter, onexit, or ` +
163
+ `lifecycle callbacks (_enter/_exit). Consider using setTimeout() to ` +
164
+ `defer the event or restructure to avoid nested state transitions.`
165
+ );
166
+ }
167
+
150
168
  const newState = this.#executeAction(event, ...args);
151
169
 
152
170
  if (newState && newState !== this.#current) {
@@ -61,11 +61,12 @@ export default class LoadingStateMachine extends FiniteStateMachine {
61
61
  },
62
62
  [STATE_ABORTING]: {
63
63
  [ERROR]: STATE_ERROR,
64
+ [LOADED]: STATE_LOADED, // A load signal might still come during ABORT
64
65
  [ABORTED]: STATE_ABORTED
65
66
  },
66
67
  [STATE_ABORTED]: {
67
68
  [LOAD]: STATE_LOADING,
68
- [LOADED]: STATE_LOADED,
69
+ [LOADED]: STATE_LOADED, // A load signal might still come after ABORT
69
70
  [UNLOAD]: STATE_UNLOADING
70
71
  },
71
72
  [STATE_TIMEOUT]: {
@@ -30,7 +30,7 @@ export function waitForState(checkFn, maxWaitMs = 1000) {
30
30
  }
31
31
 
32
32
  await tick();
33
- setTimeout( checkLoop, 5 );
33
+ setTimeout( checkLoop, 10 );
34
34
  }
35
35
 
36
36
  checkLoop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.4.47",
3
+ "version": "0.4.48",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"