@hkdigital/lib-core 0.4.62 → 0.4.64

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.
@@ -1,9 +1,3 @@
1
- export class TypeOrValueError extends Error {
2
- }
3
- export class InternalError extends Error {
4
- }
5
- export class InternalEventOrLogError extends Error {
6
- }
7
1
  export class DetailedError extends Error {
8
2
  /**
9
3
  * @param {string} [message]
@@ -16,3 +10,11 @@ export class DetailedError extends Error {
16
10
  } | null;
17
11
  cause: unknown;
18
12
  }
13
+ export class TypeOrValueError extends Error {
14
+ }
15
+ export class InternalError extends Error {
16
+ }
17
+ export class InternalEventOrLogError extends Error {
18
+ }
19
+ export class TimeoutError extends DetailedError {
20
+ }
@@ -1,9 +1,3 @@
1
- export class TypeOrValueError extends Error {}
2
-
3
- export class InternalError extends Error {}
4
-
5
- export class InternalEventOrLogError extends Error {}
6
-
7
1
  export class DetailedError extends Error
8
2
  {
9
3
  /**
@@ -28,3 +22,11 @@ export class DetailedError extends Error
28
22
  }
29
23
  }
30
24
  }
25
+
26
+ export class TypeOrValueError extends Error {}
27
+
28
+ export class InternalError extends Error {}
29
+
30
+ export class InternalEventOrLogError extends Error {}
31
+
32
+ export class TimeoutError extends DetailedError {}
@@ -464,5 +464,8 @@ export function parseFunctionName(frame) {
464
464
  }
465
465
  }
466
466
 
467
+ // Strip Firefox function naming artifacts like "</timeoutId<"
468
+ functionName = functionName.replace(/<\/[^<>]*</g, '');
469
+
467
470
  return functionName;
468
471
  }
@@ -1,10 +1,9 @@
1
- export class ResponseError extends Error {
1
+ export class ResponseError extends DetailedError {
2
2
  }
3
- export class AuthenticationError extends Error {
3
+ export class AuthenticationError extends DetailedError {
4
4
  }
5
- export class BadRequestError extends Error {
5
+ export class BadRequestError extends DetailedError {
6
6
  }
7
- export class AbortError extends Error {
8
- }
9
- export class TimeoutError extends Error {
7
+ export class AbortError extends DetailedError {
10
8
  }
9
+ import { DetailedError } from '../../generic/errors.js';
@@ -1,9 +1,11 @@
1
- export class ResponseError extends Error {}
1
+ import { DetailedError } from '../../generic/errors.js';
2
2
 
3
- export class AuthenticationError extends Error {}
3
+ export class ResponseError extends DetailedError {}
4
4
 
5
- export class BadRequestError extends Error {}
5
+ export class AuthenticationError extends DetailedError {}
6
6
 
7
- export class AbortError extends Error {}
7
+ export class BadRequestError extends DetailedError {}
8
8
 
9
- export class TimeoutError extends Error {}
9
+ export class AbortError extends DetailedError {}
10
+
11
+ // @note import TimeoutError from '../../generic/errors.js';
@@ -11,7 +11,8 @@ import {
11
11
  import { APPLICATION_JSON } from '../../constants/mime/application.js';
12
12
  import { CONTENT_TYPE } from '../../constants/http/headers.js';
13
13
 
14
- import { AbortError, TimeoutError } from '../errors/api.js';
14
+ import { AbortError } from '../errors/api.js';
15
+ import { TimeoutError } from '../../generic/errors.js';
15
16
 
16
17
  import * as expect from '../../util/expect.js';
17
18
 
@@ -93,9 +93,8 @@ export async function jsonGet(options) {
93
93
  } catch (e) {
94
94
  throw new ResponseError(
95
95
  `Failed to JSON decode server response from [${decodeURI(url.href)}]`,
96
- {
97
- cause: e
98
- }
96
+ null,
97
+ e
99
98
  );
100
99
  }
101
100
 
@@ -206,9 +205,8 @@ export async function jsonPost(options) {
206
205
  } catch (e) {
207
206
  throw new ResponseError(
208
207
  `Failed to JSON decode server response from [${decodeURI(url.href)}]`,
209
- {
210
- cause: e
211
- }
208
+ null,
209
+ e
212
210
  );
213
211
  }
214
212
 
@@ -322,9 +320,8 @@ export async function jsonPut(options) {
322
320
  } catch (e) {
323
321
  throw new ResponseError(
324
322
  `Failed to JSON decode server response from [${decodeURI(url.href)}]`,
325
- {
326
- cause: e
327
- }
323
+ null,
324
+ e
328
325
  );
329
326
  }
330
327
 
@@ -437,9 +434,8 @@ export async function jsonPatch(options) {
437
434
  } catch (e) {
438
435
  throw new ResponseError(
439
436
  `Failed to JSON decode server response from [${decodeURI(url.href)}]`,
440
- {
441
- cause: e
442
- }
437
+ null,
438
+ e
443
439
  );
444
440
  }
445
441
 
@@ -529,9 +525,8 @@ export async function jsonDelete(options) {
529
525
  } catch (e) {
530
526
  throw new ResponseError(
531
527
  `Failed to JSON decode server response from [${decodeURI(url.href)}]`,
532
- {
533
- cause: e
534
- }
528
+ null,
529
+ e
535
530
  );
536
531
  }
537
532
 
@@ -91,7 +91,8 @@ export async function expectResponseOk(response, url) {
91
91
  throw new ResponseError(
92
92
  `Server returned - ${response.status} ${response.statusText} ` +
93
93
  `[url=${href(url)}]`,
94
- { cause: error }
94
+ null,
95
+ error
95
96
  );
96
97
  }
97
98
 
@@ -186,9 +187,8 @@ export async function waitForAndCheckResponse(responsePromise, url) {
186
187
  } else if (e instanceof TypeError || response?.ok === false) {
187
188
  throw new ResponseError(
188
189
  `A network error occurred for request [${href(url)}]`,
189
- {
190
- cause: e
191
- }
190
+ null,
191
+ e
192
192
  );
193
193
  } else {
194
194
  throw e;
@@ -1,3 +1,5 @@
1
+ import { DetailedError } from '../../../generic/errors.js';
2
+
1
3
  import { LoadingStateMachine } from '../../../state/machines.js';
2
4
 
3
5
  import {
@@ -15,6 +17,7 @@ import {
15
17
  } from '../../../state/machines.js';
16
18
 
17
19
  import { waitForState } from '../../../util/svelte.js';
20
+ import { TimeoutError } from '../../../generic/errors.js';
18
21
 
19
22
  /** @typedef {import('./typedef.js').SceneLoadingProgress} SceneLoadingProgress */
20
23
 
@@ -39,7 +42,11 @@ export default class SceneBase {
39
42
  // return this.state === STATE_ABORTED;
40
43
  // });
41
44
 
45
+ /** @type {((progress:SceneLoadingProgress)=>void)[]} */
46
+ #preloadListeners = [];
42
47
 
48
+ /** @type {SceneLoadingProgress|null} */
49
+ #lastReportedProgress = null;
43
50
 
44
51
  /** @type {SceneLoadingProgress} */
45
52
  progress = $derived.by(() => {
@@ -85,7 +92,6 @@ export default class SceneBase {
85
92
  };
86
93
  });
87
94
 
88
-
89
95
  /**
90
96
  * Construct SceneBase
91
97
  */
@@ -110,25 +116,61 @@ export default class SceneBase {
110
116
  }
111
117
  });
