@hkdigital/lib-core 0.4.62 → 0.4.63

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 {}
@@ -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,8 @@ export default class SceneBase {
39
42
  // return this.state === STATE_ABORTED;
40
43
  // });
41
44
 
42
-
45
+ /** @type {((progress:SceneLoadingProgress)=>void)[]} */
46
+ #preloadListeners = [];
43
47
 
44
48
  /** @type {SceneLoadingProgress} */
45
49
  progress = $derived.by(() => {
@@ -85,7 +89,6 @@ export default class SceneBase {
85
89
  };
86
90
  });
87
91
 
88
-
89
92
  /**
90
93
  * Construct SceneBase
91
94
  */
@@ -110,25 +113,48 @@ export default class SceneBase {
110
113
  }
111
114
  });
112
115
 
113
-
114
116
  $effect(() => {
115
117
  if (this.state === STATE_LOADING) {
116
-
117
118
  // Check if any source failed during loading
118
119
  const sources = this.sources;
119
120
 
120
121
  for (const source of sources) {
121
122
  const loader = this.getLoaderFromSource(source);
122
123
  if (loader.state === STATE_ERROR) {
123
- this.#state.send(ERROR, loader.error || new Error('Source loading failed'));
124
+ this.#state.send(
125
+ ERROR,
126
+ loader.error || new Error('Source loading failed')
127
+ );
124
128
  break;
125
129
  }
126
130
  }
127
131
  }
132
+ });
128
133
 
134
+ $effect(() => {
135
+ this.#updatePreloadProgressListeners(this.progress);
129
136
  });
130
137
  } // end constructor
131
138
 
139
+ /**
140
+ * Call preload progress listeners
141
+ *
142
+ * @param {SceneLoadingProgress} progress
143
+ */
144
+ #updatePreloadProgressListeners(progress) {
145
+ for (const fn of this.#preloadListeners) {
146
+ try {
147
+ fn(progress);
148
+ } catch (e) {
149
+ throw new DetailedError(
150
+ 'Error in progress listener',
151
+ null,
152
+ /** @type {Error} */ (e)
153
+ );
154
+ }
155
+ }
156
+ }
157
+
132
158
  /* ==== Abstract methods - must be implemented by subclasses */
133
159
 
134
160
  /**
@@ -181,7 +207,6 @@ export default class SceneBase {
181
207
  * Object with promise that resolves when loaded and abort function
182
208
  */
