@hkdigital/lib-core 0.4.37 → 0.4.39

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.
@@ -382,19 +382,22 @@ export class ConsoleAdapter {
382
382
  );
383
383
 
384
384
  // Clean up pnpm paths - convert complex pnpm paths to simple package names
385
- // Before: functionName@node_modules/.pnpm/package+name@version_deps.../node_modules/package/dist/...
385
+ // Before: functionName@node_modules/.pnpm/package+name@version_deps.../node_modules/package/dist/...
386
386
  // After: functionName@package/dist/...
387
387
  cleaned = cleaned.replace(
388
388
  /node_modules\/\.pnpm\/[^\/]+@[^_]+[^\/]*\/node_modules\/(@[^\/]+\/[^\/]+|[^\/]+)\//g,
389
389
  '$1/'
390
390
  );
391
391
 
392
- // Clean up regular node_modules paths - convert to simple package names
392
+ // Clean up regular node_modules paths - convert to simple package names
393
393
  cleaned = cleaned.replace(
394
394
  /node_modules\/(@[^\/]+\/[^\/]+|[^\/]+)\//g,
395
395
  '$1/'
396
396
  );
397
397
 
398
+ // Fix double @@ that can occur from the above replacements
399
+ cleaned = cleaned.replace(/@@/g, '@');
400
+
398
401
  // Clean up query parameters on source files
399
402
  cleaned = cleaned.replace(/\?[tv]=[a-f0-9]+/g, '');
400
403
 
@@ -36,11 +36,11 @@ export class ServiceBase extends EventEmitter {
36
36
  *
37
37
  * @param {Object<string,any>|null} [config={}]
38
38
  *
39
- * @returns {Promise<boolean>} True if configuration succeeded
39
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
40
40
  */
41
41
  configure(config?: {
42
42
  [x: string]: any;
43
- } | null): Promise<boolean>;
43
+ } | null): Promise<import("./typedef.js").OperationResult>;
44
44
  /**
45
45
  * Get the last applied config
46
46
  *
@@ -52,29 +52,29 @@ export class ServiceBase extends EventEmitter {
52
52
  /**
53
53
  * Start the service
54
54
  *
55
- * @returns {Promise<boolean>} True if the service started successfully
55
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
56
56
  */
57
- start(): Promise<boolean>;
57
+ start(): Promise<import("./typedef.js").OperationResult>;
58
58
  /**
59
59
  * Stop the service with optional timeout
60
60
  *
61
61
  * @param {import('./typedef.js').StopOptions} [options={}] - Stop options
62
62
  *
63
- * @returns {Promise<boolean>} True if the service stopped successfully
63
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
64
64
  */
65
- stop(options?: import("./typedef.js").StopOptions): Promise<boolean>;
65
+ stop(options?: import("./typedef.js").StopOptions): Promise<import("./typedef.js").OperationResult>;
66
66
  /**
67
67
  * Recover the service from error state
68
68
  *
69
- * @returns {Promise<boolean>} True if recovery succeeded
69
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
70
70
  */
71
- recover(): Promise<boolean>;
71
+ recover(): Promise<import("./typedef.js").OperationResult>;
72
72
  /**
73
73
  * Destroy the service and cleanup resources
74
74
  *
75
- * @returns {Promise<boolean>} True if destruction succeeded
75
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
76
76
  */
77
- destroy(): Promise<boolean>;
77
+ destroy(): Promise<import("./typedef.js").OperationResult>;
78
78
  /**
79
79
  * Get the current health status of the service
80
80
  *
@@ -85,11 +85,11 @@ export class ServiceBase extends EventEmitter {
85
85
  /**
86
86
  * Set the service log level
87
87
  *
88
- * @param {string} level - New log level
88
+ * @param {import('../../logging/typedef.js').LogLevel} level - New log level
89
89
  *
90
90
  * @returns {boolean} True if the level was set successfully
91
91
  */
