@hkdigital/lib-core 0.4.27 → 0.4.29

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,34 +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
15
- */
16
- get progress(): {
17
- totalBytesLoaded: number;
18
- totalSize: number;
19
- sourcesLoaded: number;
20
- numberOfSources: number;
21
- };
22
- /**
23
- * Get image scene abort progress
24
- */
25
- get abortProgress(): {
26
- sourcesAborted: number;
27
- numberOfSources: number;
28
- };
29
- /**
30
- * Start loading all image sources
12
+ * Get the array of image sources managed by this scene
13
+ *
14
+ * @returns {ImageSceneSource[]}
31
15
  */
32
- load(): void;
16
+ get sources(): ImageSceneSource[];
33
17
  /**
34
- * Abort loading all image sources
18
+ * Extract the image loader from a source object
19
+ *
20
+ * @param {ImageSceneSource} source
21
+ *
22
+ * @returns {ImageLoader}
35
23
  */
36
- abort(): void;
37
- destroy(): void;
24
+ getLoaderFromSource(source: ImageSceneSource): ImageLoader;
38
25
  /**
39
26
  * Add image source
40
27
  * - Uses an ImageLoader instance to load image data from network
@@ -84,4 +71,5 @@ export type ImageSceneSource = {
84
71
  label: string;
85
72
  imageLoader: ImageLoader;
86
73
  };
74
+ import SceneBase from '../base/SceneBase.svelte.js';
87
75
  import ImageLoader from './ImageLoader.svelte.js';
@@ -2,21 +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
- STATE_ABORTING,
12
- STATE_ABORTED,
13
- STATE_ERROR,
14
- LOAD,
15
- LOADED,
16
- ABORT,
17
- ABORTED
18
- } from '../../../state/machines.js';
19
-
5
+ import SceneBase from '../base/SceneBase.svelte.js';
20
6
  import ImageLoader from './ImageLoader.svelte.js';
21
7
 
22
8
  /**
@@ -30,152 +16,40 @@ import ImageLoader from './ImageLoader.svelte.js';
30
16
  * @property {ImageLoader} imageLoader
31
17
  */
32
18
 
33
- export default class ImageScene {
34
- #state = new LoadingStateMachine();
35
-
36
- // @note this exported state is set by onenter
37
- state = $state(STATE_INITIAL);
38
-
39
- loaded = $derived.by(() => {
40
- return this.state === STATE_LOADED;
41
- });
19
+ export default class ImageScene extends SceneBase {
42
20
 
43
21
  /** @type {ImageSceneSource[]} */
44
22
  #imageSources = $state([]);
45
23
 
46
- #progress = $derived.by(() => {
47
- // console.log('update progress');
48
-
49
- let totalSize = 0;
50
- let totalBytesLoaded = 0;
51
- let sourcesLoaded = 0;
52
-
53
- const sources = this.#imageSources;
54
- const numberOfSources = sources.length;
55
-
56
- for (let j = 0; j < numberOfSources; j++) {
57
- const source = sources[j];
58
- const { imageLoader } = source;
59
-
60
- const { bytesLoaded, size, loaded } = imageLoader.progress;
61
-
62
- totalSize += size;
63
- totalBytesLoaded += bytesLoaded;
64
-
65
- if (loaded) {
66
- sourcesLoaded++;
67
- }
68
- } // end for
69
-
70
- return {
71
- totalBytesLoaded,
72
- totalSize,
73
- sourcesLoaded,
74
- numberOfSources
75
- };
76
- });
77
-
78
- #abortProgress = $derived.by(() => {
79
- let sourcesAborted = 0;
80
- const sources = this.#imageSources;
81
- const numberOfSources = sources.length;
82
-
83
- for (let j = 0; j < numberOfSources; j++) {
84
- const source = sources[j];
85
- const { imageLoader } = source;
86
- const loaderState = imageLoader.state;
87
-
88
- if (loaderState === STATE_ABORTED || loaderState === STATE_ERROR) {
89
- sourcesAborted++;
90
- }
91
- } // end for
92
-
93
- return {
94
- sourcesAborted,
95
- numberOfSources
96
- };
97
- });
98
-
99
- #sourcesLoaded = $derived( this.#progress.sourcesLoaded );
100
- #numberOfSources = $derived( this.#progress.numberOfSources );
101
24
 
102
25
  /**
103
26
  * Construct ImageScene
104
27
  */
