@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.
- package/dist/classes/services/ServiceBase.d.ts +83 -73
- package/dist/classes/services/ServiceBase.js +237 -181
- package/dist/classes/services/ServiceManager.d.ts +96 -267
- package/dist/classes/services/ServiceManager.js +367 -874
- package/dist/classes/services/_old/ServiceBase.d.ts +153 -0
- package/dist/classes/services/_old/ServiceBase.js +409 -0
- package/dist/classes/services/_old/ServiceManager.d.ts +350 -0
- package/dist/classes/services/_old/ServiceManager.js +1114 -0
- package/dist/classes/services/service-states.d.ts +154 -0
- package/dist/classes/services/service-states.js +199 -0
- package/dist/classes/services/typedef.d.ts +206 -0
- package/dist/classes/services/typedef.js +169 -0
- package/package.json +1 -1
- /package/dist/classes/services/{constants.d.ts → _old/constants.d.ts} +0 -0
- /package/dist/classes/services/{constants.js → _old/constants.js} +0 -0
- /package/dist/classes/services/{index.d.ts → _old/index.d.ts} +0 -0
- /package/dist/classes/services/{index.js → _old/index.js} +0 -0
@@ -1,52 +1,53 @@
|
|
1
1
|
/**
|
2
|
-
* @fileoverview Base service class with lifecycle management
|
2
|
+
* @fileoverview Base service class with lifecycle management, health checks,
|
3
|
+
* and integrated logging.
|
3
4
|
*
|
4
|
-
* ServiceBase provides a standardized lifecycle
|
5
|
-
*
|
6
|
-
*
|
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
|
-
* //
|
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.
|
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.
|
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
|
31
|
-
* this.connection = null;
|
32
|
-
* this.logger.info('Database disconnected');
|
23
|
+
* await this.connection?.close();
|
33
24
|
* }
|
34
25
|
* }
|
35
26
|
*
|
36
|
-
* //
|
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
|
43
|
-
* db.on('
|
44
|
-
* console.log(`Database
|
32
|
+
* // Listen to events
|
33
|
+
* db.on('healthChanged', ({ healthy }) => {
|
34
|
+
* console.log(`Database is ${healthy ? 'healthy' : 'unhealthy'}`);
|
45
35
|
* });
|
46
36
|
*
|
47
|
-
*
|
48
|
-
*
|
49
|
-
*
|
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 './
|
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
|
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 {
|
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
|
-
|
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
|
-
|
101
|
-
|
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
|
-
|
119
|
-
|
120
|
-
this._setState(CREATED);
|
109
|
+
/** @private @type {number} */
|
110
|
+
this._shutdownTimeout = options.shutdownTimeout || 5000;
|
121
111
|
}
|
122
112
|
|
123
113
|
/**
|
124
|
-
*
|
114
|
+
* Initialize the service with configuration
|
125
115
|
*
|
126
|
-
* @param {
|
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
|
-
* @
|
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.
|
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
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
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
|
-
|
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
|
221
|
+
* @returns {Promise<boolean>} True if recovery succeeded
|
219
222
|
*/
|
220
223
|
async recover() {
|
221
|
-
|
222
|
-
|
223
|
-
|
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
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
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
|
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
|
-
|
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
|
-
//
|
274
|
-
this.
|
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
|
-
*
|
293
|
+
* Get the current health status of the service
|
286
294
|
*
|
287
|
-
* @
|
288
|
-
* @param {Function} handler - Event handler
|
289
|
-
* @returns {Function} Unsubscribe function
|
295
|
+
* @returns {Promise<HealthStatus>} Health status object
|
290
296
|
*/
|
291
|
-
|
292
|
-
|
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
|
-
*
|
322
|
+
* Set the service log level
|
323
|
+
*
|
324
|
+
* @param {string} level - New log level
|
297
325
|
*
|
298
|
-
* @
|
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
|
-
|
303
|
-
return this.
|
328
|
+
setLogLevel(level) {
|
329
|
+
return this.logger.setLevel(level);
|
304
330
|
}
|
305
331
|
|
306
|
-
// Protected methods to
|
332
|
+
// Protected methods to override in subclasses
|
307
333
|
|
308
334
|
/**
|
309
|
-
* Initialize the service (
|
335
|
+
* Initialize the service (override in subclass)
|
310
336
|
*
|
311
337
|
* @protected
|
312
|
-
* @param {
|
338
|
+
* @param {ServiceConfig} config - Service configuration
|
339
|
+
*
|
313
340
|
* @returns {Promise<void>}
|
314
341
|
*/
|
315
342
|
async _init(config) {
|
316
|
-
//
|
343
|
+
// Override in subclass
|
317
344
|
}
|
318
345
|
|
319
346
|
/**
|
320
|
-
* Start the service (
|
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
|
-
//
|
354
|
+
// Override in subclass
|
327
355
|
}
|
328
356
|
|
329
357
|
/**
|
330
|
-
* Stop the service (
|
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
|
-
//
|
365
|
+
// Override in subclass
|
337
366
|
}
|
338
367
|
|
339
368
|
/**
|
340
|
-
* Destroy the service (
|
369
|
+
* Destroy the service (optional override)
|
341
370
|
*
|
342
371
|
* @protected
|
372
|
+
*
|
343
373
|
* @returns {Promise<void>}
|
344
374
|
*/
|
345
375
|
async _destroy() {
|
346
|
-
//
|
376
|
+
// Override in subclass if needed
|
347
377
|
}
|
348
378
|
|
349
379
|
/**
|
350
|
-
* Recover
|
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
|
-
//
|
357
|
-
|
387
|
+
// Override in subclass if custom recovery needed
|
388
|
+
// Default behavior is stop + start
|
358
389
|
}
|
359
390
|
|
360
|
-
|
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}
|
409
|
+
* @param {string} newState - New state value
|
367
410
|
*/
|
368
|
-
_setState(
|
411
|
+
_setState(newState) {
|
369
412
|
const oldState = this.state;
|
370
|
-
this.state =
|
413
|
+
this.state = newState;
|
371
414
|
|
372
|
-
this.
|
373
|
-
|
374
|
-
this.events.emit('stateChanged', {
|
415
|
+
this.emit('stateChanged', {
|
375
416
|
service: this.name,
|
376
417
|
oldState,
|
377
|
-
newState
|
418
|
+
newState
|
378
419
|
});
|
379
420
|
}
|
380
421
|
|
381
422
|
/**
|
382
|
-
* Set
|
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(
|
449
|
+
this._setState(ERROR_STATE);
|
450
|
+
this._setHealthy(false);
|
397
451
|
|
398
|
-
this.logger.error(`${operation}
|
452
|
+
this.logger.error(`${operation} failed`, {
|
399
453
|
error: error.message,
|
400
454
|
stack: error.stack
|
401
455
|
});
|
402
456
|
|
403
|
-
this.
|
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;
|