112
118
 
113
-
114
119
  $effect(() => {
115
120
  if (this.state === STATE_LOADING) {
116
-
117
121
  // Check if any source failed during loading
118
122
  const sources = this.sources;
119
123
 
120
124
  for (const source of sources) {
121
125
  const loader = this.getLoaderFromSource(source);
122
126
  if (loader.state === STATE_ERROR) {
123
- this.#state.send(ERROR, loader.error || new Error('Source loading failed'));
127
+ this.#state.send(
128
+ ERROR,
129
+ loader.error || new Error('Source loading failed')
130
+ );
124
131
  break;
125
132
  }
126
133
  }
127
134
  }
135
+ });
128
136
 
137
+ $effect(() => {
138
+ this.#updatePreloadProgressListeners(this.progress);
129
139
  });
130
140
  } // end constructor
131
141
 
142
+ /**
143
+ * Call preload progress listeners (with deduplication)
144
+ *
145
+ * @param {SceneLoadingProgress} progress
146
+ */
147
+ #updatePreloadProgressListeners(progress) {
148
+ // Skip if progress hasn't actually changed
149
+ if (this.#lastReportedProgress &&
150
+ this.#lastReportedProgress.totalBytesLoaded === progress.totalBytesLoaded &&
151
+ this.#lastReportedProgress.totalSize === progress.totalSize &&
152
+ this.#lastReportedProgress.sourcesLoaded === progress.sourcesLoaded &&
153
+ this.#lastReportedProgress.numberOfSources === progress.numberOfSources &&
154
+ this.#lastReportedProgress.percentageLoaded === progress.percentageLoaded) {
155
+ return;
156
+ }
157
+
158
+ // Update last reported progress
159
+ this.#lastReportedProgress = { ...progress };
160
+
161
+ for (const fn of this.#preloadListeners) {
162
+ try {
163
+ fn(progress);
164
+ } catch (e) {
165
+ throw new DetailedError(
166
+ 'Error in progress listener',
167
+ null,
168
+ /** @type {Error} */ (e)
169
+ );
170
+ }
171
+ }
172
+ }
173
+
132
174
  /* ==== Abstract methods - must be implemented by subclasses */
