@hkdigital/lib-sveltekit 0.2.11 → 0.2.13

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,1114 +1,608 @@
1
1
  /**
2
- * @fileoverview Service manager for coordinating service lifecycle.
2
+ * @fileoverview Service Manager for coordinating service lifecycle,
3
+ * dependencies, and health monitoring.
3
4
  *
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.
5
+ * The ServiceManager handles registration, dependency resolution, startup
6
+ * orchestration, and coordinated shutdown of services. It provides centralized
7
+ * logging control and health monitoring for all registered services.
8
8
  *
9
9
  * @example
10
10
  * // Basic usage
11
11
  * import { ServiceManager } from './ServiceManager.js';
12
12
  * import DatabaseService from './services/DatabaseService.js';
13
- * import ApiService from './services/ApiService.js';
13
+ * import AuthService from './services/AuthService.js';
14
14
  *
15
- * // Create a service manager
16
- * const manager = new ServiceManager();
15
+ * const manager = new ServiceManager({
16
+ * debug: true,
17
+ * stopTimeout: 10000
18
+ * });
17
19
  *
18
- * // Register services
19
- * manager.register('database', new DatabaseService());
20
- * manager.register('api', new ApiService(), { dependencies: ['database'] });
20
+ * // Register services with dependencies
21
+ * manager.register('database', DatabaseService, {
22
+ * connectionString: 'postgres://localhost/myapp'
23
+ * });
21
24
  *
22
- * // Initialize all services
23
- * await manager.initializeAll({
24
- * database: { connectionString: 'mongodb://localhost:27017' },
25
- * api: { port: 3000 }
25
+ * manager.register('auth', AuthService, {
26
+ * secret: process.env.JWT_SECRET
27
+ * }, {
28
+ * dependencies: ['database'] // auth depends on database
26
29
  * });
27
30
  *
28
- * // Start all services (respecting dependencies)
31
+ * // Start all services
29
32
  * await manager.startAll();
30
33
  *
31
- * // Listen for service events
32
- * manager.on('service:started', ({ service }) => {
33
- * console.log(`Service ${service} started`);
34
+ * @example
35
+ * // Advanced usage with health monitoring
36
+ * manager.on('service:healthChanged', ({ service, healthy }) => {
37
+ * if (!healthy) {
38
+ * console.error(`Service ${service} became unhealthy`);
39
+ * }
40
+ * });
41
+ *
42
+ * // Check health of all services
43
+ * const health = await manager.checkHealth();
44
+ * console.log('System health:', health);
45
+ *
46
+ * // Recover failed service
47
+ * manager.on('service:error', async ({ service }) => {
48
+ * console.log(`Attempting to recover ${service}`);
49
+ * await manager.recoverService(service);
34
50
  * });
35
51
  *
36
- * // Get service instance
37
- * const db = manager.getService('database');
38
- * await db.query('SELECT * FROM users');
52
+ * @example
53
+ * // Logging control
54
+ * // Set global log level
55
+ * manager.setLogLevel('*', 'DEBUG');
56
+ *
57
+ * // Set specific service log level
58
+ * manager.setLogLevel('database', 'ERROR');
39
59
  *
40
- * // Later, stop all services
41
- * await manager.stopAll();
60
+ * // Listen to all service logs
61
+ * manager.on('service:log', (logEvent) => {
62
+ * writeToLogFile(logEvent);
63
+ * });
42
64
  */
43
65
 
44
66
  import { EventEmitter } from '../events';
45
- import { Logger, INFO } from '../logging';
67
+ import { Logger, DEBUG, INFO, WARN } from '../logging';
46
68
 
47
69
  import {
48
70
  CREATED,
49
- INITIALIZING,
50
- INITIALIZED,
51
- STARTING,
52
71
  RUNNING,
53
- STOPPING,
54
- STOPPED,
55
- DESTROYING,
56
- DESTROYED,
57
- ERROR,
58
- RECOVERING
59
- } from './constants.js';
72
+ DESTROYED
73
+ } from './service-states.js';
60
74
 
