@hkdigital/lib-core 0.4.26 → 0.4.28

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.
@@ -0,0 +1,283 @@
1
+ import { LoadingStateMachine } from '../../../state/machines.js';
2
+
3
+ import {
4
+ STATE_INITIAL,
5
+ STATE_LOADING,
6
+ STATE_LOADED,
7
+ STATE_ABORTING,
8
+ STATE_ABORTED,
9
+ STATE_ERROR,
10
+ LOAD,
11
+ LOADED,
12
+ ABORT,
13
+ ABORTED
14
+ } from '../../../state/machines.js';
15
+
16
+ import { waitForState } from '../../../util/svelte.js';
17
+
18
+ /**
19
+ * Base class for scene loaders that manage collections of media sources
20
+ */
21
+ export default class SceneBase {
22
+ #state = new LoadingStateMachine();
23
+
24
+ // @note this exported state is set by onenter
25
+ state = $state(STATE_INITIAL);
26
+
27
+ loaded = $derived.by(() => {
28
+ return this.state === STATE_LOADED;
29
+ });
30
+
31
+ #progress = $derived.by(() => {
32
+ let totalSize = 0;
33
+ let totalBytesLoaded = 0;
34
+ let sourcesLoaded = 0;
35
+
36
+ const sources = this.sources;
37
+ const numberOfSources = sources.length;
38
+
39
+ for (let j = 0; j < numberOfSources; j++) {
40
+ const source = sources[j];
41
+ const loader = this.getLoaderFromSource(source);
42
+
43
+ const { bytesLoaded, size, loaded } = loader.progress;
44
+
45
+ totalSize += size;
46
+ totalBytesLoaded += bytesLoaded;
47
+
48
+ if (loaded) {
49
+ sourcesLoaded++;
50
+ }
51
+ }
52
+
53
+ return {
54
+ totalBytesLoaded,
55
+ totalSize,
56
+ sourcesLoaded,
57
+ numberOfSources
58
+ };
59
+ });
60
+
61
+ #abortProgress = $derived.by(() => {
62
+ let sourcesAborted = 0;
63
+ const sources = this.sources;
64
+ const numberOfSources = sources.length;
65
+
66
+ for (let j = 0; j < numberOfSources; j++) {
67
+ const source = sources[j];
68
+ const loader = this.getLoaderFromSource(source);
69
+ const loaderState = loader.state;
70
+
71
+ if (loaderState === STATE_ABORTED || loaderState === STATE_ERROR) {
72
+ sourcesAborted++;
73
+ }
74
+ }
75
+
76
+ return {
77
+ sourcesAborted,
78
+ numberOfSources
79
+ };
80
+ });
81
+
82
+ /**
83
+ * Construct SceneBase
84
+ */
85
+ constructor() {
86
+ const state = this.#state;
87
+
88
+ $effect(() => {
89
+ if (state.current === STATE_LOADING) {
90
+ const { sourcesLoaded, numberOfSources } = this.#progress;
91
+
92
+ if (sourcesLoaded === numberOfSources) {
93
+ this.#state.send(LOADED);
94
+ }
95
+ }
96
+ });
97
+
98
+ $effect(() => {
99
+ if (state.current === STATE_ABORTING) {
100
+ const { sourcesAborted, numberOfSources } = this.#abortProgress;
101
+
102
+ if (sourcesAborted === numberOfSources) {
103
+ this.#state.send(ABORTED);
104
+ }
105
+ }
106
+ });
107
+
108
+ state.onenter = (currentState) => {
109
+ if (currentState === STATE_LOADING) {
110
+ this.#startLoading();
111
+ } else if (currentState === STATE_ABORTING) {
112
+ this.#startAbort();
113
+ }
114
+
115
+ this.state = currentState;
116
+ };
117
+ }
118
+
119
+ /* ==== Abstract methods - must be implemented by subclasses */
120
+
121
+ /**
122
+ * Get the array of sources managed by this scene
123
+ *
124
+ * @returns {Array} Array of source objects
125
+ */
126
+ get sources() {
127
+ throw new Error('Subclass must implement sources getter');
128
+ }
129
+
130
+ /**
131
+ * Extract the loader from a source object
132
+ *
133
+ * @param {*} source
134
+ *
135
+ * @returns {*} Loader object with progress and state properties
136
+ */
137
+ // eslint-disable-next-line no-unused-vars
138
+ getLoaderFromSource(source) {
139
+ throw new Error('Subclass must implement getLoaderFromSource method');
140
+ }
141
+
142
+ /* ==== Common loader interface */
143
+
144
+ /**
145
+ * Get scene loading progress
146
+ */
147
+ get progress() {
148
+ return this.#progress;
149
+ }
150
+
151
+ /**
152
+ * Get scene abort progress
153
+ */
154
+ get abortProgress() {
155
+ return this.#abortProgress;
156
+ }
157
+
158
+ /**
159
+ * Start loading all sources
160
+ */
161
+ load() {
162
+ this.#state.send(LOAD);
163
+ }
164
+
165
+ /**
166
+ * Abort loading all sources
167
+ */
168
+ abort() {
169
+ this.#state.send(ABORT);
170
+ }
171
+
172
+ /**
173
+ * Preload all sources with progress tracking and abort capability
174
+ * - Starts loading and waits for completion
175
+ * - Supports timeout and progress callbacks
176
+ * - Returns object with promise and abort function
177
+ *
178
+ * @param {object} [options]
179
+ * @param {number} [options.timeoutMs=10000] - Timeout in milliseconds
180
+ * @param {Function} [options.onProgress] - Progress callback function
181
+ *
182
+ * @returns {object} Object with promise and abort function
183
+ * @returns {Promise<SceneBase>} returns.promise - Promise that resolves when loaded
184
+ * @returns {Function} returns.abort - Function to abort preloading
185
+ */
186
+ preload({ timeoutMs = 10000, onProgress } = {}) {
187
+ let timeoutId = null;
188
+ let progressIntervalId = null;
189
+ let isAborted = false;
190
+
191
+ const abort = () => {
192
+ if (isAborted) return;
193
+ isAborted = true;
194
+
195
+ if (timeoutId) {
196
+ clearTimeout(timeoutId);
197
+ timeoutId = null;
198
+ }
199
+
200
+ if (progressIntervalId) {
201
+ clearInterval(progressIntervalId);
202
+ progressIntervalId = null;
203
+ }
204
+
205
+ this.abort();
206
+ };
207
+
208
+ const promise = new Promise((resolve, reject) => {
209
+ // Set up progress tracking with polling
210
+ if (onProgress) {
211
+ progressIntervalId = setInterval(() => {
212
+ if (!isAborted) {
213
+ onProgress(this.progress);
214
+ }
215
+ }, 50); // Poll every 50ms
216
+ }
217
+
218
+ // Set up timeout
219
+ if (timeoutMs > 0) {
220
+ timeoutId = setTimeout(() => {
221
+ abort();
222
+ reject(new Error(`Preload timed out after ${timeoutMs}ms`));
223
+ }, timeoutMs);
224
+ }
225
+
226
+ // Start loading
227
+ this.load();
228
+
229
+ // Wait for completion with extended timeout
230
+ const waitTimeout = Math.max(timeoutMs + 1000, 2000);
231
+ waitForState(() => {
232
+ return this.loaded ||
233
+ this.state === STATE_ABORTED ||
234
+ this.state === STATE_ERROR;
235
+ }, waitTimeout)
236
+ .then(() => {
237
+ if (timeoutId) {
238
+ clearTimeout(timeoutId);
239
+ timeoutId = null;
240
+ }
241
+
242
+ if (progressIntervalId) {
243
+ clearInterval(progressIntervalId);
244
+ progressIntervalId = null;
245
+ }
246
+
247
+ if (isAborted || this.state === STATE_ABORTED) {
248
+ reject(new Error('Preload was aborted'));
249
+ } else if (this.state === STATE_ERROR) {
250
+ reject(new Error('Preload failed due to error'));
251
+ } else if (this.loaded) {
252
+ resolve(this);
253
+ } else {
254
+ reject(new Error(`Preload failed: unexpected state ${this.state}`));
255
+ }
256
+ })
257
+ .catch(reject);
258
+ });
259
+
260
+ return { promise, abort };
261
+ }
262
+
263
+ destroy() {
264
+ // TODO: disconnect all sources?
265
+ // TODO: Unload loaders?
266
+ }
267
+
268
+ /* ==== Internal methods */
269
+
270
+ #startLoading() {
271
+ for (const source of this.sources) {
272
+ const loader = this.getLoaderFromSource(source);
273
+ loader.load();
274
+ }
275
+ }
276
+
277
+ #startAbort() {
278
+ for (const source of this.sources) {
279
+ const loader = this.getLoaderFromSource(source);
280
+ loader.abort();
281
+ }
282
+ }
283
+ }
@@ -7,23 +7,21 @@
7
7
  * @property {string} label
