@hkdigital/lib-sveltekit 0.2.11 → 0.2.12

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,52 +1,53 @@
1
1
  /**
2
- * @fileoverview Base service class with lifecycle management and logging.
2
+ * @fileoverview Base service class with lifecycle management, health checks,
3
+ * and integrated logging.
3
4
  *
4
- * ServiceBase provides a standardized lifecycle (initialize, start, stop,
5
- * destroy) with state transitions, error handling, and integrated logging.
6
- * Services should extend this class and override the protected _init, _start,
7
- * _stop, and _destroy methods to implement their specific functionality.
5
+ * ServiceBase provides a standardized lifecycle for all services with states,
6
+ * events, logging, and error handling. Services extend this class and override
7
+ * the protected methods to implement their specific functionality.
8
8
  *
9
9
  * @example
10
- * // Creating a service
10
+ * // Basic service implementation
11
11
  * import { ServiceBase } from './ServiceBase.js';
12
12
  *
13
13
  * class DatabaseService extends ServiceBase {
14
- * constructor() {
15
- * super('database');
16
- * this.connection = null;
17
- * }
18
- *
19
14
  * async _init(config) {
20
- * this.config = config;
21
- * this.logger.debug('Database configured', { config });
15
+ * this.connectionString = config.connectionString;
22
16
  * }
23
17
  *
24
18
  * async _start() {
25
- * this.connection = await createConnection(this.config);
26
- * this.logger.info('Database connected', { id: this.connection.id });
19
+ * this.connection = await createConnection(this.connectionString);
27
20
  * }
28
21
  *
29
22
  * async _stop() {
30
- * await this.connection.close();
31
- * this.connection = null;
32
- * this.logger.info('Database disconnected');
23
+ * await this.connection?.close();
33
24
  * }
34
25
  * }
35
26
  *
36
- * // Using a service
37
- * const db = new DatabaseService();
38
- *
39
- * await db.initialize({ host: 'localhost', port: 27017 });
27
+ * // Usage
28
+ * const db = new DatabaseService('database');
29
+ * await db.initialize({ connectionString: 'postgres://...' });
40
30
  * await db.start();
41
31
  *
42
- * // Listen for state changes
43
- * db.on('stateChanged', ({ oldState, newState }) => {
44
- * console.log(`Database service: ${oldState} -> ${newState}`);
32
+ * // Listen to events
33
+ * db.on('healthChanged', ({ healthy }) => {
34
+ * console.log(`Database is ${healthy ? 'healthy' : 'unhealthy'}`);
45
35
  * });
46
36
  *
47
- * // Later...
48
- * await db.stop();
49
- * await db.destroy();
37
+ * @example
38
+ * // Service with recovery and health checks
39
+ * class ApiService extends ServiceBase {
40
+ * async _recover() {
41
+ * // Custom recovery logic
42
+ * await this.reconnect();
43
+ * }
44
+ *
45
+ * async _healthCheck() {
46
+ * const start = Date.now();
47
+ * await this.ping();
48
+ * return { latency: Date.now() - start };
49
+ * }
50
+ * }
50
51
  */
51
52
 
52
53
  import { EventEmitter } from '../events';
@@ -62,81 +63,68 @@ import {
62
63
  STOPPED,
63
64
  DESTROYING,
64
65
  DESTROYED,
65
- ERROR,
66
+ ERROR as ERROR_STATE,
66
67
  RECOVERING
67
- } from './constants';
68
+ } from './service-states.js';
69
+
70
+ /**
71
+ * @typedef {import('./typedef.js').ServiceConfig} ServiceConfig
72
+ * @typedef {import('./typedef.js').ServiceOptions} ServiceOptions
73
+ * @typedef {import('./typedef.js').StopOptions} StopOptions
74
+ * @typedef {import('./typedef.js').HealthStatus} HealthStatus
75
+ * @typedef {import('./typedef.js').StateChangeEvent} StateChangeEvent
76
+ * @typedef {import('./typedef.js').HealthChangeEvent} HealthChangeEvent
77
+ * @typedef {import('./typedef.js').ServiceErrorEvent} ServiceErrorEvent
78
+ */
68
79
 