61
75
  /**
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
76
+ * @typedef {import('./typedef.js').ServiceConstructor} ServiceConstructor
77
+ * @typedef {import('./typedef.js').ServiceConfig} ServiceConfig
78
+ * @typedef {import('./typedef.js').ServiceRegistrationOptions} ServiceRegistrationOptions
79
+ * @typedef {import('./typedef.js').ServiceManagerConfig} ServiceManagerConfig
80
+ * @typedef {import('./typedef.js').StopOptions} StopOptions
81
+ * @typedef {import('./typedef.js').ServiceEntry} ServiceEntry
82
+ * @typedef {import('./typedef.js').HealthCheckResult} HealthCheckResult
68
83
  */
69
84
 
70
85
  /**
71
- * Manager for coordinating services lifecycle
86
+ * Service Manager for lifecycle and dependency management
87
+ * @extends EventEmitter
72
88
  */
73
- export default class ServiceManager {
89
+ export class ServiceManager extends EventEmitter {
74
90
  /**
75
- * Create a new service manager
91
+ * Create a new ServiceManager instance
76
92
  *
77
- * @param {Object} [options] - Manager options
78
- * @param {string} [options.logLevel=INFO] - Log level for the manager
93
+ * @param {ServiceManagerConfig} [config={}] - Manager configuration
79
94
  */
80
- constructor(options = {}) {
81
- /**
82
- * Map of registered services
83
- * @type {Map<string, ServiceEntry>}
84
- * @private
85
- */
86
- this.services = new Map();
95
+ constructor(config = {}) {
96
+ super();
87
97
 
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
- }
98
+ /** @type {Map<string, ServiceEntry>} */
99
+ this.services = new Map();
109
100
 
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
- }
101
+ /** @type {Logger} */
102
+ this.logger = new Logger('ServiceManager', config.logLevel || INFO);
120
103
 
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
- }
104
+ /** @type {ServiceManagerConfig} */
105
+ this.config = {
106
+ debug: config.debug ?? false,
107
+ autoStart: config.autoStart ?? false,
108
+ stopTimeout: config.stopTimeout || 10000,
109
+ logConfig: config.logConfig || {}
110
+ };
131
111
 
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);
112
+ this._setupLogging();
142
113
  }
143
114
 
144
115
  /**
145
- * Register a service
116
+ * Register a service class with the manager
146
117
  *
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
118
+ * @param {string} name - Unique service identifier
119
+ * @param {ServiceConstructor} ServiceClass - Service class constructor
120
+ * @param {ServiceConfig} [config={}] - Service configuration
121
+ * @param {ServiceRegistrationOptions} [options={}] - Registration options
153
122
  *
154
- * @example
155
- * manager.register('database', new DatabaseService());
156
- * manager.register('api', new ApiService(), {
157
- * dependencies: ['database'],
158
- * config: { port: 3000 }
159
- * });
123
+ * @throws {Error} If service name is already registered
160
124
  */
161
- register(name, instance, options = {}) {
125
+ register(name, ServiceClass, config = {}, options = {}) {
162
126
  if (this.services.has(name)) {
163
- this.logger.warn(`Service '${name}' already registered`);
164
- return false;
127
+ throw new Error(`Service '${name}' already registered`);
165
128
  }
166
129
 
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
130
+ /** @type {ServiceEntry} */
180
131
  const entry = {
181
- instance,
132
+ ServiceClass,
133
+ instance: null,
182
134
  config,
183
- dependencies,
184
- stateChangedUnsubscribe: null,
185
- errorUnsubscribe: null
135
+ dependencies: options.dependencies || [],
136
+ dependents: new Set(),
137
+ tags: options.tags || [],
138
+ priority: options.priority || 0
186
139
  };
187
140
 
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);
141
+ // Track dependents
142
+ entry.dependencies.forEach(dep => {
143
+ const depEntry = this.services.get(dep);
144
+ if (depEntry) {
145
+ depEntry.dependents.add(name);
146
+ }
195
147
  });
196
148
 
197
- // Add to registry
198
149
  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;
150
+ this.logger.debug(`Registered service '${name}'`, {
151
+ dependencies: entry.dependencies,
152
+ tags: entry.tags
153
+ });
207
154
  }
