@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.
Files changed (31) hide show
  1. package/dist/classes/cache/IndexedDbCache.d.ts +212 -0
  2. package/dist/classes/cache/IndexedDbCache.js +673 -0
  3. package/dist/classes/cache/MemoryResponseCache.d.ts +101 -14
  4. package/dist/classes/cache/MemoryResponseCache.js +97 -12
  5. package/dist/classes/cache/index.d.ts +1 -1
  6. package/dist/classes/cache/index.js +2 -1
  7. package/dist/classes/events/EventEmitter.d.ts +142 -0
  8. package/dist/classes/events/EventEmitter.js +275 -0
  9. package/dist/classes/events/index.d.ts +1 -0
  10. package/dist/classes/events/index.js +2 -0
  11. package/dist/classes/logging/Logger.d.ts +74 -0
  12. package/dist/classes/logging/Logger.js +158 -0
  13. package/dist/classes/logging/constants.d.ts +14 -0
  14. package/dist/classes/logging/constants.js +18 -0
  15. package/dist/classes/logging/index.d.ts +2 -0
  16. package/dist/classes/logging/index.js +4 -0
  17. package/dist/classes/services/ServiceBase.d.ts +153 -0
  18. package/dist/classes/services/ServiceBase.js +409 -0
  19. package/dist/classes/services/ServiceManager.d.ts +350 -0
  20. package/dist/classes/services/ServiceManager.js +1114 -0
  21. package/dist/classes/services/constants.d.ts +11 -0
  22. package/dist/classes/services/constants.js +12 -0
  23. package/dist/classes/services/index.d.ts +3 -0
  24. package/dist/classes/services/index.js +5 -0
  25. package/dist/util/env/index.d.ts +1 -0
  26. package/dist/util/env/index.js +9 -0
  27. package/dist/util/http/caching.js +24 -12
  28. package/dist/util/http/http-request.js +12 -7
  29. package/package.json +2 -1
  30. package/dist/classes/cache/PersistentResponseCache.d.ts +0 -46
  31. /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
+ }