105
28
  constructor() {
106
- const state = this.#state;
107
-
108
- $effect( () => {
109
- if (state.current === STATE_LOADING) {
110
- if (this.#sourcesLoaded === this.#numberOfSources) {
111
- // console.log(`All [${this.#numberOfSources}] sources loaded`);
112
- this.#state.send(LOADED);
113
- }
114
- }
115
- } );
116
-
117
- $effect(() => {
118
- if (state.current === STATE_ABORTING) {
119
- const { sourcesAborted, numberOfSources } = this.#abortProgress;
120
-
121
- if (sourcesAborted === numberOfSources) {
122
- // console.debug(`ImageScene: ${numberOfSources} sources aborted`);
123
- this.#state.send(ABORTED);
124
- }
125
- }
126
- });
127
-
128
- state.onenter = ( currentState ) => {
129
- // console.log('onenter', currentState );
130
-
131
- if(currentState === STATE_LOADING )
132
- {
133
- // console.log('ImageScene:loading');
134
- this.#startLoading();
135
- }
136
- else if(currentState === STATE_ABORTING )
137
- {
138
- // console.log('ImageScene:aborting');
139
- this.#startAbort();
140
- }
141
-
142
- this.state = currentState;
143
- };
144
- }
145
-
146
- /* ==== Common loader interface */
147
-
148
- /**
149
- * Get image scene loading progress
150
- */
151
- get progress() {
152
- return this.#progress;
29
+ super();
153
30
  }
154
31
 
155
- /**
156
- * Get image scene abort progress
157
- */
158
- get abortProgress() {
159
- return this.#abortProgress;
160
- }
32
+ /* ==== SceneBase implementation */
161
33
 
162
34
  /**
163
- * Start loading all image sources
35
+ * Get the array of image sources managed by this scene
36
+ *
37
+ * @returns {ImageSceneSource[]}
164
38
  */
165
- load() {
166
- this.#state.send(LOAD);
39
+ get sources() {
40
+ return this.#imageSources;
167
41
  }
168
42
 
169
43
  /**
170
- * Abort loading all image sources
44
+ * Extract the image loader from a source object
45
+ *
46
+ * @param {ImageSceneSource} source
47
+ *
48
+ * @returns {ImageLoader}
171
49
  */
172
- abort() {
173
- this.#state.send(ABORT);
174
- }
175
-
176
- destroy() {
177
- // TODO: disconnect all image sources?
178
- // TODO: Unload ImageLoaders?
50
+ // eslint-disable-next-line no-unused-vars
51
+ getLoaderFromSource(source) {
52
+ return source.imageLoader;
179
53
  }
180
54
 
181
55
  /* ==== Source definitions */
@@ -241,18 +115,6 @@ export default class ImageScene {
241
115
  return source.imageLoader.getObjectURL();
242
116
  }
243
117
 
244
- async #startLoading() {
245
- for (const { imageLoader } of this.#imageSources) {
246
- imageLoader.load();
247
- }
248
- }
249
-
250
- #startAbort() {
251
- for (const { imageLoader } of this.#imageSources) {
252
- imageLoader.abort();
253
- }
254
- }
255
-
256
118
  /* ==== Internals */
257
119
 
258
120
  /**
@@ -129,12 +129,32 @@ export class ServiceManager extends EventEmitter {
129
129
  */
130
130
  isRunning(name: string): Promise<boolean>;
131
131
  /**
132
- * Set log level for a service or globally
132
+ * Listen to log messages emitted by individual services
133
133
  *
134
- * @param {string} name - Service name or '*' for global
135
- * @param {string} level - Log level to set
134
+ * @param {Function} listener - Log event handler
135
+ *
136
+ * @returns {Function} Unsubscribe function
137
+ */
138
+ onServiceLogEvent(listener: Function): Function;
139
+ /**
140
+ * Set log level for the ServiceManager itself
141
+ *
142
+ * @param {string} level - Log level to set for the ServiceManager
143
+ */
144
+ setManagerLogLevel(level: string): void;
145
+ /**
146
+ * Set log level for individual services
147
+ *
148
+ * @param {string|Object<string,string>} nameOrConfig
149
+ * Service configuration:
150
+ * - String with service name: 'auth' (requires level parameter)
151
+ * - String with config: 'auth:debug,database:info'
152
+ * - Object: { auth: 'debug', database: 'info' }
153
+ * @param {string} [level] - Log level (required when nameOrConfig is service name)
136
154
  */
137
- setLogLevel(name: string, level: string): void;
155
+ setServiceLogLevel(nameOrConfig: string | {
156
+ [x: string]: string;
157
+ }, level?: string): void;
138
158
  /**
139
159
  * Get all services with a specific tag
140
160
  *