208
155
 
209
156
  /**
210
- * Unregister a service
157
+ * Get or create a service instance
211
158
  *
212
159
  * @param {string} name - Service name
213
- * @returns {boolean} True if unregistration was successful
214
160
  *
215
- * @example
216
- * manager.unregister('api');
161
+ * @returns {import('./typedef.js').ServiceBase|null}
162
+ * Service instance or null if not found
217
163
  */
218
- unregister(name) {
164
+ get(name) {
219
165
  const entry = this.services.get(name);
220
166
  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
- }
167
+ this.logger.warn(`Service '${name}' not found`);
168
+ return null;
234
169
  }
235
170
 
236
- // Clean up event subscriptions
237
- if (entry.stateChangedUnsubscribe) {
238
- entry.stateChangedUnsubscribe();
239
- }
171
+ if (!entry.instance) {
172
+ try {
173
+ entry.instance = new entry.ServiceClass(name);
240
174
 
241
- if (entry.errorUnsubscribe) {
242
- entry.errorUnsubscribe();
243
- }
244
-
245
- // Remove from registry
246
- this.services.delete(name);
175
+ // Apply log level
176
+ const logLevel = this._getServiceLogLevel(name);
177
+ if (logLevel) {
178
+ entry.instance.setLogLevel(logLevel);
179
+ }
247
180
 
248
- // Update dependency graph
249
- this._updateDependencyGraph();
181
+ // Forward events
182
+ this._attachServiceEvents(name, entry.instance);
250
183
 
251
- this.logger.info(`Service '${name}' unregistered`);
252
- this._emit('service:unregistered', { service: name });
184
+ this.logger.debug(`Created instance for '${name}'`);
185
+ } catch (error) {
186
+ this.logger.error(`Failed to create instance for '${name}'`, error);
187
+ return null;
188
+ }
189
+ }
253
190
 
254
- return true;
191
+ return entry.instance;
255
192
  }
256
193
 
257
194
  /**
258
- * Get a service instance
195
+ * Initialize a service
259
196
  *
260
197
  * @param {string} name - Service name
261
- * @returns {Object|null} Service instance or null if not found
262
198
  *
263
- * @example
264
- * const db = manager.getService('database');
265
- * await db.query('SELECT * FROM users');
199
+ * @returns {Promise<boolean>} True if initialization succeeded
266
200
  */
267
- getService(name) {
268
- const entry = this.services.get(name);
269
- return entry ? entry.instance : null;
270
- }
201
+ async initService(name) {
202
+ const instance = this.get(name);
203
+ if (!instance) return false;
271
204
 
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
205
  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;
206
+ return await instance.initialize(entry.config);
342
207
  }
343
208
 
344
209
  /**
345
- * Start a specific service and its dependencies
210
+ * Start a service and its dependencies
346
211
  *
347
212
  * @param {string} name - Service name
348
- * @returns {Promise<boolean>} True if start was successful
349
213
  *
350
- * @example
351
- * await manager.startService('api');
214
+ * @returns {Promise<boolean>} True if service started successfully
352
215
  */
