@hkdigital/lib-sveltekit 0.1.70 → 0.1.72
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/cache/IndexedDbCache.d.ts +212 -0
- package/dist/classes/cache/IndexedDbCache.js +673 -0
- package/dist/classes/cache/MemoryResponseCache.d.ts +101 -14
- package/dist/classes/cache/MemoryResponseCache.js +97 -12
- package/dist/classes/cache/index.d.ts +1 -1
- package/dist/classes/cache/index.js +2 -1
- package/dist/classes/events/EventEmitter.d.ts +142 -0
- package/dist/classes/events/EventEmitter.js +275 -0
- package/dist/classes/events/index.d.ts +1 -0
- package/dist/classes/events/index.js +2 -0
- package/dist/classes/logging/Logger.d.ts +74 -0
- package/dist/classes/logging/Logger.js +158 -0
- package/dist/classes/logging/constants.d.ts +14 -0
- package/dist/classes/logging/constants.js +18 -0
- package/dist/classes/logging/index.d.ts +2 -0
- package/dist/classes/logging/index.js +4 -0
- package/dist/classes/services/ServiceBase.d.ts +153 -0
- package/dist/classes/services/ServiceBase.js +409 -0
- package/dist/classes/services/ServiceManager.d.ts +350 -0
- package/dist/classes/services/ServiceManager.js +1114 -0
- package/dist/classes/services/constants.d.ts +11 -0
- package/dist/classes/services/constants.js +12 -0
- package/dist/classes/services/index.d.ts +3 -0
- package/dist/classes/services/index.js +5 -0
- package/dist/util/env/index.d.ts +1 -0
- package/dist/util/env/index.js +9 -0
- package/dist/util/http/caching.js +24 -12
- package/dist/util/http/http-request.js +12 -7
- package/package.json +2 -1
- package/dist/classes/cache/PersistentResponseCache.d.ts +0 -46
- /package/dist/classes/cache/{PersistentResponseCache.js → PersistentResponseCache.js__} +0 -0
@@ -0,0 +1,1114 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Service manager for coordinating service lifecycle.
|
3
|
+
*
|
4
|
+
* The ServiceManager provides centralized registration, lifecycle management,
|
5
|
+
* and coordination of services. It maintains a registry of all services,
|
6
|
+
* manages dependencies between them, and provides events for global service
|
7
|
+
* state changes.
|
8
|
+
*
|
9
|
+
* @example
|
10
|
+
* // Basic usage
|
11
|
+
* import { ServiceManager } from './ServiceManager.js';
|
12
|
+
* import DatabaseService from './services/DatabaseService.js';
|
13
|
+
* import ApiService from './services/ApiService.js';
|
14
|
+
*
|
15
|
+
* // Create a service manager
|
16
|
+
* const manager = new ServiceManager();
|
17
|
+
*
|
18
|
+
* // Register services
|
19
|
+
* manager.register('database', new DatabaseService());
|
20
|
+
* manager.register('api', new ApiService(), { dependencies: ['database'] });
|
21
|
+
*
|
22
|
+
* // Initialize all services
|
23
|
+
* await manager.initializeAll({
|
24
|
+
* database: { connectionString: 'mongodb://localhost:27017' },
|
25
|
+
* api: { port: 3000 }
|
26
|
+
* });
|
27
|
+
*
|
28
|
+
* // Start all services (respecting dependencies)
|
29
|
+
* await manager.startAll();
|
30
|
+
*
|
31
|
+
* // Listen for service events
|
32
|
+
* manager.on('service:started', ({ service }) => {
|
33
|
+
* console.log(`Service ${service} started`);
|
34
|
+
* });
|
35
|
+
*
|
36
|
+
* // Get service instance
|
37
|
+
* const db = manager.getService('database');
|
38
|
+
* await db.query('SELECT * FROM users');
|
39
|
+
*
|
40
|
+
* // Later, stop all services
|
41
|
+
* await manager.stopAll();
|
42
|
+
*/
|
43
|
+
|
44
|
+
import { EventEmitter } from '../events';
|
45
|
+
import { Logger, INFO } from '../logging';
|
46
|
+
|
47
|
+
import {
|
48
|
+
CREATED,
|
49
|
+
INITIALIZING,
|
50
|
+
INITIALIZED,
|
51
|
+
STARTING,
|
52
|
+
RUNNING,
|
53
|
+
STOPPING,
|
54
|
+
STOPPED,
|
55
|
+
DESTROYING,
|
56
|
+
DESTROYED,
|
57
|
+
ERROR,
|
58
|
+
RECOVERING
|
59
|
+
} from './constants.js';
|
60
|
+
|
61
|
+
/**
|
62
|
+
* @typedef {Object} ServiceEntry
|
63
|
+
* @property {Object} instance - Service instance
|
64
|
+
* @property {Object} config - Service configuration
|
65
|
+
* @property {string[]} dependencies - List of service dependencies
|
66
|
+
* @property {Function} stateChangedUnsubscribe - Unsubscribe function for state events
|
67
|
+
* @property {Function} errorUnsubscribe - Unsubscribe function for error events
|
68
|
+
*/
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Manager for coordinating services lifecycle
|
72
|
+
*/
|
73
|
+
export default class ServiceManager {
|
74
|
+
/**
|
75
|
+
* Create a new service manager
|
76
|
+
*
|
77
|
+
* @param {Object} [options] - Manager options
|
78
|
+
* @param {string} [options.logLevel=INFO] - Log level for the manager
|
79
|
+
*/
|
80
|
+
constructor(options = {}) {
|
81
|
+
/**
|
82
|
+
* Map of registered services
|
83
|
+
* @type {Map<string, ServiceEntry>}
|
84
|
+
* @private
|
85
|
+
*/
|
86
|
+
this.services = new Map();
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Event emitter for service events
|
90
|
+
* @type {EventEmitter}
|
91
|
+
*/
|
92
|
+
this.events = new EventEmitter();
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Service manager logger
|
96
|
+
* @type {Logger}
|
97
|
+
*/
|
98
|
+
this.logger = new Logger('ServiceManager', options.logLevel || INFO);
|
99
|
+
|
100
|
+
/**
|
101
|
+
* Service dependency graph
|
102
|
+
* @type {Map<string, Set<string>>}
|
103
|
+
* @private
|
104
|
+
*/
|
105
|
+
this.dependencyGraph = new Map();
|
106
|
+
|
107
|
+
this.logger.debug('Service manager created');
|
108
|
+
}
|
109
|
+
|
110
|
+
/**
|
111
|
+
* Register an event handler
|
112
|
+
*
|
113
|
+
* @param {string} eventName - Event name
|
114
|
+
* @param {Function} handler - Event handler
|
115
|
+
* @returns {Function} Unsubscribe function
|
116
|
+
*/
|
117
|
+
on(eventName, handler) {
|
118
|
+
return this.events.on(eventName, handler);
|
119
|
+
}
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Remove an event handler
|
123
|
+
*
|
124
|
+
* @param {string} eventName - Event name
|
125
|
+
* @param {Function} handler - Event handler
|
126
|
+
* @returns {boolean} True if handler was removed
|
127
|
+
*/
|
128
|
+
off(eventName, handler) {
|
129
|
+
return this.events.off(eventName, handler);
|
130
|
+
}
|
131
|
+
|
132
|
+
/**
|
133
|
+
* Emit an event
|
134
|
+
*
|
135
|
+
* @param {string} eventName - Event name
|
136
|
+
* @param {*} data - Event data
|
137
|
+
* @returns {boolean} True if event had listeners
|
138
|
+
* @private
|
139
|
+
*/
|
140
|
+
_emit(eventName, data) {
|
141
|
+
return this.events.emit(eventName, data);
|
142
|
+
}
|
143
|
+
|
144
|
+
/**
|
145
|
+
* Register a service
|
146
|
+
*
|
147
|
+
* @param {string} name - Service name
|
148
|
+
* @param {Object} instance - Service instance
|
149
|
+
* @param {Object} [options] - Registration options
|
150
|
+
* @param {string[]} [options.dependencies=[]] - Service dependencies
|
151
|
+
* @param {Object} [options.config={}] - Service configuration
|
152
|
+
* @returns {boolean} True if registration was successful
|
153
|
+
*
|
154
|
+
* @example
|
155
|
+
* manager.register('database', new DatabaseService());
|
156
|
+
* manager.register('api', new ApiService(), {
|
157
|
+
* dependencies: ['database'],
|
158
|
+
* config: { port: 3000 }
|
159
|
+
* });
|
160
|
+
*/
|
161
|
+
register(name, instance, options = {}) {
|
162
|
+
if (this.services.has(name)) {
|
163
|
+
this.logger.warn(`Service '${name}' already registered`);
|
164
|
+
return false;
|
165
|
+
}
|
166
|
+
|
167
|
+
const { dependencies = [], config = {} } = options;
|
168
|
+
|
169
|
+
// Check if dependencies are valid
|
170
|
+
for (const dep of dependencies) {
|
171
|
+
if (!this.services.has(dep)) {
|
172
|
+
this.logger.warn(
|
173
|
+
`Cannot register service '${name}': missing dependency '${dep}'`
|
174
|
+
);
|
175
|
+
return false;
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
// Create service entry
|
180
|
+
const entry = {
|
181
|
+
instance,
|
182
|
+
config,
|
183
|
+
dependencies,
|
184
|
+
stateChangedUnsubscribe: null,
|
185
|
+
errorUnsubscribe: null
|
186
|
+
};
|
187
|
+
|
188
|
+
// Subscribe to service events
|
189
|
+
entry.stateChangedUnsubscribe = instance.on('stateChanged', event => {
|
190
|
+
this._handleStateChanged(name, event);
|
191
|
+
});
|
192
|
+
|
193
|
+
entry.errorUnsubscribe = instance.on('error', event => {
|
194
|
+
this._handleError(name, event);
|
195
|
+
});
|
196
|
+
|
197
|
+
// Add to registry
|
198
|
+
this.services.set(name, entry);
|
199
|
+
|
200
|
+
// Update dependency graph
|
201
|
+
this._updateDependencyGraph();
|
202
|
+
|
203
|
+
this.logger.info(`Service '${name}' registered`, { dependencies });
|
204
|
+
this._emit('service:registered', { service: name });
|
205
|
+
|
206
|
+
return true;
|
207
|
+
}
|
208
|
+
|
209
|
+
/**
|
210
|
+
* Unregister a service
|
211
|
+
*
|
212
|
+
* @param {string} name - Service name
|
213
|
+
* @returns {boolean} True if unregistration was successful
|
214
|
+
*
|
215
|
+
* @example
|
216
|
+
* manager.unregister('api');
|
217
|
+
*/
|
218
|
+
unregister(name) {
|
219
|
+
const entry = this.services.get(name);
|
220
|
+
if (!entry) {
|
221
|
+
this.logger.warn(`Service '${name}' not registered`);
|
222
|
+
return false;
|
223
|
+
}
|
224
|
+
|
225
|
+
// Check if other services depend on this one
|
226
|
+
for (const [serviceName, serviceEntry] of this.services.entries()) {
|
227
|
+
if (serviceEntry.dependencies.includes(name)) {
|
228
|
+
this.logger.error(
|
229
|
+
`Cannot unregister service '${name}': ` +
|
230
|
+
`'${serviceName}' depends on it`
|
231
|
+
);
|
232
|
+
return false;
|
233
|
+
}
|
234
|
+
}
|
235
|
+
|
236
|
+
// Clean up event subscriptions
|
237
|
+
if (entry.stateChangedUnsubscribe) {
|
238
|
+
entry.stateChangedUnsubscribe();
|
239
|
+
}
|
240
|
+
|
241
|
+
if (entry.errorUnsubscribe) {
|
242
|
+
entry.errorUnsubscribe();
|
243
|
+
}
|
244
|
+
|
245
|
+
// Remove from registry
|
246
|
+
this.services.delete(name);
|
247
|
+
|
248
|
+
// Update dependency graph
|
249
|
+
this._updateDependencyGraph();
|
250
|
+
|
251
|
+
this.logger.info(`Service '${name}' unregistered`);
|
252
|
+
this._emit('service:unregistered', { service: name });
|
253
|
+
|
254
|
+
return true;
|
255
|
+
}
|
256
|
+
|
257
|
+
/**
|
258
|
+
* Get a service instance
|
259
|
+
*
|
260
|
+
* @param {string} name - Service name
|
261
|
+
* @returns {Object|null} Service instance or null if not found
|
262
|
+
*
|
263
|
+
* @example
|
264
|
+
* const db = manager.getService('database');
|
265
|
+
* await db.query('SELECT * FROM users');
|
266
|
+
*/
|
267
|
+
getService(name) {
|
268
|
+
const entry = this.services.get(name);
|
269
|
+
return entry ? entry.instance : null;
|
270
|
+
}
|
271
|
+
|
272
|
+
/**
|
273
|
+
* Initialize a specific service
|
274
|
+
*
|
275
|
+
* @param {string} name - Service name
|
276
|
+
* @param {Object} [config] - Service configuration (overrides config from
|
277
|
+
* registration)
|
278
|
+
* @returns {Promise<boolean>} True if initialization was successful
|
279
|
+
*
|
280
|
+
* @example
|
281
|
+
* await manager.initializeService('database', {
|
282
|
+
* connectionString: 'mongodb://localhost:27017'
|
283
|
+
* });
|
284
|
+
*/
|
285
|
+
async initializeService(name, config) {
|
286
|
+
const entry = this.services.get(name);
|
287
|
+
if (!entry) {
|
288
|
+
this.logger.error(`Cannot initialize unknown service '${name}'`);
|
289
|
+
return false;
|
290
|
+
}
|
291
|
+
|
292
|
+
// Merge configs if provided
|
293
|
+
const mergedConfig = config
|
294
|
+
? { ...entry.config, ...config }
|
295
|
+
: entry.config;
|
296
|
+
|
297
|
+
this.logger.debug(`Initializing service '${name}'`);
|
298
|
+
const result = await entry.instance.initialize(mergedConfig);
|
299
|
+
|
300
|
+
if (result) {
|
301
|
+
this.logger.info(`Service '${name}' initialized`);
|
302
|
+
} else {
|
303
|
+
this.logger.error(`Service '${name}' initialization failed`);
|
304
|
+
}
|
305
|
+
|
306
|
+
return result;
|
307
|
+
}
|
308
|
+
|
309
|
+
/**
|
310
|
+
* Initialize all registered services
|
311
|
+
*
|
312
|
+
* @param {Object} [configs] - Configuration map for services
|
313
|
+
* @returns {Promise<boolean>} True if all services initialized successfully
|
314
|
+
*
|
315
|
+
* @example
|
316
|
+
* await manager.initializeAll({
|
317
|
+
* database: { connectionString: 'mongodb://localhost:27017' },
|
318
|
+
* api: { port: 3000 }
|
319
|
+
* });
|
320
|
+
*/
|
321
|
+
async initializeAll(configs = {}) {
|
322
|
+
let allSuccessful = true;
|
323
|
+
|
324
|
+
this.logger.info('Initializing all services');
|
325
|
+
|
326
|
+
for (const [name, entry] of this.services.entries()) {
|
327
|
+
const config = configs[name] || entry.config;
|
328
|
+
const success = await this.initializeService(name, config);
|
329
|
+
|
330
|
+
if (!success) {
|
331
|
+
allSuccessful = false;
|
332
|
+
}
|
333
|
+
}
|
334
|
+
|
335
|
+
if (allSuccessful) {
|
336
|
+
this.logger.info('All services initialized successfully');
|
337
|
+
} else {
|
338
|
+
this.logger.error('Some services failed to initialize');
|
339
|
+
}
|
340
|
+
|
341
|
+
return allSuccessful;
|
342
|
+
}
|
343
|
+
|
344
|
+
/**
|
345
|
+
* Start a specific service and its dependencies
|
346
|
+
*
|
347
|
+
* @param {string} name - Service name
|
348
|
+
* @returns {Promise<boolean>} True if start was successful
|
349
|
+
*
|
350
|
+
* @example
|
351
|
+
* await manager.startService('api');
|
352
|
+
*/
|
353
|
+
async startService(name) {
|
354
|
+
const entry = this.services.get(name);
|
355
|
+
if (!entry) {
|
356
|
+
this.logger.error(`Cannot start unknown service '${name}'`);
|
357
|
+
return false;
|
358
|
+
}
|
359
|
+
|
360
|
+
// Check if service is already running
|
361
|
+
if (entry.instance.state === RUNNING) {
|
362
|
+
this.logger.debug(`Service '${name}' is already running`);
|
363
|
+
return true;
|
364
|
+
}
|
365
|
+
|
366
|
+
// Start dependencies first
|
367
|
+
for (const depName of entry.dependencies) {
|
368
|
+
const depEntry = this.services.get(depName);
|
369
|
+
|
370
|
+
if (!depEntry) {
|
371
|
+
this.logger.error(
|
372
|
+
`Cannot start service '${name}': ` +
|
373
|
+
`dependency '${depName}' not found`
|
374
|
+
);
|
375
|
+
return false;
|
376
|
+
}
|
377
|
+
|
378
|
+
if (depEntry.instance.state !== RUNNING) {
|
379
|
+
const success = await this.startService(depName);
|
380
|
+
if (!success) {
|
381
|
+
this.logger.error(
|
382
|
+
`Cannot start service '${name}': ` +
|
383
|
+
`dependency '${depName}' failed to start`
|
384
|
+
);
|
385
|
+
return false;
|
386
|
+
}
|
387
|
+
}
|
388
|
+
}
|
389
|
+
|
390
|
+
// Start this service
|
391
|
+
this.logger.debug(`Starting service '${name}'`);
|
392
|
+
const result = await entry.instance.start();
|
393
|
+
|
394
|
+
if (result) {
|
395
|
+
this.logger.info(`Service '${name}' started`);
|
396
|
+
} else {
|
397
|
+
this.logger.error(`Service '${name}' failed to start`);
|
398
|
+
}
|
399
|
+
|
400
|
+
return result;
|
401
|
+
}
|
402
|
+
|
403
|
+
/**
|
404
|
+
* Start all services in dependency order
|
405
|
+
*
|
406
|
+
* @returns {Promise<boolean>} True if all services started successfully
|
407
|
+
*
|
408
|
+
* @example
|
409
|
+
* await manager.startAll();
|
410
|
+
*/
|
411
|
+
async startAll() {
|
412
|
+
let allSuccessful = true;
|
413
|
+
const started = new Set();
|
414
|
+
|
415
|
+
this.logger.info('Starting all services');
|
416
|
+
|
417
|
+
// Get dependency ordered list
|
418
|
+
const orderedServices = this._getStartOrder();
|
419
|
+
|
420
|
+
// Start services in order
|
421
|
+
for (const name of orderedServices) {
|
422
|
+
if (started.has(name)) {
|
423
|
+
continue; // Already started as a dependency
|
424
|
+
}
|
425
|
+
|
426
|
+
const success = await this.startService(name);
|
427
|
+
|
428
|
+
if (success) {
|
429
|
+
started.add(name);
|
430
|
+
} else {
|
431
|
+
allSuccessful = false;
|
432
|
+
this.logger.error(`Failed to start service '${name}'`);
|
433
|
+
}
|
434
|
+
}
|
435
|
+
|
436
|
+
if (allSuccessful) {
|
437
|
+
this.logger.info('All services started successfully');
|
438
|
+
} else {
|
439
|
+
this.logger.error('Some services failed to start');
|
440
|
+
}
|
441
|
+
|
442
|
+
return allSuccessful;
|
443
|
+
}
|
444
|
+
|
445
|
+
/**
|
446
|
+
* Stop a specific service and services that depend on it
|
447
|
+
*
|
448
|
+
* @param {string} name - Service name
|
449
|
+
* @param {Object} [options] - Stop options
|
450
|
+
* @param {boolean} [options.force=false] - Force stop even with dependents
|
451
|
+
* @returns {Promise<boolean>} True if stop was successful
|
452
|
+
*
|
453
|
+
* @example
|
454
|
+
* await manager.stopService('database');
|
455
|
+
*/
|
456
|
+
async stopService(name, options = {}) {
|
457
|
+
const { force = false } = options;
|
458
|
+
const entry = this.services.get(name);
|
459
|
+
|
460
|
+
if (!entry) {
|
461
|
+
this.logger.error(`Cannot stop unknown service '${name}'`);
|
462
|
+
return false;
|
463
|
+
}
|
464
|
+
|
465
|
+
// Check if already stopped
|
466
|
+
if (entry.instance.state !== RUNNING) {
|
467
|
+
this.logger.debug(`Service '${name}' is not running`);
|
468
|
+
return true;
|
469
|
+
}
|
470
|
+
|
471
|
+
// Find services that depend on this one
|
472
|
+
const dependents = [];
|
473
|
+
for (const [serviceName, serviceEntry] of this.services.entries()) {
|
474
|
+
if (serviceEntry.dependencies.includes(name) &&
|
475
|
+
serviceEntry.instance.state === RUNNING) {
|
476
|
+
dependents.push(serviceName);
|
477
|
+
}
|
478
|
+
}
|
479
|
+
|
480
|
+
// If there are dependents, stop them first or fail if not forced
|
481
|
+
if (dependents.length > 0) {
|
482
|
+
if (!force) {
|
483
|
+
this.logger.error(
|
484
|
+
`Cannot stop service '${name}': ` +
|
485
|
+
`other services depend on it: ${dependents.join(', ')}`
|
486
|
+
);
|
487
|
+
return false;
|
488
|
+
}
|
489
|
+
|
490
|
+
this.logger.warn(
|
491
|
+
`Force stopping service '${name}' with dependents: ` +
|
492
|
+
dependents.join(', ')
|
493
|
+
);
|
494
|
+
|
495
|
+
// Stop all dependents first
|
496
|
+
for (const dependent of dependents) {
|
497
|
+
const success = await this.stopService(dependent, { force });
|
498
|
+
if (!success) {
|
499
|
+
this.logger.error(
|
500
|
+
`Failed to stop dependent service '${dependent}'`
|
501
|
+
);
|
502
|
+
return false;
|
503
|
+
}
|
504
|
+
}
|
505
|
+
}
|
506
|
+
|
507
|
+
// Stop this service
|
508
|
+
this.logger.debug(`Stopping service '${name}'`);
|
509
|
+
const result = await entry.instance.stop();
|
510
|
+
|
511
|
+
if (result) {
|
512
|
+
this.logger.info(`Service '${name}' stopped`);
|
513
|
+
} else {
|
514
|
+
this.logger.error(`Service '${name}' failed to stop`);
|
515
|
+
}
|
516
|
+
|
517
|
+
return result;
|
518
|
+
}
|
519
|
+
|
520
|
+
/**
|
521
|
+
* Stop all services in reverse dependency order
|
522
|
+
*
|
523
|
+
* @param {Object} [options] - Stop options
|
524
|
+
* @param {boolean} [options.force=false] - Force stop even with errors
|
525
|
+
* @returns {Promise<boolean>} True if all services stopped successfully
|
526
|
+
*
|
527
|
+
* @example
|
528
|
+
* await manager.stopAll();
|
529
|
+
*/
|
530
|
+
async stopAll(options = {}) {
|
531
|
+
const { force = false } = options;
|
532
|
+
let allSuccessful = true;
|
533
|
+
|
534
|
+
this.logger.info('Stopping all services');
|
535
|
+
|
536
|
+
// Get reverse dependency order
|
537
|
+
const orderedServices = this._getStartOrder().reverse();
|
538
|
+
|
539
|
+
// Stop services in reverse order
|
540
|
+
for (const name of orderedServices) {
|
541
|
+
const entry = this.services.get(name);
|
542
|
+
|
543
|
+
if (entry.instance.state === RUNNING) {
|
544
|
+
const success = await this.stopService(name, { force: true });
|
545
|
+
|
546
|
+
if (!success) {
|
547
|
+
this.logger.error(`Failed to stop service '${name}'`);
|
548
|
+
allSuccessful = false;
|
549
|
+
|
550
|
+
if (!force) {
|
551
|
+
break;
|
552
|
+
}
|
553
|
+
}
|
554
|
+
}
|
555
|
+
}
|
556
|
+
|
557
|
+
if (allSuccessful) {
|
558
|
+
this.logger.info('All services stopped successfully');
|
559
|
+
} else {
|
560
|
+
this.logger.error('Some services failed to stop');
|
561
|
+
}
|
562
|
+
|
563
|
+
return allSuccessful;
|
564
|
+
}
|
565
|
+
|
566
|
+
/**
|
567
|
+
* Destroy a service and remove it from the manager
|
568
|
+
*
|
569
|
+
* @param {string} name - Service name
|
570
|
+
* @param {Object} [options] - Destroy options
|
571
|
+
* @param {boolean} [options.force=false] - Force destroy even with dependents
|
572
|
+
* @returns {Promise<boolean>} True if service was destroyed
|
573
|
+
*
|
574
|
+
* @example
|
575
|
+
* await manager.destroyService('api');
|
576
|
+
*/
|
577
|
+
async destroyService(name, options = {}) {
|
578
|
+
const { force = false } = options;
|
579
|
+
const entry = this.services.get(name);
|
580
|
+
|
581
|
+
if (!entry) {
|
582
|
+
this.logger.error(`Cannot destroy unknown service '${name}'`);
|
583
|
+
return false;
|
584
|
+
}
|
585
|
+
|
586
|
+
// If running, stop first
|
587
|
+
if (entry.instance.state === RUNNING) {
|
588
|
+
const stopSuccess = await this.stopService(name, { force });
|
589
|
+
if (!stopSuccess) {
|
590
|
+
return false;
|
591
|
+
}
|
592
|
+
}
|
593
|
+
|
594
|
+
// Check for dependents
|
595
|
+
const dependents = [];
|
596
|
+
for (const [serviceName, serviceEntry] of this.services.entries()) {
|
597
|
+
if (serviceEntry.dependencies.includes(name)) {
|
598
|
+
dependents.push(serviceName);
|
599
|
+
}
|
600
|
+
}
|
601
|
+
|
602
|
+
if (dependents.length > 0 && !force) {
|
603
|
+
this.logger.error(
|
604
|
+
`Cannot destroy service '${name}': ` +
|
605
|
+
`other services depend on it: ${dependents.join(', ')}`
|
606
|
+
);
|
607
|
+
return false;
|
608
|
+
}
|
609
|
+
|
610
|
+
// Destroy the service
|
611
|
+
this.logger.debug(`Destroying service '${name}'`);
|
612
|
+
const result = await entry.instance.destroy();
|
613
|
+
|
614
|
+
if (result) {
|
615
|
+
this.logger.info(`Service '${name}' destroyed`);
|
616
|
+
|
617
|
+
// Unregister after successful destruction
|
618
|
+
this.unregister(name);
|
619
|
+
} else {
|
620
|
+
this.logger.error(`Service '${name}' failed to destroy`);
|
621
|
+
}
|
622
|
+
|
623
|
+
return result;
|
624
|
+
}
|
625
|
+
|
626
|
+
/**
|
627
|
+
* Destroy all services and shutdown the manager
|
628
|
+
*
|
629
|
+
* @param {Object} [options] - Destroy options
|
630
|
+
* @param {boolean} [options.force=false] - Force destroy even with errors
|
631
|
+
* @returns {Promise<boolean>} True if all services were destroyed
|
632
|
+
*
|
633
|
+
* @example
|
634
|
+
* await manager.destroyAll();
|
635
|
+
*/
|
636
|
+
async destroyAll(options = {}) {
|
637
|
+
const { force = false } = options;
|
638
|
+
let allSuccessful = true;
|
639
|
+
|
640
|
+
this.logger.info('Destroying all services');
|
641
|
+
|
642
|
+
// Get reverse dependency order
|
643
|
+
const orderedServices = this._getStartOrder().reverse();
|
644
|
+
|
645
|
+
// Destroy services in reverse order
|
646
|
+
for (const name of orderedServices) {
|
647
|
+
if (this.services.has(name)) {
|
648
|
+
const success = await this.destroyService(name, { force: true });
|
649
|
+
|
650
|
+
if (!success) {
|
651
|
+
this.logger.error(`Failed to destroy service '${name}'`);
|
652
|
+
allSuccessful = false;
|
653
|
+
|
654
|
+
if (!force) {
|
655
|
+
break;
|
656
|
+
}
|
657
|
+
}
|
658
|
+
}
|
659
|
+
}
|
660
|
+
|
661
|
+
// Clean up
|
662
|
+
this.services.clear();
|
663
|
+
this.dependencyGraph.clear();
|
664
|
+
this.events.removeAllListeners();
|
665
|
+
|
666
|
+
this.logger.info('Service manager shut down');
|
667
|
+
|
668
|
+
return allSuccessful;
|
669
|
+
}
|
670
|
+
|
671
|
+
/**
|
672
|
+
* Recover a service and its dependencies from error state
|
673
|
+
*
|
674
|
+
* @param {string} name - Service name
|
675
|
+
* @param {Object} [options] - Recovery options
|
676
|
+
* @param {boolean} [options.recursive=true] - Recursively recover dependencies
|
677
|
+
* @param {boolean} [options.autoStart=true] - Auto-start service after recovery
|
678
|
+
* @returns {Promise<boolean>} True if recovery was successful
|
679
|
+
*
|
680
|
+
* @example
|
681
|
+
* // Recover a service and its dependencies
|
682
|
+
* await manager.recoverService('api');
|
683
|
+
*
|
684
|
+
* // Recover just this service without auto-starting
|
685
|
+
* await manager.recoverService('database', {
|
686
|
+
* recursive: false,
|
687
|
+
* autoStart: false
|
688
|
+
* });
|
689
|
+
*/
|
690
|
+
async recoverService(name, options = {}) {
|
691
|
+
const {
|
692
|
+
recursive = true,
|
693
|
+
autoStart = true
|
694
|
+
} = options;
|
695
|
+
|
696
|
+
const entry = this.services.get(name);
|
697
|
+
|
698
|
+
if (!entry) {
|
699
|
+
this.logger.error(`Cannot recover unknown service '${name}'`);
|
700
|
+
return false;
|
701
|
+
}
|
702
|
+
|
703
|
+
// Only proceed if service is in ERROR state
|
704
|
+
if (entry.instance.state !== ERROR) {
|
705
|
+
this.logger.debug(
|
706
|
+
`Service '${name}' is not in ERROR state (current: ${entry.instance.state})`
|
707
|
+
);
|
708
|
+
return true; // Not an error, already in a valid state
|
709
|
+
}
|
710
|
+
|
711
|
+
// First recover dependencies if needed
|
712
|
+
if (recursive) {
|
713
|
+
// Build dependency recovery order
|
714
|
+
const recoveryOrder = [];
|
715
|
+
const visited = new Set();
|
716
|
+
|
717
|
+
const visitDependencies = (serviceName) => {
|
718
|
+
if (visited.has(serviceName)) return;
|
719
|
+
visited.add(serviceName);
|
720
|
+
|
721
|
+
const deps = this.services.get(serviceName)?.dependencies || [];
|
722
|
+
for (const dep of deps) {
|
723
|
+
visitDependencies(dep);
|
724
|
+
}
|
725
|
+
|
726
|
+
recoveryOrder.push(serviceName);
|
727
|
+
};
|
728
|
+
|
729
|
+
// Visit all dependencies first
|
730
|
+
for (const dep of entry.dependencies) {
|
731
|
+
visitDependencies(dep);
|
732
|
+
}
|
733
|
+
|
734
|
+
// Recover dependencies in the correct order
|
735
|
+
for (const depName of recoveryOrder) {
|
736
|
+
if (depName === name) continue; // Skip self
|
737
|
+
|
738
|
+
const depEntry = this.services.get(depName);
|
739
|
+
if (!depEntry) continue;
|
740
|
+
|
741
|
+
if (depEntry.instance.state === ERROR) {
|
742
|
+
this.logger.info(
|
743
|
+
`Recovering dependency '${depName}' for service '${name}'`
|
744
|
+
);
|
745
|
+
|
746
|
+
const success = await this.recoverService(depName, options);
|
747
|
+
if (!success) {
|
748
|
+
this.logger.error(
|
749
|
+
`Failed to recover dependency '${depName}' for '${name}'`
|
750
|
+
);
|
751
|
+
return false;
|
752
|
+
}
|
753
|
+
}
|
754
|
+
}
|
755
|
+
}
|
756
|
+
|
757
|
+
// Now recover this service
|
758
|
+
this.logger.debug(`Recovering service '${name}'`);
|
759
|
+
const result = await entry.instance.recover();
|
760
|
+
|
761
|
+
if (!result) {
|
762
|
+
this.logger.error(`Failed to recover service '${name}'`);
|
763
|
+
return false;
|
764
|
+
}
|
765
|
+
|
766
|
+
this.logger.info(`Service '${name}' recovered successfully`);
|
767
|
+
|
768
|
+
// Auto-start if requested and all dependencies are running
|
769
|
+
if (autoStart) {
|
770
|
+
const canStart = entry.dependencies.every(dep => {
|
771
|
+
const depEntry = this.services.get(dep);
|
772
|
+
return depEntry && depEntry.instance.state === RUNNING;
|
773
|
+
});
|
774
|
+
|
775
|
+
if (canStart) {
|
776
|
+
this.logger.debug(`Auto-starting recovered service '${name}'`);
|
777
|
+
return await this.startService(name);
|
778
|
+
}
|
779
|
+
}
|
780
|
+
|
781
|
+
return true;
|
782
|
+
}
|
783
|
+
|
784
|
+
/**
|
785
|
+
* Recover all services in dependency order
|
786
|
+
*
|
787
|
+
* @param {Object} [options] - Recovery options
|
788
|
+
* @param {boolean} [options.autoStart=true] - Auto-start services after recovery
|
789
|
+
* @returns {Promise<boolean>} True if all recoveries were successful
|
790
|
+
*
|
791
|
+
* @example
|
792
|
+
* // Recover all services and auto-start them
|
793
|
+
* await manager.recoverAll();
|
794
|
+
*
|
795
|
+
* // Recover all services without auto-starting
|
796
|
+
* await manager.recoverAll({ autoStart: false });
|
797
|
+
*/
|
798
|
+
async recoverAll(options = {}) {
|
799
|
+
let allSuccessful = true;
|
800
|
+
const { autoStart = true } = options;
|
801
|
+
|
802
|
+
this.logger.info('Recovering all services');
|
803
|
+
|
804
|
+
// Find services in ERROR state
|
805
|
+
const errorServices = [];
|
806
|
+
for (const [name, entry] of this.services.entries()) {
|
807
|
+
if (entry.instance.state === ERROR) {
|
808
|
+
errorServices.push(name);
|
809
|
+
}
|
810
|
+
}
|
811
|
+
|
812
|
+
if (errorServices.length === 0) {
|
813
|
+
this.logger.info('No services in ERROR state');
|
814
|
+
return true;
|
815
|
+
}
|
816
|
+
|
817
|
+
// Get dependency ordered list
|
818
|
+
const orderedServices = this._getStartOrder();
|
819
|
+
|
820
|
+
// Recover services in order
|
821
|
+
for (const name of orderedServices) {
|
822
|
+
const entry = this.services.get(name);
|
823
|
+
|
824
|
+
if (entry.instance.state === ERROR) {
|
825
|
+
const success = await this.recoverService(name, {
|
826
|
+
recursive: false, // Already handling order here
|
827
|
+
autoStart
|
828
|
+
});
|
829
|
+
|
830
|
+
if (!success) {
|
831
|
+
allSuccessful = false;
|
832
|
+
this.logger.error(`Failed to recover service '${name}'`);
|
833
|
+
}
|
834
|
+
}
|
835
|
+
}
|
836
|
+
|
837
|
+
if (allSuccessful) {
|
838
|
+
this.logger.info('All services recovered successfully');
|
839
|
+
|
840
|
+
// If auto-start enabled, start services that weren't auto-started
|
841
|
+
if (autoStart) {
|
842
|
+
await this.startAll();
|
843
|
+
}
|
844
|
+
} else {
|
845
|
+
this.logger.error('Some services failed to recover');
|
846
|
+
}
|
847
|
+
|
848
|
+
return allSuccessful;
|
849
|
+
}
|
850
|
+
|
851
|
+
/**
|
852
|
+
* Set log level for a specific service or all services
|
853
|
+
*
|
854
|
+
* @param {string} level - New log level
|
855
|
+
* @param {string} [serviceName] - Service to set level for, or all if omitted
|
856
|
+
* @returns {boolean} True if level was set successfully
|
857
|
+
*
|
858
|
+
* @example
|
859
|
+
* // Set level for specific service
|
860
|
+
* manager.setLogLevel(DEBUG, 'database');
|
861
|
+
*
|
862
|
+
* // Set level for all services including manager
|
863
|
+
* manager.setLogLevel(INFO);
|
864
|
+
*/
|
865
|
+
setLogLevel(level, serviceName) {
|
866
|
+
if (serviceName) {
|
867
|
+
// Set for specific service
|
868
|
+
const entry = this.services.get(serviceName);
|
869
|
+
if (!entry) {
|
870
|
+
this.logger.error(`Cannot set log level for unknown service '${serviceName}'`);
|
871
|
+
return false;
|
872
|
+
}
|
873
|
+
|
874
|
+
return entry.instance.setLogLevel(level);
|
875
|
+
} else {
|
876
|
+
// Set for all services and manager
|
877
|
+
let allSuccess = true;
|
878
|
+
|
879
|
+
// Set for the manager
|
880
|
+
if (!this.logger.setLevel(level)) {
|
881
|
+
allSuccess = false;
|
882
|
+
}
|
883
|
+
|
884
|
+
// Set for all services
|
885
|
+
for (const [name, entry] of this.services.entries()) {
|
886
|
+
if (!entry.instance.setLogLevel(level)) {
|
887
|
+
this.logger.warn(`Failed to set log level for service '${name}'`);
|
888
|
+
allSuccess = false;
|
889
|
+
}
|
890
|
+
}
|
891
|
+
|
892
|
+
return allSuccess;
|
893
|
+
}
|
894
|
+
}
|
895
|
+
|
896
|
+
/**
|
897
|
+
* Get the names of all registered services
|
898
|
+
*
|
899
|
+
* @returns {string[]} List of service names
|
900
|
+
*
|
901
|
+
* @example
|
902
|
+
* const services = manager.getServiceNames();
|
903
|
+
* console.log(`Registered services: ${services.join(', ')}`);
|
904
|
+
*/
|
905
|
+
getServiceNames() {
|
906
|
+
return Array.from(this.services.keys());
|
907
|
+
}
|
908
|
+
|
909
|
+
/**
|
910
|
+
* Get service status information
|
911
|
+
*
|
912
|
+
* @param {string} [name] - Service name, or all if omitted
|
913
|
+
* @returns {Object|Array|null} Service status or null if not found
|
914
|
+
*
|
915
|
+
* @example
|
916
|
+
* // Get status for all services
|
917
|
+
* const allStatus = manager.getServiceStatus();
|
918
|
+
*
|
919
|
+
* // Get status for specific service
|
920
|
+
* const dbStatus = manager.getServiceStatus('database');
|
921
|
+
* console.log(`Database state: ${dbStatus.state}`);
|
922
|
+
*/
|
923
|
+
getServiceStatus(name) {
|
924
|
+
if (name) {
|
925
|
+
// Get status for specific service
|
926
|
+
const entry = this.services.get(name);
|
927
|
+
if (!entry) {
|
928
|
+
return null;
|
929
|
+
}
|
930
|
+
|
931
|
+
return {
|
932
|
+
name,
|
933
|
+
state: entry.instance.state,
|
934
|
+
dependencies: entry.dependencies,
|
935
|
+
error: entry.instance.error ? entry.instance.error.message : null
|
936
|
+
};
|
937
|
+
} else {
|
938
|
+
// Get status for all services
|
939
|
+
const statuses = [];
|
940
|
+
|
941
|
+
for (const [name, entry] of this.services.entries()) {
|
942
|
+
statuses.push({
|
943
|
+
name,
|
944
|
+
state: entry.instance.state,
|
945
|
+
dependencies: entry.dependencies,
|
946
|
+
error: entry.instance.error ? entry.instance.error.message : null
|
947
|
+
});
|
948
|
+
}
|
949
|
+
|
950
|
+
return statuses;
|
951
|
+
}
|
952
|
+
}
|
953
|
+
|
954
|
+
// Private methods
|
955
|
+
|
956
|
+
/**
|
957
|
+
* Handle state change events from services
|
958
|
+
*
|
959
|
+
* @private
|
960
|
+
* @param {string} serviceName - Service name
|
961
|
+
* @param {Object} event - State change event
|
962
|
+
*/
|
963
|
+
_handleStateChanged(serviceName, event) {
|
964
|
+
const { oldState, newState } = event;
|
965
|
+
|
966
|
+
this.logger.debug(
|
967
|
+
`Service '${serviceName}' state changed: ${oldState} -> ${newState}`
|
968
|
+
);
|
969
|
+
|
970
|
+
// Emit specific state events
|
971
|
+
this._emit('service:stateChanged', {
|
972
|
+
service: serviceName,
|
973
|
+
oldState,
|
974
|
+
newState
|
975
|
+
});
|
976
|
+
|
977
|
+
// Emit events for specific states
|
978
|
+
if (newState === INITIALIZED) {
|
979
|
+
this._emit('service:initialized', { service: serviceName });
|
980
|
+
} else if (newState === RUNNING) {
|
981
|
+
this._emit('service:started', { service: serviceName });
|
982
|
+
} else if (newState === STOPPED) {
|
983
|
+
this._emit('service:stopped', { service: serviceName });
|
984
|
+
} else if (newState === DESTROYED) {
|
985
|
+
this._emit('service:destroyed', { service: serviceName });
|
986
|
+
} else if (newState === ERROR) {
|
987
|
+
this._emit('service:error', {
|
988
|
+
service: serviceName,
|
989
|
+
error: this.services.get(serviceName).instance.error
|
990
|
+
});
|
991
|
+
} else if (newState === RECOVERING) {
|
992
|
+
this._emit('service:recovering', { service: serviceName });
|
993
|
+
}
|
994
|
+
}
|
995
|
+
|
996
|
+
/**
|
997
|
+
* Handle error events from services
|
998
|
+
*
|
999
|
+
* @private
|
1000
|
+
* @param {string} serviceName - Service name
|
1001
|
+
* @param {Object} event - Error event
|
1002
|
+
*/
|
1003
|
+
_handleError(serviceName, event) {
|
1004
|
+
const { operation, error } = event;
|
1005
|
+
|
1006
|
+
this.logger.error(
|
1007
|
+
`Service '${serviceName}' error during ${operation}`,
|
1008
|
+
);
|
1009
|
+
|
1010
|
+
this._emit('service:error', {
|
1011
|
+
service: serviceName,
|
1012
|
+
operation,
|
1013
|
+
error
|
1014
|
+
});
|
1015
|
+
}
|
1016
|
+
|
1017
|
+
/**
|
1018
|
+
* Update the dependency graph
|
1019
|
+
*
|
1020
|
+
* @private
|
1021
|
+
*/
|
1022
|
+
_updateDependencyGraph() {
|
1023
|
+
this.dependencyGraph.clear();
|
1024
|
+
|
1025
|
+
// Add all services to the graph
|
1026
|
+
for (const name of this.services.keys()) {
|
1027
|
+
this.dependencyGraph.set(name, new Set());
|
1028
|
+
}
|
1029
|
+
|
1030
|
+
// Add dependencies
|
1031
|
+
for (const [name, entry] of this.services.entries()) {
|
1032
|
+
for (const dep of entry.dependencies) {
|
1033
|
+
this.dependencyGraph.get(name).add(dep);
|
1034
|
+
}
|
1035
|
+
}
|
1036
|
+
|
1037
|
+
// Check for circular dependencies
|
1038
|
+
this._checkForCircularDependencies();
|
1039
|
+
}
|
1040
|
+
|
1041
|
+
/**
|
1042
|
+
* Check for circular dependencies in the graph
|
1043
|
+
*
|
1044
|
+
* @private
|
1045
|
+
*/
|
1046
|
+
_checkForCircularDependencies() {
|
1047
|
+
const visited = new Set();
|
1048
|
+
const recursionStack = new Set();
|
1049
|
+
|
1050
|
+
const checkNode = (node) => {
|
1051
|
+
if (!visited.has(node)) {
|
1052
|
+
visited.add(node);
|
1053
|
+
recursionStack.add(node);
|
1054
|
+
|
1055
|
+
const dependencies = this.dependencyGraph.get(node);
|
1056
|
+
if (dependencies) {
|
1057
|
+
for (const dep of dependencies) {
|
1058
|
+
if (!visited.has(dep) && checkNode(dep)) {
|
1059
|
+
return true;
|
1060
|
+
} else if (recursionStack.has(dep)) {
|
1061
|
+
this.logger.error(
|
1062
|
+
`Circular dependency detected: ` +
|
1063
|
+
`'${node}' -> '${dep}'`
|
1064
|
+
);
|
1065
|
+
return true;
|
1066
|
+
}
|
1067
|
+
}
|
1068
|
+
}
|
1069
|
+
}
|
1070
|
+
|
1071
|
+
recursionStack.delete(node);
|
1072
|
+
return false;
|
1073
|
+
};
|
1074
|
+
|
1075
|
+
for (const node of this.dependencyGraph.keys()) {
|
1076
|
+
if (checkNode(node)) {
|
1077
|
+
break;
|
1078
|
+
}
|
1079
|
+
}
|
1080
|
+
}
|
1081
|
+
|
1082
|
+
/**
|
1083
|
+
* Get optimal service start order based on dependencies
|
1084
|
+
*
|
1085
|
+
* @private
|
1086
|
+
* @returns {string[]} Services in dependency order
|
1087
|
+
*/
|
1088
|
+
_getStartOrder() {
|
1089
|
+
const visited = new Set();
|
1090
|
+
const result = [];
|
1091
|
+
|
1092
|
+
const visit = (node) => {
|
1093
|
+
if (visited.has(node)) return;
|
1094
|
+
|
1095
|
+
visited.add(node);
|
1096
|
+
|
1097
|
+
const dependencies = this.dependencyGraph.get(node);
|
1098
|
+
if (dependencies) {
|
1099
|
+
for (const dep of dependencies) {
|
1100
|
+
visit(dep);
|
1101
|
+
}
|
1102
|
+
}
|
1103
|
+
|
1104
|
+
result.push(node);
|
1105
|
+
};
|
1106
|
+
|
1107
|
+
// Visit all nodes
|
1108
|
+
for (const node of this.dependencyGraph.keys()) {
|
1109
|
+
visit(node);
|
1110
|
+
}
|
1111
|
+
|
1112
|
+
return result;
|
1113
|
+
}
|
1114
|
+
}
|