@hkdigital/lib-core 0.4.46 → 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.
@@ -12,13 +12,14 @@ pnpm add -D pino-pretty
12
12
 
13
13
  ```javascript
14
14
  import { createServerLogger,
15
- createClientLogger } from '@hkdigital/lib-core/logging/index.js';
15
+ createClientLogger,
16
+ DEBUG } from '@hkdigital/lib-core/logging/index.js';
16
17
 
17
18
  // Server-side logging (uses pino)
18
- const serverLogger = createServerLogger('app');
19
+ const serverLogger = createServerLogger('app', DEBUG);
19
20
 
20
21
  // Client-side logging (uses console)
21
- const clientLogger = createClientLogger('app');
22
+ const clientLogger = createClientLogger('app', DEBUG);
22
23
 
23
24
  // Log at different levels
24
25
  serverLogger.debug('Debug info', { data: 'details' });
@@ -32,13 +33,13 @@ serverLogger.error('Error message', { error: new Error('Something went wrong') }
32
33
  ### Server-side logging (src/hooks.server.js)
33
34
 
34
35
  ```javascript
35
- import { createServerLogger } from '@hkdigital/lib-core/logging/index.js';
36
+ import { createServerLogger, DEBUG } from '@hkdigital/lib-core/logging/index.js';
36
37
 
37
38
  let logger;
38
39
 
39
40
  // Initialize server logging and services