353
216
  async startService(name) {
354
217
  const entry = this.services.get(name);
355
218
  if (!entry) {
356
- this.logger.error(`Cannot start unknown service '${name}'`);
219
+ this.logger.warn(`Cannot start unregistered service '${name}'`);
357
220
  return false;
358
221
  }
359
222
 
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
223
  // 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) {
224
+ for (const dep of entry.dependencies) {
225
+ if (!await this.isRunning(dep)) {
226
+ this.logger.debug(`Starting dependency '${dep}' for '${name}'`);
227
+ const started = await this.startService(dep);
228
+ if (!started) {
381
229
  this.logger.error(
382
- `Cannot start service '${name}': ` +
383
- `dependency '${depName}' failed to start`
230
+ `Failed to start dependency '${dep}' for '${name}'`
384
231
  );
385
232
  return false;
386
233
  }
387
234
  }
388
235
  }
389
236
 
390
- // Start this service
391
- this.logger.debug(`Starting service '${name}'`);
392
- const result = await entry.instance.start();
237
+ const instance = this.get(name);
238
+ if (!instance) return false;
393
239
 
394
- if (result) {
395
- this.logger.info(`Service '${name}' started`);
396
- } else {
397
- this.logger.error(`Service '${name}' failed to start`);
240
+ // Initialize if needed
241
+ if (instance.state === CREATED || instance.state === DESTROYED) {
242
+ const initialized = await this.initService(name);
243
+ if (!initialized) return false;
398
244
  }
399
245
 
400
- return result;
246
+ return await instance.start();
401
247
  }
402
248
 
403
249
  /**
404
- * Start all services in dependency order
250
+ * Stop a service
405
251
  *
406
- * @returns {Promise<boolean>} True if all services started successfully
252
+ * @param {string} name - Service name
253
+ * @param {StopOptions} [options={}] - Stop options
407
254
  *
408
- * @example
409
- * await manager.startAll();
255
+ * @returns {Promise<boolean>} True if service stopped successfully
410
256
  */
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();
257
+ async stopService(name, options = {}) {
258
+ const instance = this.get(name);
259
+ if (!instance) {
260
+ this.logger.warn(`Cannot stop unregistered service '${name}'`);
261
+ return true; // Already stopped
262
+ }
419
263
 
420
- // Start services in order
421
- for (const name of orderedServices) {
422
- if (started.has(name)) {
423
- continue; // Already started as a dependency
264
+ // Check dependents
265
+ const entry = this.services.get(name);
266
+ if (!options.force && entry && entry.dependents.size > 0) {
267
+ const runningDependents = [];
268
+ for (const dep of entry.dependents) {
269
+ if (await this.isRunning(dep)) {
270
+ runningDependents.push(dep);
271
+ }
424
272
  }
425
273
 
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}'`);
274
+ if (runningDependents.length > 0) {
275
+ this.logger.warn(
276
+ `Cannot stop '${name}' - required by: ${runningDependents.join(', ')}`
277
+ );
278
+ return false;
433
279
  }
434
280
  }
435
281
 
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;
282
+ return await instance.stop(options);
443
283
  }
444
284
 
445
285
  /**
446
- * Stop a specific service and services that depend on it
286
+ * Recover a service from error state
447
287
  *
448
288
  * @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
289
  *
453
- * @example
454
- * await manager.stopService('database');
290
+ * @returns {Promise<boolean>} True if recovery succeeded
455
291
  */
456
- async stopService(name, options = {}) {
457
- const { force = false } = options;
458
- const entry = this.services.get(name);
292
+ async recoverService(name) {
293
+ const instance = this.get(name);
294
+ if (!instance) return false;
459
295
 
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
- }
296
+ return await instance.recover();
297
+ }
470
298
 
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
- }
299
+ /**
300
+ * Start all registered services in dependency order
301
+ *
302
+ * @returns {Promise<Object<string, boolean>>} Map of service results
303
+ */
304
+ async startAll() {
305
+ this.logger.info('Starting all services');
479
306
 
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
- }
307
+ // Sort by priority and dependencies
308
+ const sorted = this._topologicalSort();
309
+ const results = new Map();
489
310
 
490
- this.logger.warn(
491
- `Force stopping service '${name}' with dependents: ` +
492
- dependents.join(', ')
493
- );
311
+ for (const name of sorted) {
312
+ const success = await this.startService(name);
313
+ results.set(name, success);
494
314
 
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;
315
+ if (!success) {
316
+ this.logger.error(`Failed to start '${name}', stopping`);
317
+ // Mark remaining services as not started
318
+ for (const remaining of sorted) {
319
+ if (!results.has(remaining)) {
320
+ results.set(remaining, false);
321
+ }
503
322
  }
323
+ break;
504
324
  }
505
325
  }
506
326
 
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;
327
+ return Object.fromEntries(results);
518
328
  }