183
209
  preload({ timeoutMs = 10000, onProgress } = {}) {
184
-
185
210
  /** @type {number|NodeJS.Timeout|null} */
186
211
  let timeoutId = null;
187
212
 
@@ -189,17 +214,17 @@ export default class SceneBase {
189
214
  let progressIntervalId = null;
190
215
 
191
216
  let isAborted = false;
192
-
193
- /** @type {SceneLoadingProgress|null} */
194
- let lastSentProgress = null;
195
217
 
196
218
  const abort = () => {
197
219
  if (isAborted) return;
198
220
  isAborted = true;
199
221
 
200
- if (timeoutId) {
201
- clearTimeout(timeoutId);
202
- timeoutId = null;
222
+ // Remove progress listener
223
+ if (onProgress) {
224
+ const index = this.#preloadListeners.indexOf(onProgress);
225
+ if (index >= 0) {
226
+ this.#preloadListeners.splice(index, 1);
227
+ }
203
228
  }
204
229
 
205
230
  if (progressIntervalId) {
@@ -211,51 +236,64 @@ export default class SceneBase {
211
236
  };
212
237
 
213
238
  const promise = new Promise((resolve, reject) => {
214
- // Set up progress tracking with polling
239
+ // Set up progress tracking with reactive listener
215
240
  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
241
+ this.#preloadListeners.push(onProgress);
223
242
  }
224
243
 
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
- }
244
+ // // Set up progress tracking with polling (fallback if reactive doesn't work)
245
+ // if (onProgress) {
246
+ // progressIntervalId = setInterval(() => {
247
+ // if (!isAborted && this.state === STATE_LOADING) {
248
+ // const currentProgress = this.progress;
249
+ // onProgress(currentProgress);
250
+ // }
251
+ // }, 50); // Poll every 50ms
252
+ // }
253
+
254
+ // // Set up progress tracking with polling (fallback if reactive doesn't work)
255
+ // if (onProgress) {
256
+ // progressIntervalId = setInterval(() => {
257
+ // if (!isAborted && this.state === STATE_LOADING) {
258
+ // const currentProgress = this.progress;
259
+ // onProgress(currentProgress);
260
+ // }
261
+ // }, 50); // Poll every 50ms
262
+ // }
232
263
 
233
264
  // Start loading
234
265
  this.load();
235
266
 
236
- // Wait for completion with extended timeout
237
- const waitTimeout = Math.max(timeoutMs + 1000, 2000);
267
+ // Wait for completion with timeout
268
+ // 0 means no timeout, but we still need a reasonable value for waitForState
269
+ const waitTimeout = timeoutMs > 0 ? timeoutMs : 120000;
270
+
238
271
  waitForState(() => {
239
- return this.loaded ||
240
- this.state === STATE_ABORTED ||
241
- this.state === STATE_ERROR;
272
+ return (
273
+ this.loaded ||
274
+ this.state === STATE_ABORTED ||
275
+ this.state === STATE_ERROR
276
+ );
242
277
  }, waitTimeout)
243
278
  .then(() => {
244
- if (timeoutId) {
245
- clearTimeout(timeoutId);
246
- timeoutId = null;
279
+ // Remove progress listener
280
+ if (onProgress) {
281
+ const index = this.#preloadListeners.indexOf(onProgress);
282
+ if (index >= 0) {
283
+ this.#preloadListeners.splice(index, 1);
284
+ }
285
+
247
286
  }
248
287
 
288
+ // Cleanup polling (fallback if reactive doesn't work)
249
289
  if (progressIntervalId) {
250
290
  clearInterval(progressIntervalId);
251
291
  progressIntervalId = null;
252
-
253
- if (onProgress) {
292
+
293
+ // Send final progress when loading completes (for polling fallback)
294
+ if (onProgress && this.loaded) {
254
295
  const finalProgress = this.progress;
255
- // Always send final progress when loading completes
256
- if (this.loaded) {
257
- onProgress(finalProgress);
258
- }
296
+ onProgress(finalProgress);
259
297
  }
260
298
  }
261
299
 
@@ -269,7 +307,34 @@ export default class SceneBase {
269
307
  reject(new Error(`Preload failed: unexpected state ${this.state}`));
270
308
  }
271
309
  })
272
- .catch(reject);
310
+ .catch((error) => {
311
+ // Handle timeout errors from waitForState
312
+ if (error instanceof TimeoutError) {
313
+ abort();
314
+ reject(new Error(`Preload timed out after ${timeoutMs}ms`));
315
+ } else {
316
+ reject(error);
317
+ }
318
+ })
319
+ .finally(() => {
320
+ // Send final progress update regardless of success/failure
321
+ if (onProgress) {
322
+ const finalProgress = this.progress;
323
+ onProgress(finalProgress);
324
+
325
+ // Remove progress listener
326
+ const index = this.#preloadListeners.indexOf(onProgress);
327
+ if (index >= 0) {
328
+ this.#preloadListeners.splice(index, 1);
329
+ }
330
+ }
331
+
332
+ // // Cleanup polling (fallback if reactive doesn't work)
333
+ // if (progressIntervalId) {
334
+ // clearInterval(progressIntervalId);
335
+ // progressIntervalId = null;
336
+ // }
337
+ });
273
338
  });
274
339
 
275
340
  return { promise, abort };
@@ -296,7 +361,7 @@ export default class SceneBase {
296
361
  const loader = this.getLoaderFromSource(source);
297
362
  loader.abort();
298
363
  }
299
-
364
+
300
365
  // Defer ABORTED transition to avoid re-entrant state machine calls
301
366
  setTimeout(() => {
302
367
  // 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.63",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"