40
41
  export async function init() {
41
- logger = createServerLogger('server');
42
+ logger = createServerLogger('server', DEBUG);
42
43
 
43
44
  try {
44
45
  logger.info('Initializing server');
@@ -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
- if (this.#state.current === STATE_LOADING) {
94
- const { sourcesLoaded, numberOfSources } = this.#progress;
91
+ if (this.state === STATE_LOADING) {
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.current === 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
115
 
126
- state.onenter = (currentState) => {
127
- if (currentState === STATE_LOADING) {
128
- this.#startLoading();
129
- } else if (currentState === STATE_ABORTING) {
130
- this.#startAbort();
131
- }
132
-
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
  */
@@ -289,8 +257,10 @@ export default class SceneBase {
289
257
  /* ==== Internal methods */
290
258
 
291
259
  #startLoading() {
292
- for (const source of this.sources) {
260
+ for (let i = 0; i < this.sources.length; i++) {
261
+ const source = this.sources[i];
293
262
  const loader = this.getLoaderFromSource(source);
263
+
294
264
  loader.load();
295
265
  }
296
266
  }
@@ -300,5 +270,13 @@ export default class SceneBase {
300
270
  const loader = this.getLoaderFromSource(source);
301
271
  loader.abort();
302
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);
303
281
  }
304
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,21 +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());
33
- _state = new LoadingStateMachine();
32
+ #state = $state(new LoadingStateMachine());
34
33
 
35
- state = $derived.by(() => {
36
- return this._state.current;
37
- });
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);
38
47
 
39
48
  initial = $derived.by(() => {
40
- return this._state.current === STATE_INITIAL;
49
+ return this.state === STATE_INITIAL;
41
50
  });
42
51
 
43
52
  loaded = $derived.by(() => {
44
- return this._state.current === STATE_LOADED;
53
+ return this.state === STATE_LOADED;
45
54
  });
46
55
 
56
+ // aborted = $derived.by(() => {
57
+ // return this.state === STATE_ABORTED;
58
+ // });
59
+
47
60
  /** @type {string|null} */
48
61
  _url = null;
49
62
 
@@ -87,37 +100,24 @@ export default class NetworkLoader {
87
100
 
88
101
  this._url = url;
89
102
 
90
- const state = this._state;
91
- // const progress = this.progress;
103
+ this.#state.onenter = (currentState) => {
104
+ this.state = currentState;
92
105
 
93
- this._state.onenter = () => {
94
- switch (state.current) {
106
+ switch (currentState) {
95
107
  case STATE_LOADING:
96
108
  {
97
- // console.log('**** NetworkLoader:loading');
98
109
  this.#load();
99
110
  }
100
111
  break;
101
112
 
102
113
  case STATE_UNLOADING:
103
114
  {
104
- // console.log('NetworkLoader:unloading');
105
115
  this.#unload();
106
116
  }
107
117
  break;
108
118
 
109
119
  case STATE_LOADED:
110
120
  {
111
- // console.debug('NetworkLoader:loaded', $state.snapshot(state));
112
-
113
- // setTimeout(() => {
114
- // console.debug(
115
- // 'NetworkLoader:loaded',
116
- // $state.snapshot(state),
117
- // progress
118
- // );
119
- // }, 500);
120
-
121
121
  // Abort function is no longer needed
122
122
  this._abortLoading = null;
123
123
  }
@@ -125,13 +125,21 @@ export default class NetworkLoader {
125
125
 
126
126
  case STATE_ABORTING:
127
127
  {
128
- // console.log('NetworkLoader:aborting');
129
128
  if (this._abortLoading) {
130
129
  this._abortLoading();
131
130
  this._abortLoading = null;
132
131
  }
133
- // Transition to aborted state after abort completes
134
- 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);
135
143
  }
136
144
  break;
137
145
 
@@ -148,15 +156,14 @@ export default class NetworkLoader {
148
156
  * Start loading all network data
149
157
  */
150
158
  load() {
151
- // console.debug('NetworkLoader: load() called');
152
- this._state.send(LOAD);
159
+ this.#state.send(LOAD);
153
160
  }
154
161
 
155
162
  /**
156
163
  * Unoad all network data
157
164
  */
158
165
  unload() {
159
- this._state.send(UNLOAD);
166
+ this.#state.send(UNLOAD);
160
167
  }
161
168
 
162
169
  /**
@@ -165,7 +172,7 @@ export default class NetworkLoader {
165
172
  * - Aborts network requests and transitions to STATE_ABORTING
166
173
  */
167
174
  abort() {
168
- this._state.send(ABORT);
175
+ this.#state.send(ABORT);
169
176
  }
170
177
 
171
178
  /**
@@ -273,10 +280,7 @@ export default class NetworkLoader {
273
280
  */
274
281
  async #load() {
275
282
  try {
276
- // console.log('>>>> NetworkLoader:#load', this._url);
277
-
278
283
  if (this._abortLoading) {
279
- // console.log('Abort loading');
280
284
  this._abortLoading();
281
285
  this._abortLoading = null;
282
286
  }
@@ -300,9 +304,6 @@ export default class NetworkLoader {
300
304
 
301
305
  this._headers = response.headers;
302
306
 
303
- // console.log('headers', this._headers);
304
- // console.log('response', response);
305
-
306
307
  const { bufferPromise, abort: abortLoadBody } = loadResponseBuffer(
307
308
  response,
308
309
  ({ bytesLoaded, size }) => {
@@ -318,11 +319,15 @@ export default class NetworkLoader {
318
319
 
319
320
  this._buffer = await bufferPromise;
320
321
 
321
- // console.debug('#load', this._buffer, this._bytesLoaded);
322
+ // if (this._size === 0 && this._buffer) {
323
+ // // Fallback: if size was unknown (0),
324
+ // // => set it to actual buffer size when loaded
325
+ // this._size = this._buffer.byteLength;
326
+ // }
322
327
 
323
- this._state.send(LOADED);
328
+ this.#state.send(LOADED);
324
329
  } catch (e) {
325
- this._state.send(ERROR, e);
330
+ this.#state.send(ERROR, e);
326
331
  }
327
332
  }
328
333
 
@@ -342,9 +347,9 @@ export default class NetworkLoader {
342
347
  this._headers = null;
343
348
  this._buffer = null;
344
349
 
345
- this._state.send(INITIAL);
350
+ this.#state.send(INITIAL);
346
351
  } catch (e) {
347
- this._state.send(ERROR, e);
352
+ this.#state.send(ERROR, e);
348
353
  }
349
354
  }
350
355
  } // end class
@@ -296,10 +296,10 @@ Forward all service log events to a centralised logger:
296
296
 
297
297
  ```javascript
298
298
  import { ServiceManager } from '$lib/services/index.js';
299
- import { createServerLogger } from '$lib/logging/index.js';
299
+ import { createServerLogger, DEBUG } from '$lib/logging/index.js';
300
300
 
301
301
  const manager = new ServiceManager();
302
- const logger = createServerLogger('SystemLogger');
302
+ const logger = createServerLogger('SystemLogger', DEBUG);
303
303
 
304
304
  // Listen to all log events and forward them to the logger
305
305
  const unsubscribe = manager.onLogEvent((logEvent) => {
@@ -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
- checkLoop();
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.46",
3
+ "version": "0.4.48",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"