69
80
  /**
70
- * Base class for all services
81
+ * Base class for all services with lifecycle management
82
+ * @extends EventEmitter
71
83
  */
72
- export default class ServiceBase {
84
+ export class ServiceBase extends EventEmitter {
73
85
  /**
74
- * Create a new service
86
+ * Create a new service instance
75
87
  *
76
88
  * @param {string} name - Service name
77
- * @param {Object} [options] - Service options
78
- * @param {string} [options.logLevel=INFO] - Initial log level
89
+ * @param {ServiceOptions} [options={}] - Service options
79
90
  */
80
91
  constructor(name, options = {}) {
81
- /**
82
- * Service name
83
- * @type {string}
84
- */
85
- this.name = name;
92
+ super();
86
93
 
87
- /**
88
- * Event emitter for service events
89
- * @type {EventEmitter}
90
- */
91
- this.events = new EventEmitter();
94
+ /** @type {string} */
95
+ this.name = name;
92
96
 
93
- /**
94
- * Current service state
95
- * @type {string}
96
- */
97
+ /** @type {string} */
97
98
  this.state = CREATED;
98
99
 
99
- /**
100
- * Last error that occurred
101
- * @type {Error|null}
102
- */
100
+ /** @type {boolean} */
101
+ this.healthy = false;
102
+
103
+ /** @type {Error|null} */
103
104
  this.error = null;
104
105
 
105
- /**
106
- * Last stable state before error
107
- * @type {string|null}
108
- * @private
109
- */
110
- this._preErrorState = null;
111
-
112
- /**
113
- * Service logger
114
- * @type {Logger}
115
- */
106
+ /** @type {Logger} */
116
107
  this.logger = new Logger(name, options.logLevel || INFO);
117
108
 
118
- // Set the initial state through _setState to ensure
119
- // the event is emitted consistently
120
- this._setState(CREATED);
109
+ /** @private @type {number} */
110
+ this._shutdownTimeout = options.shutdownTimeout || 5000;
121
111
  }
122
112
 
123
113
  /**
124
- * Set the service log level
114
+ * Initialize the service with configuration
125
115
  *
126
- * @param {string} level - New log level
127
- * @returns {boolean} True if level was set, false if invalid
128
- */
129
- setLogLevel(level) {
130
- return this.logger.setLevel(level);
131
- }
132
-
133
- /**
134
- * Initialize the service
116
+ * @param {ServiceConfig} [config={}] - Service-specific configuration
135
117
  *
136
- * @param {Object} [config] - Service configuration
137
- * @returns {Promise<boolean>} True if initialized successfully
118
+ * @returns {Promise<boolean>} True if initialization succeeded
138
119
  */
139
120
  async initialize(config = {}) {
121
+ if (this.state !== CREATED &&
122
+ this.state !== STOPPED &&
123
+ this.state !== DESTROYED) {
124
+ this.logger.warn(`Cannot initialize from state: ${this.state}`);
125
+ return false;
126
+ }
127
+
140
128
  try {
141
129
  this._setState(INITIALIZING);
142
130
  this.logger.debug('Initializing service', { config });
@@ -155,15 +143,11 @@ export default class ServiceBase {
155
143
  /**
156
144
  * Start the service
157
145
  *
158
- * @returns {Promise<boolean>} True if started successfully
146
+ * @returns {Promise<boolean>} True if the service started successfully
159
147
  */
160
148
  async start() {
161
- // Check if service can be started
162
149
  if (this.state !== INITIALIZED && this.state !== STOPPED) {
163
- this._setError(
164
- 'startup',
165
- new Error(`Cannot start service in state: ${this.state}`)
166
- );
150
+ this.logger.warn(`Cannot start from state: ${this.state}`);
167
151
  return false;
168
152
  }
169
153
 
@@ -174,6 +158,7 @@ export default class ServiceBase {
174
158
  await this._start();
175
159
 
176
160
  this._setState(RUNNING);
161
+ this._setHealthy(true);
177
162
  this.logger.info('Service started');
178
163
  return true;
179
164
  } catch (error) {
@@ -183,95 +168,118 @@ export default class ServiceBase {
183
168
  }
184
169
 
185
170
  /**
186
- * Stop the service
171
+ * Stop the service with optional timeout
172
+ *
173
+ * @param {StopOptions} [options={}] - Stop options
187
174
  *
188
- * @returns {Promise<boolean>} True if stopped successfully
175
+ * @returns {Promise<boolean>} True if the service stopped successfully
189
176
  */
190
- async stop() {
191
- // Check if service can be stopped
192
- if (this.state !== RUNNING) {
193
- this._setError(
194
- 'stopping',
195
- new Error(`Cannot stop service in state: ${this.state}`)
196
- );
197
- return false;
177
+ async stop(options = {}) {
178
+ if (this.state !== RUNNING && this.state !== ERROR_STATE) {
179
+ this.logger.warn(`Cannot stop from state: ${this.state}`);
180
+ return true; // Already stopped
198
181
  }
199
182
 
183
+ const timeout = options.timeout ?? this._shutdownTimeout;
184
+
200
185
  try {
201
186
  this._setState(STOPPING);
187
+ this._setHealthy(false);
202
188
  this.logger.debug('Stopping service');
203
189
 
204
- await this._stop();
190
+ // Wrap _stop in a timeout
191
+ const stopPromise = this._stop();
192
+
193
+ if (timeout > 0) {
194
+ await Promise.race([
195
+ stopPromise,
196
+ new Promise((_, reject) =>
197
+ setTimeout(() => reject(new Error('Shutdown timeout')), timeout)
198
+ )
199
+ ]);
200
+ } else {
201
+ await stopPromise;
202
+ }
205
203
 
206
204
  this._setState(STOPPED);
207
205
  this.logger.info('Service stopped');
208
206
  return true;
209
207
  } catch (error) {
210
- this._setError('stopping', error);
208
+ if (error.message === 'Shutdown timeout' && options.force) {
209
+ this.logger.warn('Forced shutdown after timeout');
210
+ this._setState(STOPPED);
211
+ return true;
212
+ }
213
+ this._setError('shutdown', error);
211
214
  return false;
212
215
  }
213
216
  }
214
217
 
215
218
  /**
216
- * Recover the service
219
+ * Recover the service from error state
217
220
  *
218
- * @returns {Promise<boolean>} True if stopped successfully
221
+ * @returns {Promise<boolean>} True if recovery succeeded
219
222
  */
220
223
  async recover() {
221
- if (this.state !== ERROR) {
222
- this.logger.warn(`Can only recover from ERROR state, current state: ${this.state}`);
223
- return false;
224
- }
225
-
226
- try {
227
- this._setState(RECOVERING);
228
- this.logger.info('Attempting service recovery');
229
-
230
- const targetState = this._preErrorState;
231
-
232
- // Allow service-specific recovery logic
233
- await this._recover();
234
-
235
- // this._setState(targetState);
236
- if( this.state !== ERROR )
237
- {
238
- // Clear
239
- this._preErrorState = null;
224
+ if (this.state !== ERROR_STATE) {
225
+ this.logger.warn(
226
+ `Can only recover from ERROR state, current: ${this.state}`
227
+ );
228
+ return false;
240
229
  }
241
230
 
242
- // Clear error
243
- this.error = null;
244
-
245
-
246
- // If recovery successful, return to initialized state
247
- this._setState(INITIALIZED);
248
- this.logger.info('Service recovery successful');
249
-
250
-
251
- return true;
252
- } catch (error) {
253
- this._setError('recovery', error);
254
- return false;
231
+ try {
232
+ this._setState(RECOVERING);
233
+ this.logger.info('Attempting recovery');
234
+
235
+ // Try custom recovery first
236
+ if (this._recover) {
237
+ await this._recover();
238
+ this._setState(RUNNING);
239
+ this._setHealthy(true);
240
+ } else {
241
+ // Default: restart
242
+ this._setState(STOPPED);
243
+ await this.start();
244
+ }
245
+
246
+ this.error = null;
247
+ this.logger.info('Recovery successful');
248
+ return true;
249
+ } catch (error) {
250
+ this._setError('recovery', error);
251
+ return false;
252
+ }
255
253
  }
256
- }
257
254
 
258
255
  /**
259
- * Destroy the service
256
+ * Destroy the service and cleanup resources
260
257
  *
261
- * @returns {Promise<boolean>} True if destroyed successfully
258
+ * @returns {Promise<boolean>} True if destruction succeeded
262
259
  */
263
260
  async destroy() {
261
+ if (this.state === DESTROYED) {
262
+ return true;
263
+ }
264
+
264
265
  try {
266
+ if (this.state === RUNNING) {
267
+ await this.stop();
268
+ }
269
+
265
270
  this._setState(DESTROYING);
266
271
  this.logger.debug('Destroying service');
267
272
 
268
- await this._destroy();
273
+ if (this._destroy) {
274
+ await this._destroy();
275
+ }
269
276
 
270
277
  this._setState(DESTROYED);
278
+ this._setHealthy(false);
271
279
  this.logger.info('Service destroyed');
272
280
 
273
- // Clean up event listeners
274
- this.events.removeAllListeners();
281
+ // Cleanup
282
+ this.removeAllListeners();
275
283
  this.logger.removeAllListeners();
276
284
 
277
285
  return true;
@@ -282,128 +290,176 @@ export default class ServiceBase {
282
290
  }
283
291
 
284
292
  /**
285
- * Add an event listener
293
+ * Get the current health status of the service
286
294
  *
287
- * @param {string} eventName - Event name
288
- * @param {Function} handler - Event handler
289
- * @returns {Function} Unsubscribe function
295
+ * @returns {Promise<HealthStatus>} Health status object
290
296
  */
291
- on(eventName, handler) {
292
- return this.events.on(eventName, handler);
297
+ async getHealth() {
298
+ const baseHealth = {
299
+ name: this.name,
300
+ state: this.state,
301
+ healthy: this.healthy,
302
+ error: this.error?.message
303
+ };
304
+
305
+ if (this._healthCheck) {
306
+ try {
307
+ const customHealth = await this._healthCheck();
308
+ return { ...baseHealth, ...customHealth };
309
+ } catch (error) {
310
+ return {
311
+ ...baseHealth,
312
+ healthy: false,
313
+ checkError: error.message
314
+ };
315
+ }
316
+ }
317
+
318
+ return baseHealth;
293
319
  }
294
320
 
295
321
  /**
296
- * Emit an event
322
+ * Set the service log level
323
+ *
324
+ * @param {string} level - New log level
297
325
  *
298
- * @param {string} eventName - Event name
299
- * @param {*} data - Event data
300
- * @returns {boolean} True if event had listeners
326
+ * @returns {boolean} True if the level was set successfully
301
327
  */
302
- emit(eventName, data) {
303
- return this.events.emit(eventName, data);
328
+ setLogLevel(level) {
329
+ return this.logger.setLevel(level);
304
330
  }
305
331
 
306
- // Protected methods to be overridden by subclasses
332
+ // Protected methods to override in subclasses
307
333
 
308
334
  /**
309
- * Initialize the service (to be overridden)
335
+ * Initialize the service (override in subclass)
310
336
  *
311
337
  * @protected
312
- * @param {Object} config - Service configuration
338
+ * @param {ServiceConfig} config - Service configuration
339
+ *
313
340
  * @returns {Promise<void>}
314
341
  */
315
342
  async _init(config) {
316
- // Default implementation does nothing
343
+ // Override in subclass
317
344
  }
318
345
 
319
346
  /**
320
- * Start the service (to be overridden)
347
+ * Start the service (override in subclass)
321
348
  *
322
349
  * @protected
350
+ *
323
351
  * @returns {Promise<void>}
324
352
  */
325
353
  async _start() {
326
- // Default implementation does nothing
354
+ // Override in subclass
327
355
  }
328
356
 
329
357
  /**
330
- * Stop the service (to be overridden)
358
+ * Stop the service (override in subclass)
331
359
  *
332
360
  * @protected
361
+ *
333
362
  * @returns {Promise<void>}
334
363
  */
335
364
  async _stop() {
336
- // Default implementation does nothing
365
+ // Override in subclass
337
366
  }
338
367
 
339
368
  /**
340
- * Destroy the service (to be overridden)
369
+ * Destroy the service (optional override)
341
370
  *
342
371
  * @protected
372
+ *
343
373
  * @returns {Promise<void>}
344
374
  */
345
375
  async _destroy() {
346
- // Default implementation does nothing
376
+ // Override in subclass if needed
347
377
  }
348
378
 
349
379
  /**
350
- * Recover the service from an error (to be overridden)
380
+ * Recover from error state (optional override)
351
381
  *
352
382
  * @protected
383
+ *
353
384
  * @returns {Promise<void>}
354
385
  */
355
386
  async _recover() {
356
- // @note the user implementation is responsible for setting the target state
357
- this._setState( this._preErrorState );
387
+ // Override in subclass if custom recovery needed
388
+ // Default behavior is stop + start
358
389
  }
359
390
 
360
- // Private helper methods
391
+ /**
392
+ * Perform health check (optional override)
393
+ *
394
+ * @protected
395
+ *
396
+ * @returns {Promise<Object>} Additional health information
397
+ */
398
+ async _healthCheck() {
399
+ // Override in subclass if health checks needed
400
+ return {};
401
+ }
402
+
403
+ // Private methods
361
404
 
362
405
  /**
363
- * Set the service state
406
+ * Set the service state and emit event
364
407
  *
365
408
  * @private
366
- * @param {string} state - New state
409
+ * @param {string} newState - New state value
367
410
  */
368
- _setState(state) {
411
+ _setState(newState) {
369
412
  const oldState = this.state;
370
- this.state = state;
413
+ this.state = newState;
371
414
 
372
- this.logger.debug(`State changed from ${oldState} to ${state}`);
373
-
374
- this.events.emit('stateChanged', {
415
+ this.emit('stateChanged', {
375
416
  service: this.name,
376
417
  oldState,
377
- newState: state
418
+ newState
378
419
  });
379
420
  }
380
421
 
381
422
  /**
382
- * Set an error state
423
+ * Set the health status and emit event if changed
424
+ *
425
+ * @private
426
+ * @param {boolean} healthy - New health status
427
+ */
428
+ _setHealthy(healthy) {
429
+ const wasHealthy = this.healthy;
430
+ this.healthy = healthy;
431
+
432
+ if (wasHealthy !== healthy) {
433
+ this.emit('healthChanged', {
434
+ service: this.name,
435
+ healthy
436
+ });
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Set error state and emit error event
383
442
  *
384
443
  * @private
385
444
  * @param {string} operation - Operation that failed
386
445
  * @param {Error} error - Error that occurred
387
446
  */
388
447
  _setError(operation, error) {
389
-
390
- if (this.state !== ERROR) {
391
- // Store current state before transitioning to ERROR
392
- this._preErrorState = this.state;
393
- }
394
-
395
448
  this.error = error;
396
- this._setState(ERROR);
449
+ this._setState(ERROR_STATE);
450
+ this._setHealthy(false);
397
451
 
398
- this.logger.error(`${operation} error`, {
452
+ this.logger.error(`${operation} failed`, {
399
453
  error: error.message,
400
454
  stack: error.stack
401
455
  });
402
456
 
403
- this.events.emit('error', {
457
+ this.emit('error', {
404
458
  service: this.name,
405
459
  operation,
406
460
  error
407
461
  });
408
462
  }
409
463
  }
464
+
465
+ export default ServiceBase;