92
- setLogLevel(level: string): boolean;
92
+ setLogLevel(level: import("../../logging/typedef.js").LogLevel): boolean;
93
93
  /**
94
94
  * Configure the service (handles both initial config and reconfiguration)
95
95
  *
@@ -130,7 +130,7 @@ export class ServiceBase extends EventEmitter {
130
130
  *
131
131
  * @param {Object<string,any>|null} [config={}]
132
132
  *
133
- * @returns {Promise<boolean>} True if configuration succeeded
133
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
134
134
  */
135
135
  async configure(config = {}) {
136
136
  if (
@@ -140,8 +140,9 @@ export class ServiceBase extends EventEmitter {
140
140
  this.state !== STATE_STOPPED &&
141
141
  this.state !== STATE_DESTROYED
142
142
  ) {
143
- this.logger.warn(`Cannot configure from state: ${this.state}`);
144
- return false;
143
+ const error = new Error(`Cannot configure from state: ${this.state}`);
144
+ this.logger.warn(error.message);
145
+ return { ok: false, error };
145
146
  }
146
147
 
147
148
  const wasRunning = this.state === STATE_RUNNING;
@@ -156,10 +157,10 @@ export class ServiceBase extends EventEmitter {
156
157
 
157
158
  this._setState(wasRunning ? STATE_RUNNING : STATE_CONFIGURED);
158
159
  this.logger.info('Service configured');
159
- return true;
160
+ return { ok: true };
160
161
  } catch (error) {
161
162
  this._setError('configuration', /** @type {Error} */ (error));
162
- return false;
163
+ return { ok: false, error: this.error };
163
164
  }
164
165
  }
165
166
 
@@ -175,12 +176,13 @@ export class ServiceBase extends EventEmitter {
175
176
  /**
176
177
  * Start the service
177
178
  *
178
- * @returns {Promise<boolean>} True if the service started successfully
179
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
179
180
  */
180
181
  async start() {
181
182
  if (this.state !== STATE_CONFIGURED && this.state !== STATE_STOPPED) {
182
- this.logger.warn(`Cannot start from state: ${this.state}`);
183
- return false;
183
+ const error = new Error(`Cannot start from state: ${this.state}`);
184
+ this.logger.warn(error.message);
185
+ return { ok: false, error };
184
186
  }
185
187
 
186
188
  try {
@@ -193,10 +195,10 @@ export class ServiceBase extends EventEmitter {
193
195
  this._setState(STATE_RUNNING);
194
196
  this._setHealthy(true);
195
197
  this.logger.info('Service started');
196
- return true;
198
+ return { ok: true };
197
199
  } catch (error) {
198
200
  this._setError('startup', /** @type {Error} */ (error));
199
- return false;
201
+ return { ok: false, error: this.error };
200
202
  }
201
203
  }
202
204
 
@@ -205,12 +207,12 @@ export class ServiceBase extends EventEmitter {
205
207
  *
206
208
  * @param {import('./typedef.js').StopOptions} [options={}] - Stop options
207
209
  *
208
- * @returns {Promise<boolean>} True if the service stopped successfully
210
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
209
211
  */
210
212
  async stop(options = {}) {
211
213
  if (this.state !== STATE_RUNNING && this.state !== STATE_ERROR) {
212
214
  this.logger.warn(`Cannot stop from state: ${this.state}`);
213
- return true; // Already stopped
215
+ return { ok: true }; // Already stopped
214
216
  }
215
217
 
216
218
  const timeout = options.timeout ?? this._shutdownTimeout;
@@ -237,7 +239,7 @@ export class ServiceBase extends EventEmitter {
237
239
 
238
240
  this._setState(STATE_STOPPED);
239
241
  this.logger.info('Service stopped');
240
- return true;
242
+ return { ok: true };
241
243
  } catch (error) {
242
244
  if (
243
245
  /** @type {Error} */ (error).message === 'Shutdown timeout' &&
@@ -245,24 +247,25 @@ export class ServiceBase extends EventEmitter {
245
247
  ) {
246
248
  this.logger.warn('Forced shutdown after timeout');
247
249
  this._setState(STATE_STOPPED);
248
- return true;
250
+ return { ok: true };
249
251
  }
250
252
  this._setError('shutdown', /** @type {Error} */ (error));
251
- return false;
253
+ return { ok: false, error: this.error };
252
254
  }
253
255
  }
254
256
 
255
257
  /**
256
258
  * Recover the service from error state
257
259
  *
258
- * @returns {Promise<boolean>} True if recovery succeeded
260
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
259
261
  */
260
262
  async recover() {
261
263
  if (this.state !== STATE_ERROR) {
262
- this.logger.warn(
264
+ const error = new Error(
263
265
  `Can only recover from ERROR state, current: ${this.state}`
264
266
  );
265
- return false;
267
+ this.logger.warn(error.message);
268
+ return { ok: false, error };
266
269
  }
267
270
 
268
271
  try {
@@ -278,31 +281,37 @@ export class ServiceBase extends EventEmitter {
278
281
  } else {
279
282
  // Default: restart
280
283
  this._setState(STATE_STOPPED);
281
- await this.start();
284
+ const startResult = await this.start();
285
+ if (!startResult.ok) {
286
+ return startResult; // Forward the start error
287
+ }
282
288
  }
283
289
 
284
290
  this.error = null;
285
291
  this.logger.info('Recovery successful');
286
- return true;
292
+ return { ok: true };
287
293
  } catch (error) {
288
294
  this._setError('recovery', /** @type {Error} */ (error));
289
- return false;
295
+ return { ok: false, error: this.error };
290
296
  }
291
297
  }
292
298
 
293
299
  /**
294
300
  * Destroy the service and cleanup resources
295
301
  *
296
- * @returns {Promise<boolean>} True if destruction succeeded
302
+ * @returns {Promise<import('./typedef.js').OperationResult>} Operation result
297
303
  */
298
304
  async destroy() {
299
305
  if (this.state === STATE_DESTROYED) {
300
- return true;
306
+ return { ok: true };
301
307
  }
302
308
 
303
309
  try {
304
310
  if (this.state === STATE_RUNNING) {
305
- await this.stop();
311
+ const stopResult = await this.stop();
312
+ if (!stopResult.ok) {
313
+ return stopResult; // Forward the stop error
314
+ }
306
315
  }
307
316
 
308
317
  this._setTargetState(STATE_DESTROYED);
@@ -321,10 +330,10 @@ export class ServiceBase extends EventEmitter {
321
330
  this.removeAllListeners();
322
331
  this.logger.removeAllListeners();
323
332
 
324
- return true;
333
+ return { ok: true };
325
334
  } catch (error) {
326
335
  this._setError('destruction', /** @type {Error} */ (error));
327
- return false;
336
+ return { ok: false, error: this.error };
328
337
  }
329
338
  }
330
339
 
@@ -361,7 +370,7 @@ export class ServiceBase extends EventEmitter {
361
370
  /**
362
371
  * Set the service log level
363
372
  *
364
- * @param {string} level - New log level
373
+ * @param {import('../../logging/typedef.js').LogLevel} level - New log level
365
374
  *
366
375
  * @returns {boolean} True if the level was set successfully
367
376
  */
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Result of a service operation
3
+ */
4
+ export type OperationResult = {
5
+ /**
6
+ * - Whether the operation succeeded
7
+ */
8
+ ok: boolean;
9
+ /**
10
+ * - Error details if operation failed
11
+ */
12
+ error?: Error | undefined;
13
+ };
1
14
  /**
2
15
  * All possible service states during lifecycle management
3
16
  */
@@ -23,6 +23,14 @@
23
23
  // PUBLIC TYPES
24
24
  // ============================================================================
25
25
 
26
+ /**
27
+ * Result of a service operation
28
+ *
29
+ * @typedef {Object} OperationResult
30
+ * @property {boolean} ok - Whether the operation succeeded
31
+ * @property {Error} [error] - Error details if operation failed
32
+ */
33
+
26
34
  /**
27
35
  * All possible service states during lifecycle management
28
36
  *
@@ -72,42 +72,46 @@ export class ServiceManager extends EventEmitter {
72
72
  *
73
73
  * @param {string} name - Service name
74
74
  *
75
- * @returns {Promise<boolean>} True if configuration succeeded
75
+ * @returns {Promise<import('../service-base/typedef.js').OperationResult>}
76
+ * Operation result
76
77
  */
77
- configureService(name: string): Promise<boolean>;
78
+ configureService(name: string): Promise<import("../service-base/typedef.js").OperationResult>;
78
79
  /**
79
80
  * Start a service and its dependencies
80
81
  *
81
82
  * @param {string} name - Service name
82
83
  *
83
- * @returns {Promise<boolean>} True if service started successfully
84
+ * @returns {Promise<import('../service-base/typedef.js').OperationResult>}
85
+ * Operation result
84
86
  */
85
- startService(name: string): Promise<boolean>;
87
+ startService(name: string): Promise<import("../service-base/typedef.js").OperationResult>;
86
88
  /**
87
89
  * Stop a service
88
90
  *
89
91
  * @param {string} name - Service name
90
92
  * @param {StopOptions} [options={}] - Stop options
91
93
  *
92
- * @returns {Promise<boolean>} True if service stopped successfully
94
+ * @returns {Promise<import('../service-base/typedef.js').OperationResult>}
95
+ * Operation result
93
96
  */
94
- stopService(name: string, options?: StopOptions): Promise<boolean>;
97
+ stopService(name: string, options?: StopOptions): Promise<import("../service-base/typedef.js").OperationResult>;
95
98
  /**
96
99
  * Recover a service from error state
97
100
  *
98
101
  * @param {string} name - Service name
99
102
  *
100
- * @returns {Promise<boolean>} True if recovery succeeded
103
+ * @returns {Promise<import('../service-base/typedef.js').OperationResult>}
104
+ * Operation result
101
105
  */
102
- recoverService(name: string): Promise<boolean>;
106
+ recoverService(name: string): Promise<import("../service-base/typedef.js").OperationResult>;
103
107
  /**
104
108
  * Start all registered services in dependency order
105
109
  *
106
- * @returns {Promise<Object<string, boolean>>} Map of service results
110
+ * @throws {DetailedError} if one of the services did not start
111
+ *
112
+ * @returns {Promise<string[]>} list of started service names
107
113
  */
108
- startAll(): Promise<{
109
- [x: string]: boolean;
110
- }>;
114
+ startAll(): Promise<string[]>;
111
115
  /**
112
116
  * Stop all services in reverse dependency order
113
117
  *
@@ -65,6 +65,7 @@
65
65
 
66
66
  import { EventEmitter } from '../../generic/events.js';
67
67
  import { Logger, DEBUG, INFO } from '../../logging/index.js';
68
+ import { DetailedError } from '../../generic/errors.js';
68
69
 
69
70
  import {
70
71
  SERVICE_LOG,
@@ -265,11 +266,15 @@ export class ServiceManager extends EventEmitter {
265
266
  *
266
267
  * @param {string} name - Service name
267
268
  *
268
- * @returns {Promise<boolean>} True if configuration succeeded
269
+ * @returns {Promise<import('../service-base/typedef.js').OperationResult>}
270
+ * Operation result
269
271
  */
270
272
  async configureService(name) {
271
273
  const instance = this.get(name);
272
- if (!instance) return false;
274
+ if (!instance) {
275
+ const error = new Error(`Service [${name}] not found`);
276
+ return { ok: false, error };
277
+ }
273
278
 
274
279
  const entry = this.#getServiceEntry(name);
275
280
 
@@ -283,13 +288,15 @@ export class ServiceManager extends EventEmitter {
283
288
  *
284
289
  * @param {string} name - Service name
285
290
  *
286
- * @returns {Promise<boolean>} True if service started successfully
291
+ * @returns {Promise<import('../service-base/typedef.js').OperationResult>}
292
+ * Operation result
287
293
  */
288
294
  async startService(name) {
289
295
  const entry = this.#getServiceEntry(name);
290
296
  if (!entry) {
291
- this.logger.warn(`Cannot start unregistered service '${name}'`);
292
- return false;
297
+ const error = new Error(`Service [${name}] not registered`);
298
+ this.logger.warn(error.message);
299
+ return { ok: false, error };
293
300
  }
294
301
 
295
302
  // Start dependencies first
@@ -297,19 +304,25 @@ export class ServiceManager extends EventEmitter {
297
304
  if (!(await this.isRunning(dep))) {
298
305
  this.logger.debug(`Starting dependency '${dep}' for '${name}'`);
299
306
 
300
- const started = await this.startService(dep);
307
+ const dependencyResult = await this.startService(dep);
301
308
 
302
- if (!started) {
303
- this.logger.error(
304
- new Error(`Failed to start dependency '${dep}' for '${name}'`)
309
+ if (!dependencyResult.ok) {
310
+ const error = new DetailedError(
311
+ `Failed to start dependency [${dep}] for service [${name}]`,
312
+ null,
313
+ dependencyResult.error
305
314
  );
306
- return false;
315
+ this.logger.error(error);
316
+ return { ok: false, error };
307
317
  }
308
318
  }
309
319
  }
310
320
 
311
321
  const instance = this.get(name);
312
- if (!instance) return false;
322
+ if (!instance) {
323
+ const error = new Error(`Service [${name}] instance not found`);
324
+ return { ok: false, error };
325
+ }
313
326
 
314
327
  if (
315
328
  instance.state === STATE_CREATED ||
@@ -318,10 +331,10 @@ export class ServiceManager extends EventEmitter {
318
331
  // Service is not created or has been destroyed
319
332
  // => configure needed
320
333
 
321
- const configured = await this.configureService(name);
334
+ const configResult = await this.configureService(name);
322
335
 
323
- if (!configured) {
324
- return false;
336
+ if (!configResult.ok) {
337
+ return configResult; // Forward the configuration error
325
338
  }
326
339
  }
327
340
 
@@ -334,14 +347,15 @@ export class ServiceManager extends EventEmitter {
334
347
  * @param {string} name - Service name
335
348
  * @param {StopOptions} [options={}] - Stop options
336
349
  *
337
- * @returns {Promise<boolean>} True if service stopped successfully
350
+ * @returns {Promise<import('../service-base/typedef.js').OperationResult>}
351
+ * Operation result
338
352
  */
339
353
  async stopService(name, options = {}) {
340
354
  const instance = this.get(name);
341
355
 
342
356
  if (!instance) {
343
357
  this.logger.warn(`Cannot stop unregistered service '${name}'`);
344
- return true; // Already stopped
358
+ return { ok: true }; // Already stopped
345
359
  }
346
360
 
347
361
  // Check dependents
@@ -356,10 +370,11 @@ export class ServiceManager extends EventEmitter {
356
370
  }
357
371
 
358
372
  if (runningDependents.length > 0) {
359
- this.logger.warn(
360
- `Cannot stop '${name}' - required by: ${runningDependents.join(', ')}`
373
+ const error = new Error(
374
+ `Cannot stop [${name}] - required by: ${runningDependents.join(', ')}`
361
375
  );
362
- return false;
376
+ this.logger.warn(error.message);
377
+ return { ok: false, error };
363
378
  }
364
379
  }
365
380
 
@@ -371,11 +386,15 @@ export class ServiceManager extends EventEmitter {
371
386
  *
372
387
  * @param {string} name - Service name
373
388
  *
374
- * @returns {Promise<boolean>} True if recovery succeeded
389
+ * @returns {Promise<import('../service-base/typedef.js').OperationResult>}
390
+ * Operation result
375
391
  */
376
392
  async recoverService(name) {
377
393
  const instance = this.get(name);
378
- if (!instance) return false;
394
+ if (!instance) {
395
+ const error = new Error(`Service [${name}] not found`);
396
+ return { ok: false, error };
397
+ }
379
398
 
380
399
  return await instance.recover();
381
400
  }
@@ -383,25 +402,35 @@ export class ServiceManager extends EventEmitter {
383
402
  /**
384
403
  * Start all registered services in dependency order
385
404
  *
386
- * @returns {Promise<Object<string, boolean>>} Map of service results
405
+ * @throws {DetailedError} if one of the services did not start
406
+ *
407
+ * @returns {Promise<string[]>} list of started service names
387
408
  */
388
409
  async startAll() {
389
410
  this.logger.info('Starting all services');
390
411
 
391
412
  // Sort by priority and dependencies
392
413
  const sorted = this.#topologicalSort();
393
- const results = new Map();
394
414
 
395
- for (const name of sorted) {
396
- const success = await this.startService(name);
397
- results.set(name, success);
415
+ /** @type {string[]} */
416
+ const startedServiceNames = [];
398
417
 
399
- if (!success) {
400
- throw new Error(`Failed to start service [${name}], stopping`);
418
+ for (const name of sorted) {
419
+ const result = await this.startService(name);
420
+ startedServiceNames.push(name);
421
+
422
+ if (!result.ok) {
423
+ // Create detailed error with the actual service failure
424
+ const detailedError = new DetailedError(
425
+ `Failed to start service [${name}], stopping`,
426
+ null,
427
+ result.error
428
+ );
429
+ throw detailedError;
401
430
  }
402
431
  }
403
432
 
404
- return Object.fromEntries(results);
433
+ return startedServiceNames;
405
434
  }
406
435
 
407
436
  /**
@@ -729,8 +758,8 @@ export class ServiceManager extends EventEmitter {
729
758
  async #stopAllSequentially(serviceNames, results, options) {
730
759
  for (const name of serviceNames) {
731
760
  try {
732
- const success = await this.stopService(name, options);
733
- results.set(name, success);
761
+ const result = await this.stopService(name, options);
762
+ results.set(name, result.ok);
734
763
  } catch (error) {
735
764
  this.logger.error(
736
765
  `Error stopping '${name}'`,
@@ -1,3 +1,4 @@
1
+ export type LogLevel = import("../../logging/typedef.js").LogLevel;
1
2
  /**
2
3
  * Service configuration - either a config object or a config label string
3
4
  */
@@ -40,18 +41,18 @@ export type ServiceManagerConfig = {
40
41
  /**
41
42
  * - Default log level for new services
42
43
  */
43
- defaultLogLevel?: string | undefined;
44
+ defaultLogLevel?: import("../../logging/typedef.js").LogLevel | undefined;
44
45
  /**
45
46
  * - Initial log level for ServiceManager
46
47
  */
47
- managerLogLevel?: string | undefined;
48
+ managerLogLevel?: import("../../logging/typedef.js").LogLevel | undefined;
48
49
  /**
49
50
  * Per-service log levels:
50
51
  * - String: "auth:debug,database:info"
51
52
  * - Object: { auth: "debug", database: "info" }
52
53
  */
53
54
  serviceLogLevels?: string | {
54
- [x: string]: string;
55
+ [x: string]: import("../../logging/typedef.js").LogLevel;
55
56
  } | undefined;
56
57
  };
57
58
  /**
@@ -26,6 +26,8 @@
26
26
  * manager.register('auth', AuthService, {}, options);
27
27
  */
28
28
 
29
+ /** @typedef {import('../../logging/typedef.js').LogLevel} LogLevel */
30
+
29
31
  // ============================================================================
30
32
  // PUBLIC TYPES
31
33
  // ============================================================================
@@ -52,9 +54,9 @@
52
54
  * @property {boolean} [debug=false] - Debug mode switch
53
55
  * @property {boolean} [autoStart=false] - Auto-start services on registration
54
56
  * @property {number} [stopTimeout=10000] - Default timeout for stopping services
55
- * @property {string} [defaultLogLevel] - Default log level for new services
56
- * @property {string} [managerLogLevel] - Initial log level for ServiceManager
57
- * @property {string|Object<string,string>} [serviceLogLevels]
57
+ * @property {LogLevel} [defaultLogLevel] - Default log level for new services
58
+ * @property {LogLevel} [managerLogLevel] - Initial log level for ServiceManager
59
+ * @property {string|Object<string,LogLevel>} [serviceLogLevels]
58
60
  * Per-service log levels:
59
61
  * - String: "auth:debug,database:info"
60
62
  * - Object: { auth: "debug", database: "info" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.4.37",
3
+ "version": "0.4.39",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"