519
329
 
520
330
  /**
521
331
  * Stop all services in reverse dependency order
522
332
  *
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
333
+ * @param {StopOptions} [options={}] - Stop options
526
334
  *
527
- * @example
528
- * await manager.stopAll();
335
+ * @returns {Promise<Object<string, boolean>>} Map of service results
529
336
  */
530
337
  async stopAll(options = {}) {
531
- const { force = false } = options;
532
- let allSuccessful = true;
533
-
534
338
  this.logger.info('Stopping all services');
535
339
 
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 });
340
+ const stopOptions = {
341
+ timeout: options.timeout || this.config.stopTimeout,
342
+ force: options.force || false
343
+ };
545
344
 
546
- if (!success) {
547
- this.logger.error(`Failed to stop service '${name}'`);
548
- allSuccessful = false;
345
+ // Stop in reverse order
346
+ const sorted = this._topologicalSort().reverse();
347
+ const results = new Map();
348
+
349
+ // Handle global timeout if specified
350
+ if (stopOptions.timeout > 0) {
351
+ const timeoutPromise = new Promise((_, reject) =>
352
+ setTimeout(
353
+ () => reject(new Error('Global shutdown timeout')),
354
+ stopOptions.timeout
355
+ )
356
+ );
549
357
 
550
- if (!force) {
551
- break;
358
+ try {
359
+ // Race between stopping all services and timeout
360
+ await Promise.race([
361
+ this._stopAllSequentially(sorted, results, stopOptions),
362
+ timeoutPromise
363
+ ]);
364
+ } catch (error) {
365
+ if (error.message === 'Global shutdown timeout') {
366
+ this.logger.error('Global shutdown timeout reached');
367
+ // Mark any remaining services as failed
368
+ for (const name of sorted) {
369
+ if (!results.has(name)) {
370
+ results.set(name, false);
371
+ }
552
372
  }
373
+ } else {
374
+ throw error;
553
375
  }
554
376
  }
555
- }
556
-
557
- if (allSuccessful) {
558
- this.logger.info('All services stopped successfully');
559
377
  } else {
560
- this.logger.error('Some services failed to stop');
378
+ // No timeout, just stop sequentially
379
+ await this._stopAllSequentially(sorted, results, stopOptions);
561
380
  }
562
381
 
563
- return allSuccessful;
382
+ return Object.fromEntries(results);
564
383
  }
565
384
 
566
385
  /**
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
386
+ * Stop services sequentially
573
387
  *
574
- * @example
575
- * await manager.destroyService('api');
388
+ * @private
389
+ * @param {string[]} serviceNames - Ordered list of service names
390
+ * @param {Map<string, boolean>} results - Results map to populate
391
+ * @param {StopOptions} options - Stop options
576
392
  */
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;
393
+ async _stopAllSequentially(serviceNames, results, options) {
394
+ for (const name of serviceNames) {
395
+ try {
396
+ const success = await this.stopService(name, options);
397
+ results.set(name, success);
398
+ } catch (error) {
399
+ this.logger.error(`Error stopping '${name}'`, error);
400
+ results.set(name, false);
591
401
  }
592
402
  }
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
403
  }
625
404
 
626
405
  /**
627
- * Destroy all services and shutdown the manager
406
+ * Get health status for all services
628
407
  *
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();
408
+ * @returns {Promise<HealthCheckResult>} Health status for all services
635
409
  */
636
- async destroyAll(options = {}) {
637
- const { force = false } = options;
638
- let allSuccessful = true;
639
-
640
- this.logger.info('Destroying all services');
410
+ async checkHealth() {
411
+ const health = {};
641
412
 
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
- }
413
+ for (const [name, entry] of this.services) {
414
+ if (entry.instance) {
415
+ health[name] = await entry.instance.getHealth();
416
+ } else {
417
+ health[name] = {
418
+ name,
419
+ state: 'NOT_CREATED',
420
+ healthy: false
421
+ };
658
422
  }
659
423
  }
660
424
 
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;
425
+ return health;
669
426
  }
670
427
 
671
428
  /**
672
- * Recover a service and its dependencies from error state
429
+ * Check if a service is currently running
673
430
  *
674
431
  * @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
432
  *
684
- * // Recover just this service without auto-starting
685
- * await manager.recoverService('database', {
686
- * recursive: false,
687
- * autoStart: false
688
- * });
433
+ * @returns {Promise<boolean>} True if service is running
689
434
  */
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;
435
+ async isRunning(name) {
436
+ const instance = this.get(name);
437
+ return instance ? instance.state === RUNNING : false;
782
438
  }
783
439
 
784
440
  /**
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
441
+ * Set log level for a service or globally
790
442
  *
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 });
443
+ * @param {string} name - Service name or '*' for global
444
+ * @param {string} level - Log level to set
797
445
  */
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}'`);
446
+ setLogLevel(name, level) {
447
+ if (name === '*') {
448
+ // Global level
449
+ this.config.logConfig.globalLevel = level;
450
+
451
+ // Apply to all existing services
452
+ // eslint-disable-next-line no-unused-vars
453
+ for (const [_, entry] of this.services) {
454
+ if (entry.instance) {
455
+ entry.instance.setLogLevel(level);
833
456
  }
834
457
  }
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
458
  } 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;
459
+ // Service-specific level
460
+ if (!this.config.logConfig.serviceLevels) {
461
+ this.config.logConfig.serviceLevels = {};
882
462
  }
463
+ this.config.logConfig.serviceLevels[name] = level;
883
464
 
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
- }
465
+ // Apply to existing instance
466
+ const instance = this.get(name);
467
+ if (instance) {
468
+ instance.setLogLevel(level);
890
469
  }