133
175
 
134
176
  /**
@@ -181,7 +223,6 @@ export default class SceneBase {
181
223
  * Object with promise that resolves when loaded and abort function
182
224
  */
183
225
  preload({ timeoutMs = 10000, onProgress } = {}) {
184
-
185
226
  /** @type {number|NodeJS.Timeout|null} */
186
227
  let timeoutId = null;
187
228
 
@@ -189,17 +230,17 @@ export default class SceneBase {
189
230
  let progressIntervalId = null;
190
231
 
191
232
  let isAborted = false;
192
-
193
- /** @type {SceneLoadingProgress|null} */
194
- let lastSentProgress = null;
195
233
 
196
234
  const abort = () => {
197
235
  if (isAborted) return;
198
236
  isAborted = true;
199
237
 
200
- if (timeoutId) {
201
- clearTimeout(timeoutId);
202
- timeoutId = null;
238
+ // Remove progress listener
239
+ if (onProgress) {
240
+ const index = this.#preloadListeners.indexOf(onProgress);
241
+ if (index >= 0) {
242
+ this.#preloadListeners.splice(index, 1);
243
+ }
203
244
  }
204
245
 
205
246
  if (progressIntervalId) {
@@ -211,51 +252,64 @@ export default class SceneBase {
211
252
  };
212
253
 
213
254
  const promise = new Promise((resolve, reject) => {
214
- // Set up progress tracking with polling
255
+ // Set up progress tracking with reactive listener
215
256
  if (onProgress) {
216
- progressIntervalId = setInterval(() => {
217
- if (!isAborted && this.state === STATE_LOADING) {
218
- const currentProgress = this.progress;
219
- lastSentProgress = currentProgress;
220
- onProgress(currentProgress);
221
- }
222
- }, 50); // Poll every 50ms
257
+ this.#preloadListeners.push(onProgress);
223
258
  }
224
259
 
225
- // Set up timeout
226
- if (timeoutMs > 0) {
227
- timeoutId = setTimeout(() => {
228
- abort();
229
- reject(new Error(`Preload timed out after ${timeoutMs}ms`));
230
- }, timeoutMs);
231
- }
260
+ // // Set up progress tracking with polling (fallback if reactive doesn't work)
261
+ // if (onProgress) {
262
+ // progressIntervalId = setInterval(() => {
263
+ // if (!isAborted && this.state === STATE_LOADING) {
264
+ // const currentProgress = this.progress;
265
+ // onProgress(currentProgress);
266
+ // }
267
+ // }, 50); // Poll every 50ms
268
+ // }
269
+
270
+ // // Set up progress tracking with polling (fallback if reactive doesn't work)
271
+ // if (onProgress) {
272
+ // progressIntervalId = setInterval(() => {
273
+ // if (!isAborted && this.state === STATE_LOADING) {
274
+ // const currentProgress = this.progress;
275
+ // onProgress(currentProgress);
276
+ // }
277
+ // }, 50); // Poll every 50ms
278
+ // }
232
279
 
233
280
  // Start loading
234
281
  this.load();
235
282
 
236
- // Wait for completion with extended timeout
237
- const waitTimeout = Math.max(timeoutMs + 1000, 2000);
283
+ // Wait for completion with timeout
284
+ // 0 means no timeout, but we still need a reasonable value for waitForState
285
+ const waitTimeout = timeoutMs > 0 ? timeoutMs : 120000;
286
+
238
287
  waitForState(() => {
239
- return this.loaded ||
240
- this.state === STATE_ABORTED ||
241
- this.state === STATE_ERROR;
288
+ return (
289
+ this.loaded ||
290
+ this.state === STATE_ABORTED ||
291
+ this.state === STATE_ERROR
292
+ );
242
293
  }, waitTimeout)
243
294
  .then(() => {
244
- if (timeoutId) {
245
- clearTimeout(timeoutId);
246
- timeoutId = null;
295
+ // Remove progress listener
296
+ if (onProgress) {
297
+ const index = this.#preloadListeners.indexOf(onProgress);
298
+ if (index >= 0) {
299
+ this.#preloadListeners.splice(index, 1);
300
+ }
301
+
247
302
  }
248
303
 
304
+ // Cleanup polling (fallback if reactive doesn't work)
249
305
  if (progressIntervalId) {
250
306
  clearInterval(progressIntervalId);
251
307
  progressIntervalId = null;
252
-
253
- if (onProgress) {
308
+
309
+ // Send final progress when loading completes (for polling fallback)
310
+ if (onProgress && this.loaded) {
254
311
  const finalProgress = this.progress;
255
- // Always send final progress when loading completes
256
- if (this.loaded) {
257
- onProgress(finalProgress);
258
- }
312
+ onProgress(finalProgress);
259
313
  }
260
314
  }
261
315
 
@@ -269,7 +323,34 @@ export default class SceneBase {
269
323
  reject(new Error(`Preload failed: unexpected state ${this.state}`));
270
324
  }
271
325
  })
272
- .catch(reject);
326
+ .catch((error) => {
327
+ // Handle timeout errors from waitForState
328
+ if (error instanceof TimeoutError) {
329
+ abort();
330
+ reject(new Error(`Preload timed out after ${timeoutMs}ms`));
331
+ } else {
332
+ reject(error);
333
+ }
334
+ })
335
+ .finally(() => {
336
+ // Send final progress update regardless of success/failure
337
+ if (onProgress) {
338
+ const finalProgress = this.progress;
339
+ onProgress(finalProgress);
340
+
341
+ // Remove progress listener
342
+ const index = this.#preloadListeners.indexOf(onProgress);
343
+ if (index >= 0) {
344
+ this.#preloadListeners.splice(index, 1);
345
+ }
346
+ }
347
+
348
+ // // Cleanup polling (fallback if reactive doesn't work)
349
+ // if (progressIntervalId) {
350
+ // clearInterval(progressIntervalId);
351
+ // progressIntervalId = null;
352
+ // }
353
+ });
273
354
  });
274
355
 
275
356
  return { promise, abort };
@@ -296,7 +377,7 @@ export default class SceneBase {
296
377
  const loader = this.getLoaderFromSource(source);
297
378
  loader.abort();
298
379
  }
299
-
380
+
300
381
  // Defer ABORTED transition to avoid re-entrant state machine calls
301
382
  setTimeout(() => {
302
383
  // Only transition to ABORTED if still in ABORTING state
@@ -1,5 +1,7 @@
1
1
  import { tick } from 'svelte';
2
2
 
3
+ import { TimeoutError } from '../../../generic/errors.js';
4
+
3
5
  /**
4
6
  * Waits for a state condition to be met by running the checkFn
5
7
  * function after each Svelte tick
@@ -25,7 +27,7 @@ export function waitForState(checkFn, maxWaitMs = 1000) {
25
27
  }
26
28
 
27
29
  if (Date.now() - startedAt >= maxWaitMs) {
28
- reject(new Error(`State change timeout`));
30
+ reject(new TimeoutError(`State change timeout`));
29
31
  return;
30
32
  }
31
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.4.62",
3
+ "version": "0.4.64",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"