@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.
- package/dist/classes/services/ServiceBase.d.ts +83 -73
- package/dist/classes/services/ServiceBase.js +237 -181
- package/dist/classes/services/ServiceManager.d.ts +96 -267
- package/dist/classes/services/ServiceManager.js +368 -874
- package/dist/classes/services/_old/ServiceBase.d.ts +153 -0
- package/dist/classes/services/_old/ServiceBase.js +409 -0
- package/dist/classes/services/_old/ServiceManager.d.ts +350 -0
- package/dist/classes/services/_old/ServiceManager.js +1114 -0
- package/dist/classes/services/service-states.d.ts +154 -0
- package/dist/classes/services/service-states.js +199 -0
- package/dist/classes/services/typedef.d.ts +206 -0
- package/dist/classes/services/typedef.js +169 -0
- package/package.json +1 -1
- /package/dist/classes/services/{constants.d.ts → _old/constants.d.ts} +0 -0
- /package/dist/classes/services/{constants.js → _old/constants.js} +0 -0
- /package/dist/classes/services/{index.d.ts → _old/index.d.ts} +0 -0
- /package/dist/classes/services/{index.js → _old/index.js} +0 -0
@@ -1,1114 +1,608 @@
|
|
1
1
|
/**
|
2
|
-
* @fileoverview Service
|
2
|
+
* @fileoverview Service Manager for coordinating service lifecycle,
|
3
|
+
* dependencies, and health monitoring.
|
3
4
|
*
|
4
|
-
* The ServiceManager
|
5
|
-
* and
|
6
|
-
*
|
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
|
13
|
+
* import AuthService from './services/AuthService.js';
|
14
14
|
*
|
15
|
-
*
|
16
|
-
*
|
15
|
+
* const manager = new ServiceManager({
|
16
|
+
* debug: true,
|
17
|
+
* stopTimeout: 10000
|
18
|
+
* });
|
17
19
|
*
|
18
|
-
* // Register services
|
19
|
-
* manager.register('database',
|
20
|
-
*
|
20
|
+
* // Register services with dependencies
|
21
|
+
* manager.register('database', DatabaseService, {
|
22
|
+
* connectionString: 'postgres://localhost/myapp'
|
23
|
+
* });
|
21
24
|
*
|
22
|
-
*
|
23
|
-
*
|
24
|
-
*
|
25
|
-
*
|
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
|
31
|
+
* // Start all services
|
29
32
|
* await manager.startAll();
|
30
33
|
*
|
31
|
-
*
|
32
|
-
*
|
33
|
-
*
|
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
|
-
*
|
37
|
-
*
|
38
|
-
*
|
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
|
-
* //
|
41
|
-
*
|
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
|
-
|
54
|
-
|
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 {
|
63
|
-
* @
|
64
|
-
* @
|
65
|
-
* @
|
66
|
-
* @
|
67
|
-
* @
|
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
|
86
|
+
* Service Manager for lifecycle and dependency management
|
87
|
+
* @extends EventEmitter
|
72
88
|
*/
|
73
|
-
export
|
89
|
+
export class ServiceManager extends EventEmitter {
|
74
90
|
/**
|
75
|
-
* Create a new
|
91
|
+
* Create a new ServiceManager instance
|
76
92
|
*
|
77
|
-
* @param {
|
78
|
-
* @param {string} [options.logLevel=INFO] - Log level for the manager
|
93
|
+
* @param {ServiceManagerConfig} [config={}] - Manager configuration
|
79
94
|
*/
|
80
|
-
constructor(
|
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
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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 -
|
148
|
-
* @param {
|
149
|
-
* @param {
|
150
|
-
* @param {
|
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
|
-
* @
|
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,
|
125
|
+
register(name, ServiceClass, config = {}, options = {}) {
|
162
126
|
if (this.services.has(name)) {
|
163
|
-
|
164
|
-
return false;
|
127
|
+
throw new Error(`Service '${name}' already registered`);
|
165
128
|
}
|
166
129
|
|
167
|
-
|
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
|
-
|
132
|
+
ServiceClass,
|
133
|
+
instance: null,
|
182
134
|
config,
|
183
|
-
dependencies,
|
184
|
-
|
185
|
-
|
135
|
+
dependencies: options.dependencies || [],
|
136
|
+
dependents: new Set(),
|
137
|
+
tags: options.tags || [],
|
138
|
+
priority: options.priority || 0
|
186
139
|
};
|
187
140
|
|
188
|
-
//
|
189
|
-
entry.
|
190
|
-
this.
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
201
|
-
|
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
|
-
*
|
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
|
-
* @
|
216
|
-
*
|
161
|
+
* @returns {import('./typedef.js').ServiceBase|null}
|
162
|
+
* Service instance or null if not found
|
217
163
|
*/
|
218
|
-
|
164
|
+
get(name) {
|
219
165
|
const entry = this.services.get(name);
|
220
166
|
if (!entry) {
|
221
|
-
this.logger.warn(`Service '${name}' not
|
222
|
-
return
|
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
|
-
|
237
|
-
|
238
|
-
|
239
|
-
}
|
171
|
+
if (!entry.instance) {
|
172
|
+
try {
|
173
|
+
entry.instance = new entry.ServiceClass(name);
|
240
174
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
|
249
|
-
|
181
|
+
// Forward events
|
182
|
+
this._attachServiceEvents(name, entry.instance);
|
250
183
|
|
251
|
-
|
252
|
-
|
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
|
191
|
+
return entry.instance;
|
255
192
|
}
|
256
193
|
|
257
194
|
/**
|
258
|
-
*
|
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
|
-
* @
|
264
|
-
* const db = manager.getService('database');
|
265
|
-
* await db.query('SELECT * FROM users');
|
199
|
+
* @returns {Promise<boolean>} True if initialization succeeded
|
266
200
|
*/
|
267
|
-
|
268
|
-
const
|
269
|
-
|
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
|
-
|
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
|
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
|
-
* @
|
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.
|
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
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
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
|
-
`
|
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
|
-
|
391
|
-
|
392
|
-
const result = await entry.instance.start();
|
237
|
+
const instance = this.get(name);
|
238
|
+
if (!instance) return false;
|
393
239
|
|
394
|
-
if
|
395
|
-
|
396
|
-
|
397
|
-
|
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
|
246
|
+
return await instance.start();
|
401
247
|
}
|
402
248
|
|
403
249
|
/**
|
404
|
-
*
|
250
|
+
* Stop a service
|
405
251
|
*
|
406
|
-
* @
|
252
|
+
* @param {string} name - Service name
|
253
|
+
* @param {StopOptions} [options={}] - Stop options
|
407
254
|
*
|
408
|
-
* @
|
409
|
-
* await manager.startAll();
|
255
|
+
* @returns {Promise<boolean>} True if service stopped successfully
|
410
256
|
*/
|
411
|
-
async
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
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
|
-
//
|
421
|
-
|
422
|
-
|
423
|
-
|
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
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
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
|
-
|
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
|
-
*
|
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
|
-
* @
|
454
|
-
* await manager.stopService('database');
|
290
|
+
* @returns {Promise<boolean>} True if recovery succeeded
|
455
291
|
*/
|
456
|
-
async
|
457
|
-
const
|
458
|
-
|
292
|
+
async recoverService(name) {
|
293
|
+
const instance = this.get(name);
|
294
|
+
if (!instance) return false;
|
459
295
|
|
460
|
-
|
461
|
-
|
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
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
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
|
-
//
|
481
|
-
|
482
|
-
|
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
|
-
|
491
|
-
|
492
|
-
|
493
|
-
);
|
311
|
+
for (const name of sorted) {
|
312
|
+
const success = await this.startService(name);
|
313
|
+
results.set(name, success);
|
494
314
|
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
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
|
-
|
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 {
|
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
|
-
* @
|
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
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
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
|
-
|
547
|
-
|
548
|
-
|
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
|
-
|
551
|
-
|
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
|
-
|
378
|
+
// No timeout, just stop sequentially
|
379
|
+
await this._stopAllSequentially(sorted, results, stopOptions);
|
561
380
|
}
|
562
381
|
|
563
|
-
return
|
382
|
+
return Object.fromEntries(results);
|
564
383
|
}
|
565
384
|
|
566
385
|
/**
|
567
|
-
*
|
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
|
-
* @
|
575
|
-
*
|
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
|
578
|
-
const
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
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
|
-
*
|
406
|
+
* Get health status for all services
|
628
407
|
*
|
629
|
-
* @
|
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
|
637
|
-
const
|
638
|
-
let allSuccessful = true;
|
639
|
-
|
640
|
-
this.logger.info('Destroying all services');
|
410
|
+
async checkHealth() {
|
411
|
+
const health = {};
|
641
412
|
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
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
|
-
|
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
|
-
*
|
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
|
-
*
|
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
|
691
|
-
const
|
692
|
-
|
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
|
-
*
|
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
|
-
* @
|
792
|
-
*
|
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
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
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
|
-
//
|
877
|
-
|
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
|
-
//
|
885
|
-
|
886
|
-
|
887
|
-
|
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
|
474
|
+
* Get all services with a specific tag
|
898
475
|
*
|
899
|
-
* @
|
476
|
+
* @param {string} tag - Tag to filter by
|
900
477
|
*
|
901
|
-
* @
|
902
|
-
* const services = manager.getServiceNames();
|
903
|
-
* console.log(`Registered services: ${services.join(', ')}`);
|
478
|
+
* @returns {string[]} Array of service names
|
904
479
|
*/
|
905
|
-
|
906
|
-
|
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
|
-
*
|
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
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
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
|
-
//
|
978
|
-
if (
|
979
|
-
this.
|
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
|
-
*
|
512
|
+
* Get the appropriate log level for a service
|
998
513
|
*
|
999
514
|
* @private
|
1000
|
-
* @param {string}
|
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
|
-
* @
|
517
|
+
* @returns {string|undefined} Log level or undefined
|
1021
518
|
*/
|
1022
|
-
|
1023
|
-
this.
|
519
|
+
_getServiceLogLevel(name) {
|
520
|
+
const config = this.config.logConfig;
|
1024
521
|
|
1025
|
-
//
|
1026
|
-
|
1027
|
-
|
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
|
-
//
|
1031
|
-
|
1032
|
-
|
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
|
-
//
|
1038
|
-
|
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
|
-
*
|
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
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
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
|
-
|
1072
|
-
|
1073
|
-
};
|
552
|
+
instance.on('healthChanged', (data) => {
|
553
|
+
this.emit('service:healthChanged', { ...data, service: name });
|
554
|
+
});
|
1074
555
|
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
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
|
-
*
|
567
|
+
* Sort services by dependencies using topological sort
|
1084
568
|
*
|
1085
569
|
* @private
|
1086
|
-
*
|
570
|
+
*
|
571
|
+
* @returns {string[]} Service names in dependency order
|
572
|
+
* @throws {Error} If circular dependencies are detected
|
1087
573
|
*/
|
1088
|
-
|
574
|
+
_topologicalSort() {
|
575
|
+
const sorted = [];
|
1089
576
|
const visited = new Set();
|
1090
|
-
const
|
577
|
+
const visiting = new Set();
|
1091
578
|
|
1092
|
-
const visit = (
|
1093
|
-
if (visited.has(
|
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
|
-
|
585
|
+
visiting.add(name);
|
1096
586
|
|
1097
|
-
const
|
1098
|
-
if (
|
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
|
-
|
594
|
+
visiting.delete(name);
|
595
|
+
visited.add(name);
|
596
|
+
sorted.push(name);
|
1105
597
|
};
|
1106
598
|
|
1107
|
-
// Visit all
|
1108
|
-
for (const
|
1109
|
-
visit(
|
599
|
+
// Visit all services
|
600
|
+
for (const name of this.services.keys()) {
|
601
|
+
visit(name);
|
1110
602
|
}
|
1111
603
|
|
1112
|
-
return
|
604
|
+
return sorted;
|
1113
605
|
}
|
1114
606
|
}
|
607
|
+
|
608
|
+
export default ServiceManager;
|