891
-
892
- return allSuccess;
893
470
  }
894
471
  }
895
472
 
896
473
  /**
897
- * Get the names of all registered services
474
+ * Get all services with a specific tag
898
475
  *
899
- * @returns {string[]} List of service names
476
+ * @param {string} tag - Tag to filter by
900
477
  *
901
- * @example
902
- * const services = manager.getServiceNames();
903
- * console.log(`Registered services: ${services.join(', ')}`);
478
+ * @returns {string[]} Array of service names
904
479
  */
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
- });
480
+ getServicesByTag(tag) {
481
+ const services = [];
482
+ for (const [name, entry] of this.services) {
483
+ if (entry.tags.includes(tag)) {
484
+ services.push(name);
948
485
  }
949
-
950
- return statuses;
951
486
  }
487
+ return services;
952
488
  }
953
489
 
954
490
  // Private methods
955
491
 
956
492
  /**
957
- * Handle state change events from services
493
+ * Setup logging configuration based on config.dev
958
494
  *
959
495
  * @private
960
- * @param {string} serviceName - Service name
961
- * @param {Object} event - State change event
962
496
  */
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
- });
497
+ _setupLogging() {
498
+ // Set default log levels based on config.debug flag
499
+ if (this.config.debug) {
500
+ this.config.logConfig.defaultLevel = DEBUG;
501
+ } else {
502
+ this.config.logConfig.defaultLevel = WARN;
503
+ }
976
504
 
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 });
505
+ // Apply config
506
+ if (this.config.logConfig.globalLevel) {
507
+ this.logger.setLevel(this.config.logConfig.globalLevel);
993
508
  }
994
509
  }
995
510
 
996
511
  /**
997
- * Handle error events from services
512
+ * Get the appropriate log level for a service
998
513
  *
999
514
  * @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
515
+ * @param {string} name - Service name
1019
516
  *
1020
- * @private
517
+ * @returns {string|undefined} Log level or undefined
1021
518
  */