8
8
  * @property {ImageLoader} imageLoader
9
9
  */
10
- export default class ImageScene {
11
- state: string;
12
- loaded: boolean;
10
+ export default class ImageScene extends SceneBase {
13
11
  /**
14
- * Get image scene loading progress
12
+ * Get the array of image sources managed by this scene
13
+ *
14
+ * @returns {ImageSceneSource[]}
15
15
  */
16
- get progress(): {
17
- totalBytesLoaded: number;
18
- totalSize: number;
19
- sourcesLoaded: number;
20
- numberOfSources: number;
21
- };
16
+ get sources(): ImageSceneSource[];
22
17
  /**
23
- * Start loading all image sources
18
+ * Extract the image loader from a source object
19
+ *
20
+ * @param {ImageSceneSource} source
21
+ *
22
+ * @returns {ImageLoader}
24
23
  */
25
- load(): void;
26
- destroy(): void;
24
+ getLoaderFromSource(source: ImageSceneSource): ImageLoader;
27
25
  /**
28
26
  * Add image source
29
27
  * - Uses an ImageLoader instance to load image data from network
@@ -73,4 +71,5 @@ export type ImageSceneSource = {
73
71
  label: string;
74
72
  imageLoader: ImageLoader;
75
73
  };
74
+ import SceneBase from '../base/SceneBase.svelte.js';
76
75
  import ImageLoader from './ImageLoader.svelte.js';
@@ -2,16 +2,7 @@
2
2
 
3
3
  import * as expect from '../../../util/expect.js';
4
4
 
5
- import { LoadingStateMachine } from '../../../state/machines.js';
6
-
7
- import {
8
- STATE_INITIAL,
9
- STATE_LOADING,
10
- STATE_LOADED,
11
- LOAD,
12
- LOADED
13
- } from '../../../state/machines.js';
14
-
5
+ import SceneBase from '../base/SceneBase.svelte.js';
15
6
  import ImageLoader from './ImageLoader.svelte.js';
16
7
 
17
8
  /**
@@ -25,101 +16,40 @@ import ImageLoader from './ImageLoader.svelte.js';
25
16
  * @property {ImageLoader} imageLoader
26
17
  */
27
18
 
28
- export default class ImageScene {
29
- #state = new LoadingStateMachine();
30
-
31
- // @note this exported state is set by onenter
32
- state = $state(STATE_INITIAL);
33
-
34
- loaded = $derived.by(() => {
35
- return this.state === STATE_LOADED;
36
- });
19
+ export default class ImageScene extends SceneBase {
37
20
 
38
21
  /** @type {ImageSceneSource[]} */
39
22
  #imageSources = $state([]);
40
23
 
41
- #progress = $derived.by(() => {
42
- // console.log('update progress');
43
-
44
- let totalSize = 0;
45
- let totalBytesLoaded = 0;
46
- let sourcesLoaded = 0;
47
-
48
- const sources = this.#imageSources;
49
- const numberOfSources = sources.length;
50
-
51
- for (let j = 0; j < numberOfSources; j++) {
52
- const source = sources[j];
53
- const { imageLoader } = source;
54
-
55
- const { bytesLoaded, size, loaded } = imageLoader.progress;
56
-
57
- totalSize += size;
58
- totalBytesLoaded += bytesLoaded;
59
-
60
- if (loaded) {
61
- sourcesLoaded++;
62
- }
63
- } // end for
64
-
65
- return {
66
- totalBytesLoaded,
67
- totalSize,
68
- sourcesLoaded,
69
- numberOfSources
70
- };
71
- });
72
-
73
- #sourcesLoaded = $derived( this.#progress.sourcesLoaded );
74
- #numberOfSources = $derived( this.#progress.numberOfSources );
75
24
 
76
25
  /**
77
26
  * Construct ImageScene
78
27
  */
79
28
  constructor() {
80
- const state = this.#state;
81
-
82
- $effect( () => {
83
- if (state.current === STATE_LOADING) {
84
- if (this.#sourcesLoaded === this.#numberOfSources) {
85
- // console.log(`All [${this.#numberOfSources}] sources loaded`);
86
- this.#state.send(LOADED);
87
- }
88
- }
89
- } );
90
-
91
- state.onenter = ( currentState ) => {
92
- // console.log('onenter', currentState );
93
-
94
- if(currentState === STATE_LOADING )
95
- {
96
- // console.log('ImageScene:loading');
97
- this.#startLoading();
98
- }
99
-
100
- this.state = currentState;
101
- };
29
+ super();
102
30
  }
103
31
 
104
- /* ==== Common loader interface */
32
+ /* ==== SceneBase implementation */
105
33
 
106
34
  /**
107
- * Get image scene loading progress
35
+ * Get the array of image sources managed by this scene
36
+ *
37
+ * @returns {ImageSceneSource[]}
108
38
  */
109
- get progress() {
110
- return this.#progress;
39
+ get sources() {
40
+ return this.#imageSources;
111
41
  }
112
42
 
113
43
  /**
114
- * Start loading all image sources
44
+ * Extract the image loader from a source object
45
+ *
46
+ * @param {ImageSceneSource} source
47
+ *
48
+ * @returns {ImageLoader}
115
49
  */
116
- load() {
117
- this.#state.send(LOAD);
118
- }
119
-
120
- destroy() {
121
- // TODO: disconnect all image sources?
122
- // TODO: Unload ImageLoaders?
50
+ // eslint-disable-next-line no-unused-vars
51
+ getLoaderFromSource(source) {
52
+ return source.imageLoader;
123
53
  }
124
54
 
125
55
  /* ==== Source definitions */
@@ -185,12 +115,6 @@ export default class ImageScene {
185
115
  return source.imageLoader.getObjectURL();
186
116
  }
187
117
 
188
- async #startLoading() {
189
- for (const { imageLoader } of this.#imageSources) {
190
- imageLoader.load();
191
- }
192
- }
193
-
194
118
  /* ==== Internals */
195
119
 
196
120
  /**
@@ -45,9 +45,9 @@ export default class NetworkLoader {
45
45
  /**
46
46
  * Abort the current loading operation
47
47
  * - Only works when in STATE_LOADING
48
- * - Aborts network requests and transitions to STATE_CANCELLED
48
+ * - Aborts network requests and transitions to STATE_ABORTING
49
49
  */
50
- doAbort(): void;
50
+ abort(): void;
51
51
  /**
52
52
  * Get network data size in bytes
53
53
  * - Info comes from the content length response header
@@ -7,14 +7,14 @@ import {
7
7
  STATE_LOADING,
8
8
  STATE_UNLOADING,
9
9
  STATE_LOADED,
10
- STATE_CANCELLED,
11
- STATE_ERROR,
10
+ STATE_ABORTING,
12
11
  LOAD,
13
12
  ERROR,
14
13
  LOADED,
15
14
  UNLOAD,
16
15
  INITIAL,
17
- CANCEL
16
+ ABORT,
17
+ ABORTED
18
18
  } from '../../state/machines.js';
19
19
 
20
20
  import * as expect from '../../util/expect.js';
@@ -123,21 +123,23 @@ export default class NetworkLoader {
123
123
  }
124
124
  break;
125
125
 
126
- case STATE_CANCELLED:
126
+ case STATE_ABORTING:
127
127
  {
128
- // console.log('NetworkLoader:cancelled');
128
+ // console.log('NetworkLoader:aborting');
129
129
  if (this._abortLoading) {
130
130
  this._abortLoading();
131
131
  this._abortLoading = null;
132
132
  }
133
+ // Transition to aborted state after abort completes
134
+ this._state.send(ABORTED);
133
135
  }
134
136
  break;
135
137
 
136
- case STATE_ERROR:
137
- {
138
- console.log('NetworkLoader:error', state.error);
139
- }
140
- break;
138
+ // case STATE_ERROR:
139
+ // {
140
+ // console.log('NetworkLoader:error', state.error);
141
+ // }
142
+ // break;
141
143
  } // end switch
142
144
  };
143
145
  }
@@ -160,10 +162,10 @@ export default class NetworkLoader {
160
162
  /**
161
163
  * Abort the current loading operation
162
164
  * - Only works when in STATE_LOADING
163
- * - Aborts network requests and transitions to STATE_CANCELLED
165
+ * - Aborts network requests and transitions to STATE_ABORTING
164
166
  */
165
- doAbort() {
166
- this._state.send(CANCEL);
167
+ abort() {
168
+ this._state.send(ABORT);
167
169
  }
168
170
 
169
171
  /**
@@ -10,13 +10,13 @@ export default class LoadingStateMachine extends FiniteStateMachine {
10
10
  * - Only valid when currently loading
11
11
  * - Useful for external timeout management
12
12
  */
13
- doTimeout(): void;
13
+ timeout(): void;
14
14
  /**
15
- * Transition to cancelled state
15
+ * Transition to aborting state
16
16
  * - Only valid when currently loading
17
- * - Useful for external cancellation management
17
+ * - Useful for external abort management
18
18
  */
19
- doCancel(): void;
19
+ abort(): void;
20
20
  #private;
21
21
  }
22
22
  import FiniteStateMachine from '../finite-state-machine/FiniteStateMachine.svelte.js';
@@ -8,14 +8,16 @@ import {
8
8
  STATE_LOADING,
9
9
  STATE_UNLOADING,
10
10
  STATE_LOADED,
11
- STATE_CANCELLED,
11
+ STATE_ABORTING,
12
+ STATE_ABORTED,
12
13
  STATE_ERROR,
13
14
  STATE_TIMEOUT,
14
15
 
15
16
  // > Signals
16
17
  INITIAL,
17
18
  LOAD,
18
- CANCEL,
19
+ ABORT,
20
+ ABORTED,
19
21
  ERROR,
20
22
  LOADED,
21
23
  UNLOAD,
@@ -41,7 +43,7 @@ export default class LoadingStateMachine extends FiniteStateMachine {
41
43
  // _enter: () => {
42
44
  // console.log('LoadingStateMachine: enter LOADING');
43
45
  // },
44
- [CANCEL]: STATE_CANCELLED,
46
+ [ABORT]: STATE_ABORTING,
45
47
  [ERROR]: STATE_ERROR,
46
48
  [LOADED]: STATE_LOADED,
47
49
  [TIMEOUT]: STATE_TIMEOUT
@@ -57,7 +59,11 @@ export default class LoadingStateMachine extends FiniteStateMachine {
57
59
  [ERROR]: STATE_ERROR,
58
60
  [INITIAL]: STATE_INITIAL
59
61
  },
60
- [STATE_CANCELLED]: {
62
+ [STATE_ABORTING]: {
63
+ [ERROR]: STATE_ERROR,
64
+ [ABORTED]: STATE_ABORTED
65
+ },
66
+ [STATE_ABORTED]: {
61
67
  [LOAD]: STATE_LOADING,
62
68
  [UNLOAD]: STATE_UNLOADING
63
69
  },
@@ -99,16 +105,16 @@ export default class LoadingStateMachine extends FiniteStateMachine {
99
105
  * - Only valid when currently loading
100
106
  * - Useful for external timeout management
101
107
  */
102
- doTimeout() {
108
+ timeout() {
103
109
  this.send(TIMEOUT);
104
110
  }
105
111
 
106
112
  /**
107
- * Transition to cancelled state
113
+ * Transition to aborting state
108
114
  * - Only valid when currently loading
109
- * - Useful for external cancellation management
115
+ * - Useful for external abort management
110
116
  */
111
- doCancel() {
112
- this.send(CANCEL);
117
+ abort() {
118
+ this.send(ABORT);
113
119
  }
114
120
  }