1022
- _updateDependencyGraph() {
1023
- this.dependencyGraph.clear();
519
+ _getServiceLogLevel(name) {
520
+ const config = this.config.logConfig;
1024
521
 
1025
- // Add all services to the graph
1026
- for (const name of this.services.keys()) {
1027
- this.dependencyGraph.set(name, new Set());
522
+ // Check in order of precedence:
523
+ // 1. Global level (overrides everything)
524
+ if (config.globalLevel) {
525
+ return config.globalLevel;
1028
526
  }
1029
527
 
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
- }
528
+ // 2. Service-specific level
529
+ if (config.serviceLevels?.[name]) {
530
+ return config.serviceLevels[name];
1035
531
  }
1036
532
 
1037
- // Check for circular dependencies
1038
- this._checkForCircularDependencies();
533
+ // 3. Don't use defaultLevel as it might be too restrictive
534
+ // Return undefined to let the service use its own default
535
+ return undefined;
1039
536
  }
1040
537
 
1041
538
  /**
1042
- * Check for circular dependencies in the graph
539
+ * Attach event listeners to forward service events
1043
540
  *
1044
541
  * @private
542
+ * @param {string} name - Service name
543
+ * @param {import('./typedef.js').ServiceBase} instance
544
+ * Service instance
1045
545
  */
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
- }
546
+ _attachServiceEvents(name, instance) {
547
+ // Forward service events
548
+ instance.on('stateChanged', (data) => {
549
+ this.emit('service:stateChanged', { ...data, service: name });
550
+ });
1070
551
 
1071
- recursionStack.delete(node);
1072
- return false;
1073
- };
552
+ instance.on('healthChanged', (data) => {
553
+ this.emit('service:healthChanged', { ...data, service: name });
554
+ });
1074
555
 
1075
- for (const node of this.dependencyGraph.keys()) {
1076
- if (checkNode(node)) {
1077
- break;
1078
- }
1079
- }
556
+ instance.on('error', (data) => {
557
+ this.emit('service:error', { ...data, service: name });
558
+ });
559
+
560
+ // Forward log events
561
+ instance.logger.on('log', (logEvent) => {
562
+ this.emit('service:log', { ...logEvent, service: name });
563
+ });
1080
564
  }
1081
565
 
1082
566
  /**
1083
- * Get optimal service start order based on dependencies
567
+ * Sort services by dependencies using topological sort
1084
568
  *
1085
569
  * @private
1086
- * @returns {string[]} Services in dependency order
570
+ *
571
+ * @returns {string[]} Service names in dependency order
572
+ * @throws {Error} If circular dependencies are detected
1087
573
  */
1088
- _getStartOrder() {
574
+ _topologicalSort() {
575
+ const sorted = [];
1089
576
  const visited = new Set();
1090
- const result = [];
577
+ const visiting = new Set();
1091
578
 
1092
- const visit = (node) => {
1093
- if (visited.has(node)) return;
579
+ const visit = (name) => {
580
+ if (visited.has(name)) return;
581
+ if (visiting.has(name)) {
582
+ throw new Error(`Circular dependency detected involving '${name}'`);
583
+ }
1094
584
 
1095
- visited.add(node);
585
+ visiting.add(name);
1096
586
 
1097
- const dependencies = this.dependencyGraph.get(node);
1098
- if (dependencies) {
1099
- for (const dep of dependencies) {
587
+ const entry = this.services.get(name);
588
+ if (entry) {
589
+ for (const dep of entry.dependencies) {
1100
590
  visit(dep);
1101
591
  }
1102
592
  }
1103
593
 
1104
- result.push(node);
594
+ visiting.delete(name);
595
+ visited.add(name);
596
+ sorted.push(name);
1105
597
  };
1106
598
 
1107
- // Visit all nodes
1108
- for (const node of this.dependencyGraph.keys()) {
1109
- visit(node);
599
+ // Visit all services
600
+ for (const name of this.services.keys()) {
601
+ visit(name);
1110
602
  }
1111
603
 
1112
- return result;
604
+ return sorted;
1113
605
  }
1114
606
  }
607
+
608
+ export default ServiceManager;