@hkdigital/lib-sveltekit 0.2.20 → 0.2.21
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/README.md +135 -135
- package/dist/assets/autospuiten/car-paint-picker.js +41 -41
- package/dist/assets/autospuiten/labels.js +7 -7
- package/dist/classes/cache/IndexedDbCache.js +1407 -1407
- package/dist/classes/cache/MemoryResponseCache.js +138 -138
- package/dist/classes/cache/index.js +5 -5
- package/dist/classes/cache/typedef.js +41 -41
- package/dist/classes/data/IterableTree.js +243 -243
- package/dist/classes/data/Selector.js +190 -190
- package/dist/classes/data/index.js +2 -2
- package/dist/classes/events/EventEmitter.js +275 -275
- package/dist/classes/events/index.js +2 -2
- package/dist/classes/index.js +4 -4
- package/dist/classes/logging/Logger.js +210 -210
- package/dist/classes/logging/constants.js +16 -16
- package/dist/classes/logging/index.js +4 -4
- package/dist/classes/logging/typedef.js +17 -17
- package/dist/classes/promise/HkPromise.js +377 -377
- package/dist/classes/promise/index.js +1 -1
- package/dist/classes/services/ServiceBase.js +463 -463
- package/dist/classes/services/ServiceManager.js +614 -614
- package/dist/classes/services/index.js +5 -5
- package/dist/classes/services/service-states.js +205 -205
- package/dist/classes/services/typedef.js +179 -179
- package/dist/classes/stores/SubscribersCount.js +107 -107
- package/dist/classes/stores/index.js +1 -1
- package/dist/classes/streams/LogTransformStream.js +19 -19
- package/dist/classes/streams/ServerEventsStore.js +110 -110
- package/dist/classes/streams/TimeStampSource.js +26 -26
- package/dist/classes/streams/index.js +3 -3
- package/dist/classes/svelte/audio/AudioLoader.svelte.js +58 -58
- package/dist/classes/svelte/audio/AudioScene.svelte.js +324 -324
- package/dist/classes/svelte/audio/mocks.js +35 -35
- package/dist/classes/svelte/finite-state-machine/FiniteStateMachine.svelte.js +133 -133
- package/dist/classes/svelte/finite-state-machine/index.js +1 -1
- package/dist/classes/svelte/image/ImageLoader.svelte.js +45 -45
- package/dist/classes/svelte/image/ImageScene.svelte.js +249 -249
- package/dist/classes/svelte/image/ImageVariantsLoader.svelte.js +152 -152
- package/dist/classes/svelte/image/index.js +4 -4
- package/dist/classes/svelte/image/mocks.js +35 -35
- package/dist/classes/svelte/image/typedef.js +8 -8
- package/dist/classes/svelte/index.js +14 -14
- package/dist/classes/svelte/loading-state-machine/LoadingStateMachine.svelte.js +109 -109
- package/dist/classes/svelte/loading-state-machine/constants.js +16 -16
- package/dist/classes/svelte/loading-state-machine/index.js +3 -3
- package/dist/classes/svelte/network-loader/NetworkLoader.svelte.js +338 -338
- package/dist/classes/svelte/network-loader/constants.js +3 -3
- package/dist/classes/svelte/network-loader/index.js +3 -3
- package/dist/classes/svelte/network-loader/mocks.js +30 -30
- package/dist/classes/svelte/network-loader/typedef.js +8 -8
- package/dist/components/area/HkArea.svelte +49 -49
- package/dist/components/area/HkGridArea.svelte +77 -77
- package/dist/components/area/index.js +2 -2
- package/dist/components/buttons/button/Button.svelte +82 -82
- package/dist/components/buttons/button-icon-steeze/SteezeIconButton.svelte +30 -30
- package/dist/components/buttons/button-text/TextButton.svelte +21 -21
- package/dist/components/buttons/index.js +3 -3
- package/dist/components/debug/debug-panel-design-scaling/DebugPanelDesignScaling.svelte +146 -146
- package/dist/components/debug/index.js +1 -1
- package/dist/components/drag-drop/DragController.js +44 -44
- package/dist/components/drag-drop/DragDropContext.svelte +111 -111
- package/dist/components/drag-drop/Draggable.svelte +519 -519
- package/dist/components/drag-drop/DropZoneArea.svelte +119 -119
- package/dist/components/drag-drop/DropZoneList.svelte +125 -125
- package/dist/components/drag-drop/{DropZone.svelte → Dropzone.svelte} +258 -258
- package/dist/components/drag-drop/actions.js +26 -26
- package/dist/components/drag-drop/drag-state.svelte.js +322 -322
- package/dist/components/drag-drop/index.js +7 -7
- package/dist/components/drag-drop/util.js +85 -85
- package/dist/components/hkdev/blocks/TextBlock.svelte +46 -46
- package/dist/components/hkdev/buttons/CheckButton.svelte +62 -62
- package/dist/components/icons/HkIcon.svelte +86 -86
- package/dist/components/icons/HkTabIcon.svelte +116 -116
- package/dist/components/icons/SteezeIcon.svelte +97 -97
- package/dist/components/icons/index.js +6 -6
- package/dist/components/icons/typedef.js +16 -16
- package/dist/components/index.js +2 -2
- package/dist/components/inputs/index.js +1 -1
- package/dist/components/inputs/text-input/TestTextInput.svelte__ +102 -102
- package/dist/components/inputs/text-input/TextInput.svelte +223 -223
- package/dist/components/inputs/text-input/TextInput.svelte___ +83 -83
- package/dist/components/inputs/text-input/assets/IconInvalid.svelte +14 -14
- package/dist/components/inputs/text-input/assets/IconValid.svelte +12 -12
- package/dist/components/layout/grid-layers/GridLayers.svelte +63 -63
- package/dist/components/layout/grid-layers/util.js +74 -74
- package/dist/components/layout/index.js +1 -1
- package/dist/components/panels/index.js +1 -1
- package/dist/components/panels/panel/Panel.svelte +43 -43
- package/dist/components/rows/index.js +3 -3
- package/dist/components/rows/panel-grid-row/PanelGridRow.svelte +104 -104
- package/dist/components/rows/panel-row-2/PanelRow2.svelte +40 -40
- package/dist/components/tab-bar/HkTabBar.state.svelte.js +149 -149
- package/dist/components/tab-bar/HkTabBar.svelte +74 -74
- package/dist/components/tab-bar/HkTabBarSelector.state.svelte.js +93 -93
- package/dist/components/tab-bar/HkTabBarSelector.svelte +49 -49
- package/dist/components/tab-bar/index.js +17 -17
- package/dist/components/tab-bar/typedef.js +11 -11
- package/dist/config/imagetools-config.js +189 -189
- package/dist/config/imagetools.d.ts +72 -72
- package/dist/constants/bases.js +13 -13
- package/dist/constants/errors/api.js +9 -9
- package/dist/constants/errors/generic.js +5 -5
- package/dist/constants/errors/index.js +3 -3
- package/dist/constants/errors/jwt.js +5 -5
- package/dist/constants/http/headers.js +6 -6
- package/dist/constants/http/index.js +2 -2
- package/dist/constants/http/methods.js +14 -14
- package/dist/constants/index.js +3 -3
- package/dist/constants/mime/application.js +5 -5
- package/dist/constants/mime/audio.js +13 -13
- package/dist/constants/mime/image.js +3 -3
- package/dist/constants/mime/index.js +4 -4
- package/dist/constants/mime/text.js +2 -2
- package/dist/constants/regexp/index.js +31 -31
- package/dist/constants/regexp/inspiratie.js__ +95 -95
- package/dist/constants/regexp/text.js +49 -49
- package/dist/constants/regexp/user.js +32 -32
- package/dist/constants/regexp/web.js +3 -3
- package/dist/constants/state-labels/drag-states.js +6 -6
- package/dist/constants/state-labels/drop-states.js +6 -6
- package/dist/constants/state-labels/input-states.js +11 -11
- package/dist/constants/state-labels/submit-states.js +4 -4
- package/dist/constants/time.js +28 -28
- package/dist/css/utilities.css +43 -43
- package/dist/design/design-config.js +73 -73
- package/dist/design/tailwind-theme-extend.js +158 -158
- package/dist/features/button-group/ButtonGroup.svelte +82 -82
- package/dist/features/button-group/typedef.js +10 -10
- package/dist/features/compare-left-right/CompareLeftRight.svelte +179 -179
- package/dist/features/compare-left-right/index.js +1 -1
- package/dist/features/game-box/GameBox.svelte +577 -577
- package/dist/features/game-box/gamebox.util.js +83 -83
- package/dist/features/hk-app-layout/HkAppLayout.state.svelte.js +25 -25
- package/dist/features/hk-app-layout/HkAppLayout.svelte +251 -251
- package/dist/features/image-box/ImageBox.svelte +210 -210
- package/dist/features/image-box/index.js +5 -5
- package/dist/features/image-box/typedef.js +32 -32
- package/dist/features/index.js +23 -23
- package/dist/features/presenter/ImageSlide.svelte +64 -64
- package/dist/features/presenter/Presenter.state.svelte.js +638 -638
- package/dist/features/presenter/Presenter.svelte +142 -142
- package/dist/features/presenter/constants.js +7 -7
- package/dist/features/presenter/index.js +10 -10
- package/dist/features/presenter/typedef.js +106 -106
- package/dist/features/presenter/util.js +210 -210
- package/dist/features/virtual-viewport/VirtualViewport.svelte +196 -196
- package/dist/logging/adapters/console.js +114 -114
- package/dist/logging/adapters/pino.js +60 -60
- package/dist/logging/constants.js +1 -1
- package/dist/logging/factories/client.js +21 -21
- package/dist/logging/factories/server.js +22 -22
- package/dist/logging/factories/universal.js +23 -23
- package/dist/logging/index.js +8 -8
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/validate-url.js +180 -180
- package/dist/server/index.js +1 -1
- package/dist/server/logger.js +94 -94
- package/dist/states/index.js +1 -1
- package/dist/states/navigation.svelte.js +55 -55
- package/dist/stores/index.js +1 -1
- package/dist/stores/theme.js +80 -80
- package/dist/themes/hkdev/components/blocks/text-block.css +34 -41
- package/dist/themes/hkdev/components/boxes/game-box.css +11 -12
- package/dist/themes/hkdev/components/buttons/button-icon-steeze.css +22 -22
- package/dist/themes/hkdev/components/buttons/button-text.css +32 -32
- package/dist/themes/hkdev/components/buttons/button.css +146 -146
- package/dist/themes/hkdev/components/buttons/skip-button.css +5 -6
- package/dist/themes/hkdev/components/drag-drop/draggable.css +73 -73
- package/dist/themes/hkdev/components/drag-drop/drop-zone.css +58 -48
- package/dist/themes/hkdev/components/icons/icon-steeze.css +16 -22
- package/dist/themes/hkdev/components/inputs/text-input.css +102 -104
- package/dist/themes/hkdev/components/panels/panel.css +25 -27
- package/dist/themes/hkdev/components/rows/panel-grid-row.css +4 -6
- package/dist/themes/hkdev/components/rows/panel-row-2.css +5 -7
- package/dist/themes/hkdev/components.css +29 -53
- package/dist/themes/hkdev/debug.css +1 -1
- package/dist/themes/hkdev/global/layout.css +32 -39
- package/dist/themes/hkdev/global/on-colors.css +32 -53
- package/dist/themes/hkdev/globals.css +4 -11
- package/dist/themes/hkdev/responsive.css +12 -12
- package/dist/themes/hkdev/theme-ext.js +12 -15
- package/dist/themes/hkdev/theme.css +219 -0
- package/dist/themes/index.d.ts +1 -1
- package/dist/themes/index.js +1 -1
- package/dist/typedef/context.js +6 -6
- package/dist/typedef/drag.js +25 -25
- package/dist/typedef/drop.js +12 -12
- package/dist/typedef/image.js +38 -38
- package/dist/typedef/index.js +4 -4
- package/dist/util/array/index.js +436 -436
- package/dist/util/bases/base58.js +262 -262
- package/dist/util/bases/index.js +1 -1
- package/dist/util/compare/index.js +247 -247
- package/dist/util/css/css-vars.js +83 -83
- package/dist/util/css/index.js +1 -1
- package/dist/util/design-system/components/states.js +22 -22
- package/dist/util/design-system/css/clamp.js +66 -66
- package/dist/util/design-system/css/root-design-vars.js +102 -102
- package/dist/util/design-system/index.js +5 -5
- package/dist/util/design-system/layout/scaling.js +228 -228
- package/dist/util/design-system/skeleton.js +208 -208
- package/dist/util/design-system/tailwind.js +288 -288
- package/dist/util/env/index.js +9 -9
- package/dist/util/expect/arrays.js +47 -47
- package/dist/util/expect/index.js +259 -259
- package/dist/util/expect/primitives.js +55 -55
- package/dist/util/expect/url.js +60 -60
- package/dist/util/function/index.js +218 -218
- package/dist/util/geo/index.js +26 -26
- package/dist/util/http/caching.js +263 -263
- package/dist/util/http/errors.js +97 -97
- package/dist/util/http/headers.js +75 -75
- package/dist/util/http/http-request.js +578 -578
- package/dist/util/http/index.js +22 -22
- package/dist/util/http/json-request.js +224 -224
- package/dist/util/http/mocks.js +65 -65
- package/dist/util/http/response.js +294 -294
- package/dist/util/http/typedef.js +93 -93
- package/dist/util/http/url.js +52 -52
- package/dist/util/image/index.js +86 -86
- package/dist/util/index.js +2 -2
- package/dist/util/is/index.js +140 -140
- package/dist/util/iterate/index.js +234 -234
- package/dist/util/object/index.js +1361 -1361
- package/dist/util/singleton/index.js +97 -97
- package/dist/util/string/array-path.js +75 -75
- package/dist/util/string/convert.js +54 -54
- package/dist/util/string/fs.js +226 -226
- package/dist/util/string/index.js +5 -5
- package/dist/util/string/interpolate.js +61 -61
- package/dist/util/string/pad.js +10 -10
- package/dist/util/svelte/index.js +4 -4
- package/dist/util/svelte/loading/loading-tracker.svelte.js +108 -108
- package/dist/util/svelte/observe/index.js +49 -49
- package/dist/util/svelte/state-context/index.js +117 -117
- package/dist/util/svelte/wait/index.js +38 -38
- package/dist/util/sveltekit/index.js +1 -1
- package/dist/util/sveltekit/route-folders/index.js +101 -101
- package/dist/util/time/index.js +323 -323
- package/dist/util/unique/index.js +249 -249
- package/dist/valibot/date.js__ +10 -10
- package/dist/valibot/index.js +9 -9
- package/dist/valibot/url.js +95 -95
- package/dist/valibot/user.js +23 -23
- package/dist/zod/all.js +33 -33
- package/dist/zod/generic.js +11 -11
- package/dist/zod/javascript.js +32 -32
- package/dist/zod/user.js +16 -16
- package/dist/zod/web.js +52 -52
- package/package.json +132 -129
- package/dist/components/layout/grid-layers/GridLayers.svelte__heightFrom__ +0 -372
- package/dist/themes/hkdev/theme.d.ts +0 -234
- package/dist/themes/hkdev/theme.js +0 -235
- package/dist/util/http/test-data__/content-length-test-hkdigital-small.V4HfZyBQ.avif +0 -0
@@ -1,614 +1,614 @@
|
|
1
|
-
/**
|
2
|
-
* @fileoverview Service Manager for coordinating service lifecycle,
|
3
|
-
* dependencies, and health monitoring.
|
4
|
-
*
|
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
|
-
*
|
9
|
-
* @example
|
10
|
-
* // Basic usage
|
11
|
-
* import { ServiceManager } from './ServiceManager.js';
|
12
|
-
* import DatabaseService from './services/DatabaseService.js';
|
13
|
-
* import AuthService from './services/AuthService.js';
|
14
|
-
*
|
15
|
-
* const manager = new ServiceManager({
|
16
|
-
* debug: true,
|
17
|
-
* stopTimeout: 10000
|
18
|
-
* });
|
19
|
-
*
|
20
|
-
* // Register services with dependencies
|
21
|
-
* manager.register('database', DatabaseService, {
|
22
|
-
* connectionString: 'postgres://localhost/myapp'
|
23
|
-
* });
|
24
|
-
*
|
25
|
-
* manager.register('auth', AuthService, {
|
26
|
-
* secret: process.env.JWT_SECRET
|
27
|
-
* }, {
|
28
|
-
* dependencies: ['database'] // auth depends on database
|
29
|
-
* });
|
30
|
-
*
|
31
|
-
* // Start all services
|
32
|
-
* await manager.startAll();
|
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);
|
50
|
-
* });
|
51
|
-
*
|
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');
|
59
|
-
*
|
60
|
-
* // Listen to all service logs
|
61
|
-
* manager.on('service:log', (logEvent) => {
|
62
|
-
* writeToLogFile(logEvent);
|
63
|
-
* });
|
64
|
-
*/
|
65
|
-
|
66
|
-
import { EventEmitter } from '../events';
|
67
|
-
import { Logger, DEBUG, INFO, WARN } from '../logging';
|
68
|
-
|
69
|
-
import {
|
70
|
-
NOT_CREATED,
|
71
|
-
CREATED,
|
72
|
-
RUNNING,
|
73
|
-
DESTROYED
|
74
|
-
} from './service-states.js';
|
75
|
-
|
76
|
-
/**
|
77
|
-
* @typedef {import('./typedef.js').ServiceConstructor} ServiceConstructor
|
78
|
-
* @typedef {import('./typedef.js').ServiceConfig} ServiceConfig
|
79
|
-
* @typedef {import('./typedef.js').ServiceRegistrationOptions} ServiceRegistrationOptions
|
80
|
-
* @typedef {import('./typedef.js').ServiceManagerConfig} ServiceManagerConfig
|
81
|
-
* @typedef {import('./typedef.js').StopOptions} StopOptions
|
82
|
-
* @typedef {import('./typedef.js').ServiceEntry} ServiceEntry
|
83
|
-
* @typedef {import('./typedef.js').HealthCheckResult} HealthCheckResult
|
84
|
-
*/
|
85
|
-
|
86
|
-
/**
|
87
|
-
* Service Manager for lifecycle and dependency management
|
88
|
-
* @extends EventEmitter
|
89
|
-
*/
|
90
|
-
export class ServiceManager extends EventEmitter {
|
91
|
-
/**
|
92
|
-
* Create a new ServiceManager instance
|
93
|
-
*
|
94
|
-
* @param {ServiceManagerConfig} [config={}] - Manager configuration
|
95
|
-
*/
|
96
|
-
constructor(config = {}) {
|
97
|
-
super();
|
98
|
-
|
99
|
-
/** @type {Map<string, ServiceEntry>} */
|
100
|
-
this.services = new Map();
|
101
|
-
|
102
|
-
/** @type {Logger} */
|
103
|
-
this.logger = new Logger('ServiceManager', config.logLevel || INFO);
|
104
|
-
|
105
|
-
/** @type {ServiceManagerConfig} */
|
106
|
-
this.config = {
|
107
|
-
debug: config.debug ?? false,
|
108
|
-
autoStart: config.autoStart ?? false,
|
109
|
-
stopTimeout: config.stopTimeout || 10000,
|
110
|
-
logConfig: config.logConfig || {}
|
111
|
-
};
|
112
|
-
|
113
|
-
this._setupLogging();
|
114
|
-
}
|
115
|
-
|
116
|
-
/**
|
117
|
-
* Register a service class with the manager
|
118
|
-
*
|
119
|
-
* @param {string} name - Unique service identifier
|
120
|
-
* @param {ServiceConstructor} ServiceClass - Service class constructor
|
121
|
-
* @param {ServiceConfig} [config={}] - Service configuration
|
122
|
-
* @param {ServiceRegistrationOptions} [options={}] - Registration options
|
123
|
-
*
|
124
|
-
* @throws {Error} If service name is already registered
|
125
|
-
*/
|
126
|
-
register(name, ServiceClass, config = {}, options = {}) {
|
127
|
-
if (this.services.has(name)) {
|
128
|
-
throw new Error(`Service '${name}' already registered`);
|
129
|
-
}
|
130
|
-
|
131
|
-
/** @type {ServiceEntry} */
|
132
|
-
const entry = {
|
133
|
-
ServiceClass,
|
134
|
-
instance: null,
|
135
|
-
config,
|
136
|
-
dependencies: options.dependencies || [],
|
137
|
-
dependents: new Set(),
|
138
|
-
tags: options.tags || [],
|
139
|
-
priority: options.priority || 0
|
140
|
-
};
|
141
|
-
|
142
|
-
// Track dependents
|
143
|
-
entry.dependencies.forEach(dep => {
|
144
|
-
const depEntry = this.services.get(dep);
|
145
|
-
if (depEntry) {
|
146
|
-
depEntry.dependents.add(name);
|
147
|
-
}
|
148
|
-
});
|
149
|
-
|
150
|
-
this.services.set(name, entry);
|
151
|
-
|
152
|
-
this.logger.debug(`Registered service '${name}'`, {
|
153
|
-
dependencies: entry.dependencies,
|
154
|
-
tags: entry.tags
|
155
|
-
});
|
156
|
-
}
|
157
|
-
|
158
|
-
/**
|
159
|
-
* Get or create a service instance
|
160
|
-
*
|
161
|
-
* @param {string} name - Service name
|
162
|
-
*
|
163
|
-
* @returns {import('./typedef.js').ServiceInstance|null}
|
164
|
-
* Service instance or null if not found
|
165
|
-
*/
|
166
|
-
get(name) {
|
167
|
-
const entry = this.services.get(name);
|
168
|
-
if (!entry) {
|
169
|
-
this.logger.warn(`Service '${name}' not found`);
|
170
|
-
return null;
|
171
|
-
}
|
172
|
-
|
173
|
-
if (!entry.instance) {
|
174
|
-
try {
|
175
|
-
entry.instance = new entry.ServiceClass(name);
|
176
|
-
|
177
|
-
// Apply log level
|
178
|
-
const logLevel = this._getServiceLogLevel(name);
|
179
|
-
if (logLevel) {
|
180
|
-
entry.instance.setLogLevel(logLevel);
|
181
|
-
}
|
182
|
-
|
183
|
-
// Forward events
|
184
|
-
this._attachServiceEvents(name, entry.instance);
|
185
|
-
|
186
|
-
this.logger.debug(`Created instance for '${name}'`);
|
187
|
-
} catch (error) {
|
188
|
-
this.logger.error(`Failed to create instance for '${name}'`, error);
|
189
|
-
return null;
|
190
|
-
}
|
191
|
-
}
|
192
|
-
|
193
|
-
return entry.instance;
|
194
|
-
}
|
195
|
-
|
196
|
-
/**
|
197
|
-
* Initialize a service
|
198
|
-
*
|
199
|
-
* @param {string} name - Service name
|
200
|
-
*
|
201
|
-
* @returns {Promise<boolean>} True if initialization succeeded
|
202
|
-
*/
|
203
|
-
async initService(name) {
|
204
|
-
const instance = this.get(name);
|
205
|
-
if (!instance) return false;
|
206
|
-
|
207
|
-
const entry = this.services.get(name);
|
208
|
-
return await instance.initialize(entry.config);
|
209
|
-
}
|
210
|
-
|
211
|
-
/**
|
212
|
-
* Start a service and its dependencies
|
213
|
-
*
|
214
|
-
* @param {string} name - Service name
|
215
|
-
*
|
216
|
-
* @returns {Promise<boolean>} True if service started successfully
|
217
|
-
*/
|
218
|
-
async startService(name) {
|
219
|
-
const entry = this.services.get(name);
|
220
|
-
if (!entry) {
|
221
|
-
this.logger.warn(`Cannot start unregistered service '${name}'`);
|
222
|
-
return false;
|
223
|
-
}
|
224
|
-
|
225
|
-
// Start dependencies first
|
226
|
-
for (const dep of entry.dependencies) {
|
227
|
-
if (!await this.isRunning(dep)) {
|
228
|
-
|
229
|
-
this.logger.debug(`Starting dependency '${dep}' for '${name}'`);
|
230
|
-
|
231
|
-
const started = await this.startService(dep);
|
232
|
-
if (!started) {
|
233
|
-
this.logger.error(
|
234
|
-
`Failed to start dependency '${dep}' for '${name}'`
|
235
|
-
);
|
236
|
-
return false;
|
237
|
-
}
|
238
|
-
}
|
239
|
-
}
|
240
|
-
|
241
|
-
const instance = this.get(name);
|
242
|
-
if (!instance) return false;
|
243
|
-
|
244
|
-
// Initialize if needed
|
245
|
-
if (instance.state === CREATED || instance.state === DESTROYED) {
|
246
|
-
const initialized = await this.initService(name);
|
247
|
-
if (!initialized) return false;
|
248
|
-
}
|
249
|
-
|
250
|
-
return await instance.start();
|
251
|
-
}
|
252
|
-
|
253
|
-
/**
|
254
|
-
* Stop a service
|
255
|
-
*
|
256
|
-
* @param {string} name - Service name
|
257
|
-
* @param {StopOptions} [options={}] - Stop options
|
258
|
-
*
|
259
|
-
* @returns {Promise<boolean>} True if service stopped successfully
|
260
|
-
*/
|
261
|
-
async stopService(name, options = {}) {
|
262
|
-
const instance = this.get(name);
|
263
|
-
if (!instance) {
|
264
|
-
this.logger.warn(`Cannot stop unregistered service '${name}'`);
|
265
|
-
return true; // Already stopped
|
266
|
-
}
|
267
|
-
|
268
|
-
// Check dependents
|
269
|
-
const entry = this.services.get(name);
|
270
|
-
if (!options.force && entry && entry.dependents.size > 0) {
|
271
|
-
const runningDependents = [];
|
272
|
-
for (const dep of entry.dependents) {
|
273
|
-
if (await this.isRunning(dep)) {
|
274
|
-
runningDependents.push(dep);
|
275
|
-
}
|
276
|
-
}
|
277
|
-
|
278
|
-
if (runningDependents.length > 0) {
|
279
|
-
this.logger.warn(
|
280
|
-
`Cannot stop '${name}' - required by: ${runningDependents.join(', ')}`
|
281
|
-
);
|
282
|
-
return false;
|
283
|
-
}
|
284
|
-
}
|
285
|
-
|
286
|
-
return await instance.stop(options);
|
287
|
-
}
|
288
|
-
|
289
|
-
/**
|
290
|
-
* Recover a service from error state
|
291
|
-
*
|
292
|
-
* @param {string} name - Service name
|
293
|
-
*
|
294
|
-
* @returns {Promise<boolean>} True if recovery succeeded
|
295
|
-
*/
|
296
|
-
async recoverService(name) {
|
297
|
-
const instance = this.get(name);
|
298
|
-
if (!instance) return false;
|
299
|
-
|
300
|
-
return await instance.recover();
|
301
|
-
}
|
302
|
-
|
303
|
-
/**
|
304
|
-
* Start all registered services in dependency order
|
305
|
-
*
|
306
|
-
* @returns {Promise<Object<string, boolean>>} Map of service results
|
307
|
-
*/
|
308
|
-
async startAll() {
|
309
|
-
this.logger.info('Starting all services');
|
310
|
-
|
311
|
-
// Sort by priority and dependencies
|
312
|
-
const sorted = this._topologicalSort();
|
313
|
-
const results = new Map();
|
314
|
-
|
315
|
-
for (const name of sorted) {
|
316
|
-
const success = await this.startService(name);
|
317
|
-
results.set(name, success);
|
318
|
-
|
319
|
-
if (!success) {
|
320
|
-
this.logger.error(`Failed to start '${name}', stopping`);
|
321
|
-
// Mark remaining services as not started
|
322
|
-
for (const remaining of sorted) {
|
323
|
-
if (!results.has(remaining)) {
|
324
|
-
results.set(remaining, false);
|
325
|
-
}
|
326
|
-
}
|
327
|
-
break;
|
328
|
-
}
|
329
|
-
}
|
330
|
-
|
331
|
-
return Object.fromEntries(results);
|
332
|
-
}
|
333
|
-
|
334
|
-
/**
|
335
|
-
* Stop all services in reverse dependency order
|
336
|
-
*
|
337
|
-
* @param {StopOptions} [options={}] - Stop options
|
338
|
-
*
|
339
|
-
* @returns {Promise<Object<string, boolean>>} Map of service results
|
340
|
-
*/
|
341
|
-
async stopAll(options = {}) {
|
342
|
-
this.logger.info('Stopping all services');
|
343
|
-
|
344
|
-
const stopOptions = {
|
345
|
-
timeout: options.timeout || this.config.stopTimeout,
|
346
|
-
force: options.force || false
|
347
|
-
};
|
348
|
-
|
349
|
-
// Stop in reverse order
|
350
|
-
const sorted = this._topologicalSort().reverse();
|
351
|
-
const results = new Map();
|
352
|
-
|
353
|
-
// Handle global timeout if specified
|
354
|
-
if (stopOptions.timeout > 0) {
|
355
|
-
const timeoutPromise = new Promise((_, reject) =>
|
356
|
-
setTimeout(
|
357
|
-
() => reject(new Error('Global shutdown timeout')),
|
358
|
-
stopOptions.timeout
|
359
|
-
)
|
360
|
-
);
|
361
|
-
|
362
|
-
try {
|
363
|
-
// Race between stopping all services and timeout
|
364
|
-
await Promise.race([
|
365
|
-
this._stopAllSequentially(sorted, results, stopOptions),
|
366
|
-
timeoutPromise
|
367
|
-
]);
|
368
|
-
} catch (error) {
|
369
|
-
if (error.message === 'Global shutdown timeout') {
|
370
|
-
this.logger.error('Global shutdown timeout reached');
|
371
|
-
// Mark any remaining services as failed
|
372
|
-
for (const name of sorted) {
|
373
|
-
if (!results.has(name)) {
|
374
|
-
results.set(name, false);
|
375
|
-
}
|
376
|
-
}
|
377
|
-
} else {
|
378
|
-
throw error;
|
379
|
-
}
|
380
|
-
}
|
381
|
-
} else {
|
382
|
-
// No timeout, just stop sequentially
|
383
|
-
await this._stopAllSequentially(sorted, results, stopOptions);
|
384
|
-
}
|
385
|
-
|
386
|
-
return Object.fromEntries(results);
|
387
|
-
}
|
388
|
-
|
389
|
-
/**
|
390
|
-
* Stop services sequentially
|
391
|
-
*
|
392
|
-
* @private
|
393
|
-
* @param {string[]} serviceNames - Ordered list of service names
|
394
|
-
* @param {Map<string, boolean>} results - Results map to populate
|
395
|
-
* @param {StopOptions} options - Stop options
|
396
|
-
*/
|
397
|
-
async _stopAllSequentially(serviceNames, results, options) {
|
398
|
-
for (const name of serviceNames) {
|
399
|
-
try {
|
400
|
-
const success = await this.stopService(name, options);
|
401
|
-
results.set(name, success);
|
402
|
-
} catch (error) {
|
403
|
-
this.logger.error(`Error stopping '${name}'`, error);
|
404
|
-
results.set(name, false);
|
405
|
-
}
|
406
|
-
}
|
407
|
-
}
|
408
|
-
|
409
|
-
/**
|
410
|
-
* Get health status for all services
|
411
|
-
*
|
412
|
-
* @returns {Promise<HealthCheckResult>} Health status for all services
|
413
|
-
*/
|
414
|
-
async checkHealth() {
|
415
|
-
/** @type {HealthCheckResult} */
|
416
|
-
const health = {};
|
417
|
-
|
418
|
-
for (const [name, entry] of this.services) {
|
419
|
-
if (entry.instance) {
|
420
|
-
health[name] = await entry.instance.getHealth();
|
421
|
-
} else {
|
422
|
-
health[name] = {
|
423
|
-
name,
|
424
|
-
state: NOT_CREATED,
|
425
|
-
healthy: false
|
426
|
-
};
|
427
|
-
}
|
428
|
-
}
|
429
|
-
|
430
|
-
return health;
|
431
|
-
}
|
432
|
-
|
433
|
-
/**
|
434
|
-
* Check if a service is currently running
|
435
|
-
*
|
436
|
-
* @param {string} name - Service name
|
437
|
-
*
|
438
|
-
* @returns {Promise<boolean>} True if service is running
|
439
|
-
*/
|
440
|
-
async isRunning(name) {
|
441
|
-
const instance = this.get(name);
|
442
|
-
return instance ? instance.state === RUNNING : false;
|
443
|
-
}
|
444
|
-
|
445
|
-
/**
|
446
|
-
* Set log level for a service or globally
|
447
|
-
*
|
448
|
-
* @param {string} name - Service name or '*' for global
|
449
|
-
* @param {string} level - Log level to set
|
450
|
-
*/
|
451
|
-
setLogLevel(name, level) {
|
452
|
-
if (name === '*') {
|
453
|
-
// Global level
|
454
|
-
this.config.logConfig.globalLevel = level;
|
455
|
-
|
456
|
-
// Apply to all existing services
|
457
|
-
// eslint-disable-next-line no-unused-vars
|
458
|
-
for (const [_, entry] of this.services) {
|
459
|
-
if (entry.instance) {
|
460
|
-
entry.instance.setLogLevel(level);
|
461
|
-
}
|
462
|
-
}
|
463
|
-
} else {
|
464
|
-
// Service-specific level
|
465
|
-
if (!this.config.logConfig.serviceLevels) {
|
466
|
-
this.config.logConfig.serviceLevels = {};
|
467
|
-
}
|
468
|
-
this.config.logConfig.serviceLevels[name] = level;
|
469
|
-
|
470
|
-
// Apply to existing instance
|
471
|
-
const instance = this.get(name);
|
472
|
-
if (instance) {
|
473
|
-
instance.setLogLevel(level);
|
474
|
-
}
|
475
|
-
}
|
476
|
-
}
|
477
|
-
|
478
|
-
/**
|
479
|
-
* Get all services with a specific tag
|
480
|
-
*
|
481
|
-
* @param {string} tag - Tag to filter by
|
482
|
-
*
|
483
|
-
* @returns {string[]} Array of service names
|
484
|
-
*/
|
485
|
-
getServicesByTag(tag) {
|
486
|
-
const services = [];
|
487
|
-
for (const [name, entry] of this.services) {
|
488
|
-
if (entry.tags.includes(tag)) {
|
489
|
-
services.push(name);
|
490
|
-
}
|
491
|
-
}
|
492
|
-
return services;
|
493
|
-
}
|
494
|
-
|
495
|
-
// Private methods
|
496
|
-
|
497
|
-
/**
|
498
|
-
* Setup logging configuration based on config.dev
|
499
|
-
*
|
500
|
-
* @private
|
501
|
-
*/
|
502
|
-
_setupLogging() {
|
503
|
-
// Set default log levels based on config.debug flag
|
504
|
-
if (this.config.debug) {
|
505
|
-
this.config.logConfig.defaultLevel = DEBUG;
|
506
|
-
} else {
|
507
|
-
this.config.logConfig.defaultLevel = WARN;
|
508
|
-
}
|
509
|
-
|
510
|
-
// Apply config
|
511
|
-
if (this.config.logConfig.globalLevel) {
|
512
|
-
this.logger.setLevel(this.config.logConfig.globalLevel);
|
513
|
-
}
|
514
|
-
}
|
515
|
-
|
516
|
-
/**
|
517
|
-
* Get the appropriate log level for a service
|
518
|
-
*
|
519
|
-
* @private
|
520
|
-
* @param {string} name - Service name
|
521
|
-
*
|
522
|
-
* @returns {string|undefined} Log level or undefined
|
523
|
-
*/
|
524
|
-
_getServiceLogLevel(name) {
|
525
|
-
const config = this.config.logConfig;
|
526
|
-
|
527
|
-
// Check in order of precedence:
|
528
|
-
// 1. Global level (overrides everything)
|
529
|
-
if (config.globalLevel) {
|
530
|
-
return config.globalLevel;
|
531
|
-
}
|
532
|
-
|
533
|
-
// 2. Service-specific level
|
534
|
-
if (config.serviceLevels?.[name]) {
|
535
|
-
return config.serviceLevels[name];
|
536
|
-
}
|
537
|
-
|
538
|
-
// 3. Don't use defaultLevel as it might be too restrictive
|
539
|
-
// Return undefined to let the service use its own default
|
540
|
-
return undefined;
|
541
|
-
}
|
542
|
-
|
543
|
-
/**
|
544
|
-
* Attach event listeners to forward service events
|
545
|
-
*
|
546
|
-
* @private
|
547
|
-
* @param {string} name - Service name
|
548
|
-
* @param {import('./typedef.js').ServiceInstance} instance
|
549
|
-
* Service instance
|
550
|
-
*/
|
551
|
-
_attachServiceEvents(name, instance) {
|
552
|
-
// Forward service events
|
553
|
-
instance.on('stateChanged', (data) => {
|
554
|
-
this.emit('service:stateChanged', { service: name, data });
|
555
|
-
});
|
556
|
-
|
557
|
-
instance.on('healthChanged', (data) => {
|
558
|
-
this.emit('service:healthChanged', { service: name, data });
|
559
|
-
});
|
560
|
-
|
561
|
-
instance.on('error', (data) => {
|
562
|
-
this.emit('service:error', { service: name, data });
|
563
|
-
});
|
564
|
-
|
565
|
-
// Forward log events
|
566
|
-
|
567
|
-
instance.logger.on('log', (logEvent) => {
|
568
|
-
this.emit('service:log', logEvent);
|
569
|
-
});
|
570
|
-
}
|
571
|
-
|
572
|
-
/**
|
573
|
-
* Sort services by dependencies using topological sort
|
574
|
-
*
|
575
|
-
* @private
|
576
|
-
*
|
577
|
-
* @returns {string[]} Service names in dependency order
|
578
|
-
* @throws {Error} If circular dependencies are detected
|
579
|
-
*/
|
580
|
-
_topologicalSort() {
|
581
|
-
const sorted = [];
|
582
|
-
const visited = new Set();
|
583
|
-
const visiting = new Set();
|
584
|
-
|
585
|
-
const visit = (name) => {
|
586
|
-
if (visited.has(name)) return;
|
587
|
-
if (visiting.has(name)) {
|
588
|
-
throw new Error(`Circular dependency detected involving '${name}'`);
|
589
|
-
}
|
590
|
-
|
591
|
-
visiting.add(name);
|
592
|
-
|
593
|
-
const entry = this.services.get(name);
|
594
|
-
if (entry) {
|
595
|
-
for (const dep of entry.dependencies) {
|
596
|
-
visit(dep);
|
597
|
-
}
|
598
|
-
}
|
599
|
-
|
600
|
-
visiting.delete(name);
|
601
|
-
visited.add(name);
|
602
|
-
sorted.push(name);
|
603
|
-
};
|
604
|
-
|
605
|
-
// Visit all services
|
606
|
-
for (const name of this.services.keys()) {
|
607
|
-
visit(name);
|
608
|
-
}
|
609
|
-
|
610
|
-
return sorted;
|
611
|
-
}
|
612
|
-
}
|
613
|
-
|
614
|
-
export default ServiceManager;
|
1
|
+
/**
|
2
|
+
* @fileoverview Service Manager for coordinating service lifecycle,
|
3
|
+
* dependencies, and health monitoring.
|
4
|
+
*
|
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
|
+
*
|
9
|
+
* @example
|
10
|
+
* // Basic usage
|
11
|
+
* import { ServiceManager } from './ServiceManager.js';
|
12
|
+
* import DatabaseService from './services/DatabaseService.js';
|
13
|
+
* import AuthService from './services/AuthService.js';
|
14
|
+
*
|
15
|
+
* const manager = new ServiceManager({
|
16
|
+
* debug: true,
|
17
|
+
* stopTimeout: 10000
|
18
|
+
* });
|
19
|
+
*
|
20
|
+
* // Register services with dependencies
|
21
|
+
* manager.register('database', DatabaseService, {
|
22
|
+
* connectionString: 'postgres://localhost/myapp'
|
23
|
+
* });
|
24
|
+
*
|
25
|
+
* manager.register('auth', AuthService, {
|
26
|
+
* secret: process.env.JWT_SECRET
|
27
|
+
* }, {
|
28
|
+
* dependencies: ['database'] // auth depends on database
|
29
|
+
* });
|
30
|
+
*
|
31
|
+
* // Start all services
|
32
|
+
* await manager.startAll();
|
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);
|
50
|
+
* });
|
51
|
+
*
|
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');
|
59
|
+
*
|
60
|
+
* // Listen to all service logs
|
61
|
+
* manager.on('service:log', (logEvent) => {
|
62
|
+
* writeToLogFile(logEvent);
|
63
|
+
* });
|
64
|
+
*/
|
65
|
+
|
66
|
+
import { EventEmitter } from '../events';
|
67
|
+
import { Logger, DEBUG, INFO, WARN } from '../logging';
|
68
|
+
|
69
|
+
import {
|
70
|
+
NOT_CREATED,
|
71
|
+
CREATED,
|
72
|
+
RUNNING,
|
73
|
+
DESTROYED
|
74
|
+
} from './service-states.js';
|
75
|
+
|
76
|
+
/**
|
77
|
+
* @typedef {import('./typedef.js').ServiceConstructor} ServiceConstructor
|
78
|
+
* @typedef {import('./typedef.js').ServiceConfig} ServiceConfig
|
79
|
+
* @typedef {import('./typedef.js').ServiceRegistrationOptions} ServiceRegistrationOptions
|
80
|
+
* @typedef {import('./typedef.js').ServiceManagerConfig} ServiceManagerConfig
|
81
|
+
* @typedef {import('./typedef.js').StopOptions} StopOptions
|
82
|
+
* @typedef {import('./typedef.js').ServiceEntry} ServiceEntry
|
83
|
+
* @typedef {import('./typedef.js').HealthCheckResult} HealthCheckResult
|
84
|
+
*/
|
85
|
+
|
86
|
+
/**
|
87
|
+
* Service Manager for lifecycle and dependency management
|
88
|
+
* @extends EventEmitter
|
89
|
+
*/
|
90
|
+
export class ServiceManager extends EventEmitter {
|
91
|
+
/**
|
92
|
+
* Create a new ServiceManager instance
|
93
|
+
*
|
94
|
+
* @param {ServiceManagerConfig} [config={}] - Manager configuration
|
95
|
+
*/
|
96
|
+
constructor(config = {}) {
|
97
|
+
super();
|
98
|
+
|
99
|
+
/** @type {Map<string, ServiceEntry>} */
|
100
|
+
this.services = new Map();
|
101
|
+
|
102
|
+
/** @type {Logger} */
|
103
|
+
this.logger = new Logger('ServiceManager', config.logLevel || INFO);
|
104
|
+
|
105
|
+
/** @type {ServiceManagerConfig} */
|
106
|
+
this.config = {
|
107
|
+
debug: config.debug ?? false,
|
108
|
+
autoStart: config.autoStart ?? false,
|
109
|
+
stopTimeout: config.stopTimeout || 10000,
|
110
|
+
logConfig: config.logConfig || {}
|
111
|
+
};
|
112
|
+
|
113
|
+
this._setupLogging();
|
114
|
+
}
|
115
|
+
|
116
|
+
/**
|
117
|
+
* Register a service class with the manager
|
118
|
+
*
|
119
|
+
* @param {string} name - Unique service identifier
|
120
|
+
* @param {ServiceConstructor} ServiceClass - Service class constructor
|
121
|
+
* @param {ServiceConfig} [config={}] - Service configuration
|
122
|
+
* @param {ServiceRegistrationOptions} [options={}] - Registration options
|
123
|
+
*
|
124
|
+
* @throws {Error} If service name is already registered
|
125
|
+
*/
|
126
|
+
register(name, ServiceClass, config = {}, options = {}) {
|
127
|
+
if (this.services.has(name)) {
|
128
|
+
throw new Error(`Service '${name}' already registered`);
|
129
|
+
}
|
130
|
+
|
131
|
+
/** @type {ServiceEntry} */
|
132
|
+
const entry = {
|
133
|
+
ServiceClass,
|
134
|
+
instance: null,
|
135
|
+
config,
|
136
|
+
dependencies: options.dependencies || [],
|
137
|
+
dependents: new Set(),
|
138
|
+
tags: options.tags || [],
|
139
|
+
priority: options.priority || 0
|
140
|
+
};
|
141
|
+
|
142
|
+
// Track dependents
|
143
|
+
entry.dependencies.forEach(dep => {
|
144
|
+
const depEntry = this.services.get(dep);
|
145
|
+
if (depEntry) {
|
146
|
+
depEntry.dependents.add(name);
|
147
|
+
}
|
148
|
+
});
|
149
|
+
|
150
|
+
this.services.set(name, entry);
|
151
|
+
|
152
|
+
this.logger.debug(`Registered service '${name}'`, {
|
153
|
+
dependencies: entry.dependencies,
|
154
|
+
tags: entry.tags
|
155
|
+
});
|
156
|
+
}
|
157
|
+
|
158
|
+
/**
|
159
|
+
* Get or create a service instance
|
160
|
+
*
|
161
|
+
* @param {string} name - Service name
|
162
|
+
*
|
163
|
+
* @returns {import('./typedef.js').ServiceInstance|null}
|
164
|
+
* Service instance or null if not found
|
165
|
+
*/
|
166
|
+
get(name) {
|
167
|
+
const entry = this.services.get(name);
|
168
|
+
if (!entry) {
|
169
|
+
this.logger.warn(`Service '${name}' not found`);
|
170
|
+
return null;
|
171
|
+
}
|
172
|
+
|
173
|
+
if (!entry.instance) {
|
174
|
+
try {
|
175
|
+
entry.instance = new entry.ServiceClass(name);
|
176
|
+
|
177
|
+
// Apply log level
|
178
|
+
const logLevel = this._getServiceLogLevel(name);
|
179
|
+
if (logLevel) {
|
180
|
+
entry.instance.setLogLevel(logLevel);
|
181
|
+
}
|
182
|
+
|
183
|
+
// Forward events
|
184
|
+
this._attachServiceEvents(name, entry.instance);
|
185
|
+
|
186
|
+
this.logger.debug(`Created instance for '${name}'`);
|
187
|
+
} catch (error) {
|
188
|
+
this.logger.error(`Failed to create instance for '${name}'`, error);
|
189
|
+
return null;
|
190
|
+
}
|
191
|
+
}
|
192
|
+
|
193
|
+
return entry.instance;
|
194
|
+
}
|
195
|
+
|
196
|
+
/**
|
197
|
+
* Initialize a service
|
198
|
+
*
|
199
|
+
* @param {string} name - Service name
|
200
|
+
*
|
201
|
+
* @returns {Promise<boolean>} True if initialization succeeded
|
202
|
+
*/
|
203
|
+
async initService(name) {
|
204
|
+
const instance = this.get(name);
|
205
|
+
if (!instance) return false;
|
206
|
+
|
207
|
+
const entry = this.services.get(name);
|
208
|
+
return await instance.initialize(entry.config);
|
209
|
+
}
|
210
|
+
|
211
|
+
/**
|
212
|
+
* Start a service and its dependencies
|
213
|
+
*
|
214
|
+
* @param {string} name - Service name
|
215
|
+
*
|
216
|
+
* @returns {Promise<boolean>} True if service started successfully
|
217
|
+
*/
|
218
|
+
async startService(name) {
|
219
|
+
const entry = this.services.get(name);
|
220
|
+
if (!entry) {
|
221
|
+
this.logger.warn(`Cannot start unregistered service '${name}'`);
|
222
|
+
return false;
|
223
|
+
}
|
224
|
+
|
225
|
+
// Start dependencies first
|
226
|
+
for (const dep of entry.dependencies) {
|
227
|
+
if (!await this.isRunning(dep)) {
|
228
|
+
|
229
|
+
this.logger.debug(`Starting dependency '${dep}' for '${name}'`);
|
230
|
+
|
231
|
+
const started = await this.startService(dep);
|
232
|
+
if (!started) {
|
233
|
+
this.logger.error(
|
234
|
+
`Failed to start dependency '${dep}' for '${name}'`
|
235
|
+
);
|
236
|
+
return false;
|
237
|
+
}
|
238
|
+
}
|
239
|
+
}
|
240
|
+
|
241
|
+
const instance = this.get(name);
|
242
|
+
if (!instance) return false;
|
243
|
+
|
244
|
+
// Initialize if needed
|
245
|
+
if (instance.state === CREATED || instance.state === DESTROYED) {
|
246
|
+
const initialized = await this.initService(name);
|
247
|
+
if (!initialized) return false;
|
248
|
+
}
|
249
|
+
|
250
|
+
return await instance.start();
|
251
|
+
}
|
252
|
+
|
253
|
+
/**
|
254
|
+
* Stop a service
|
255
|
+
*
|
256
|
+
* @param {string} name - Service name
|
257
|
+
* @param {StopOptions} [options={}] - Stop options
|
258
|
+
*
|
259
|
+
* @returns {Promise<boolean>} True if service stopped successfully
|
260
|
+
*/
|
261
|
+
async stopService(name, options = {}) {
|
262
|
+
const instance = this.get(name);
|
263
|
+
if (!instance) {
|
264
|
+
this.logger.warn(`Cannot stop unregistered service '${name}'`);
|
265
|
+
return true; // Already stopped
|
266
|
+
}
|
267
|
+
|
268
|
+
// Check dependents
|
269
|
+
const entry = this.services.get(name);
|
270
|
+
if (!options.force && entry && entry.dependents.size > 0) {
|
271
|
+
const runningDependents = [];
|
272
|
+
for (const dep of entry.dependents) {
|
273
|
+
if (await this.isRunning(dep)) {
|
274
|
+
runningDependents.push(dep);
|
275
|
+
}
|
276
|
+
}
|
277
|
+
|
278
|
+
if (runningDependents.length > 0) {
|
279
|
+
this.logger.warn(
|
280
|
+
`Cannot stop '${name}' - required by: ${runningDependents.join(', ')}`
|
281
|
+
);
|
282
|
+
return false;
|
283
|
+
}
|
284
|
+
}
|
285
|
+
|
286
|
+
return await instance.stop(options);
|
287
|
+
}
|
288
|
+
|
289
|
+
/**
|
290
|
+
* Recover a service from error state
|
291
|
+
*
|
292
|
+
* @param {string} name - Service name
|
293
|
+
*
|
294
|
+
* @returns {Promise<boolean>} True if recovery succeeded
|
295
|
+
*/
|
296
|
+
async recoverService(name) {
|
297
|
+
const instance = this.get(name);
|
298
|
+
if (!instance) return false;
|
299
|
+
|
300
|
+
return await instance.recover();
|
301
|
+
}
|
302
|
+
|
303
|
+
/**
|
304
|
+
* Start all registered services in dependency order
|
305
|
+
*
|
306
|
+
* @returns {Promise<Object<string, boolean>>} Map of service results
|
307
|
+
*/
|
308
|
+
async startAll() {
|
309
|
+
this.logger.info('Starting all services');
|
310
|
+
|
311
|
+
// Sort by priority and dependencies
|
312
|
+
const sorted = this._topologicalSort();
|
313
|
+
const results = new Map();
|
314
|
+
|
315
|
+
for (const name of sorted) {
|
316
|
+
const success = await this.startService(name);
|
317
|
+
results.set(name, success);
|
318
|
+
|
319
|
+
if (!success) {
|
320
|
+
this.logger.error(`Failed to start '${name}', stopping`);
|
321
|
+
// Mark remaining services as not started
|
322
|
+
for (const remaining of sorted) {
|
323
|
+
if (!results.has(remaining)) {
|
324
|
+
results.set(remaining, false);
|
325
|
+
}
|
326
|
+
}
|
327
|
+
break;
|
328
|
+
}
|
329
|
+
}
|
330
|
+
|
331
|
+
return Object.fromEntries(results);
|
332
|
+
}
|
333
|
+
|
334
|
+
/**
|
335
|
+
* Stop all services in reverse dependency order
|
336
|
+
*
|
337
|
+
* @param {StopOptions} [options={}] - Stop options
|
338
|
+
*
|
339
|
+
* @returns {Promise<Object<string, boolean>>} Map of service results
|
340
|
+
*/
|
341
|
+
async stopAll(options = {}) {
|
342
|
+
this.logger.info('Stopping all services');
|
343
|
+
|
344
|
+
const stopOptions = {
|
345
|
+
timeout: options.timeout || this.config.stopTimeout,
|
346
|
+
force: options.force || false
|
347
|
+
};
|
348
|
+
|
349
|
+
// Stop in reverse order
|
350
|
+
const sorted = this._topologicalSort().reverse();
|
351
|
+
const results = new Map();
|
352
|
+
|
353
|
+
// Handle global timeout if specified
|
354
|
+
if (stopOptions.timeout > 0) {
|
355
|
+
const timeoutPromise = new Promise((_, reject) =>
|
356
|
+
setTimeout(
|
357
|
+
() => reject(new Error('Global shutdown timeout')),
|
358
|
+
stopOptions.timeout
|
359
|
+
)
|
360
|
+
);
|
361
|
+
|
362
|
+
try {
|
363
|
+
// Race between stopping all services and timeout
|
364
|
+
await Promise.race([
|
365
|
+
this._stopAllSequentially(sorted, results, stopOptions),
|
366
|
+
timeoutPromise
|
367
|
+
]);
|
368
|
+
} catch (error) {
|
369
|
+
if (error.message === 'Global shutdown timeout') {
|
370
|
+
this.logger.error('Global shutdown timeout reached');
|
371
|
+
// Mark any remaining services as failed
|
372
|
+
for (const name of sorted) {
|
373
|
+
if (!results.has(name)) {
|
374
|
+
results.set(name, false);
|
375
|
+
}
|
376
|
+
}
|
377
|
+
} else {
|
378
|
+
throw error;
|
379
|
+
}
|
380
|
+
}
|
381
|
+
} else {
|
382
|
+
// No timeout, just stop sequentially
|
383
|
+
await this._stopAllSequentially(sorted, results, stopOptions);
|
384
|
+
}
|
385
|
+
|
386
|
+
return Object.fromEntries(results);
|
387
|
+
}
|
388
|
+
|
389
|
+
/**
|
390
|
+
* Stop services sequentially
|
391
|
+
*
|
392
|
+
* @private
|
393
|
+
* @param {string[]} serviceNames - Ordered list of service names
|
394
|
+
* @param {Map<string, boolean>} results - Results map to populate
|
395
|
+
* @param {StopOptions} options - Stop options
|
396
|
+
*/
|
397
|
+
async _stopAllSequentially(serviceNames, results, options) {
|
398
|
+
for (const name of serviceNames) {
|
399
|
+
try {
|
400
|
+
const success = await this.stopService(name, options);
|
401
|
+
results.set(name, success);
|
402
|
+
} catch (error) {
|
403
|
+
this.logger.error(`Error stopping '${name}'`, error);
|
404
|
+
results.set(name, false);
|
405
|
+
}
|
406
|
+
}
|
407
|
+
}
|
408
|
+
|
409
|
+
/**
|
410
|
+
* Get health status for all services
|
411
|
+
*
|
412
|
+
* @returns {Promise<HealthCheckResult>} Health status for all services
|
413
|
+
*/
|
414
|
+
async checkHealth() {
|
415
|
+
/** @type {HealthCheckResult} */
|
416
|
+
const health = {};
|
417
|
+
|
418
|
+
for (const [name, entry] of this.services) {
|
419
|
+
if (entry.instance) {
|
420
|
+
health[name] = await entry.instance.getHealth();
|
421
|
+
} else {
|
422
|
+
health[name] = {
|
423
|
+
name,
|
424
|
+
state: NOT_CREATED,
|
425
|
+
healthy: false
|
426
|
+
};
|
427
|
+
}
|
428
|
+
}
|
429
|
+
|
430
|
+
return health;
|
431
|
+
}
|
432
|
+
|
433
|
+
/**
|
434
|
+
* Check if a service is currently running
|
435
|
+
*
|
436
|
+
* @param {string} name - Service name
|
437
|
+
*
|
438
|
+
* @returns {Promise<boolean>} True if service is running
|
439
|
+
*/
|
440
|
+
async isRunning(name) {
|
441
|
+
const instance = this.get(name);
|
442
|
+
return instance ? instance.state === RUNNING : false;
|
443
|
+
}
|
444
|
+
|
445
|
+
/**
|
446
|
+
* Set log level for a service or globally
|
447
|
+
*
|
448
|
+
* @param {string} name - Service name or '*' for global
|
449
|
+
* @param {string} level - Log level to set
|
450
|
+
*/
|
451
|
+
setLogLevel(name, level) {
|
452
|
+
if (name === '*') {
|
453
|
+
// Global level
|
454
|
+
this.config.logConfig.globalLevel = level;
|
455
|
+
|
456
|
+
// Apply to all existing services
|
457
|
+
// eslint-disable-next-line no-unused-vars
|
458
|
+
for (const [_, entry] of this.services) {
|
459
|
+
if (entry.instance) {
|
460
|
+
entry.instance.setLogLevel(level);
|
461
|
+
}
|
462
|
+
}
|
463
|
+
} else {
|
464
|
+
// Service-specific level
|
465
|
+
if (!this.config.logConfig.serviceLevels) {
|
466
|
+
this.config.logConfig.serviceLevels = {};
|
467
|
+
}
|
468
|
+
this.config.logConfig.serviceLevels[name] = level;
|
469
|
+
|
470
|
+
// Apply to existing instance
|
471
|
+
const instance = this.get(name);
|
472
|
+
if (instance) {
|
473
|
+
instance.setLogLevel(level);
|
474
|
+
}
|
475
|
+
}
|
476
|
+
}
|
477
|
+
|
478
|
+
/**
|
479
|
+
* Get all services with a specific tag
|
480
|
+
*
|
481
|
+
* @param {string} tag - Tag to filter by
|
482
|
+
*
|
483
|
+
* @returns {string[]} Array of service names
|
484
|
+
*/
|
485
|
+
getServicesByTag(tag) {
|
486
|
+
const services = [];
|
487
|
+
for (const [name, entry] of this.services) {
|
488
|
+
if (entry.tags.includes(tag)) {
|
489
|
+
services.push(name);
|
490
|
+
}
|
491
|
+
}
|
492
|
+
return services;
|
493
|
+
}
|
494
|
+
|
495
|
+
// Private methods
|
496
|
+
|
497
|
+
/**
|
498
|
+
* Setup logging configuration based on config.dev
|
499
|
+
*
|
500
|
+
* @private
|
501
|
+
*/
|
502
|
+
_setupLogging() {
|
503
|
+
// Set default log levels based on config.debug flag
|
504
|
+
if (this.config.debug) {
|
505
|
+
this.config.logConfig.defaultLevel = DEBUG;
|
506
|
+
} else {
|
507
|
+
this.config.logConfig.defaultLevel = WARN;
|
508
|
+
}
|
509
|
+
|
510
|
+
// Apply config
|
511
|
+
if (this.config.logConfig.globalLevel) {
|
512
|
+
this.logger.setLevel(this.config.logConfig.globalLevel);
|
513
|
+
}
|
514
|
+
}
|
515
|
+
|
516
|
+
/**
|
517
|
+
* Get the appropriate log level for a service
|
518
|
+
*
|
519
|
+
* @private
|
520
|
+
* @param {string} name - Service name
|
521
|
+
*
|
522
|
+
* @returns {string|undefined} Log level or undefined
|
523
|
+
*/
|
524
|
+
_getServiceLogLevel(name) {
|
525
|
+
const config = this.config.logConfig;
|
526
|
+
|
527
|
+
// Check in order of precedence:
|
528
|
+
// 1. Global level (overrides everything)
|
529
|
+
if (config.globalLevel) {
|
530
|
+
return config.globalLevel;
|
531
|
+
}
|
532
|
+
|
533
|
+
// 2. Service-specific level
|
534
|
+
if (config.serviceLevels?.[name]) {
|
535
|
+
return config.serviceLevels[name];
|
536
|
+
}
|
537
|
+
|
538
|
+
// 3. Don't use defaultLevel as it might be too restrictive
|
539
|
+
// Return undefined to let the service use its own default
|
540
|
+
return undefined;
|
541
|
+
}
|
542
|
+
|
543
|
+
/**
|
544
|
+
* Attach event listeners to forward service events
|
545
|
+
*
|
546
|
+
* @private
|
547
|
+
* @param {string} name - Service name
|
548
|
+
* @param {import('./typedef.js').ServiceInstance} instance
|
549
|
+
* Service instance
|
550
|
+
*/
|
551
|
+
_attachServiceEvents(name, instance) {
|
552
|
+
// Forward service events
|
553
|
+
instance.on('stateChanged', (data) => {
|
554
|
+
this.emit('service:stateChanged', { service: name, data });
|
555
|
+
});
|
556
|
+
|
557
|
+
instance.on('healthChanged', (data) => {
|
558
|
+
this.emit('service:healthChanged', { service: name, data });
|
559
|
+
});
|
560
|
+
|
561
|
+
instance.on('error', (data) => {
|
562
|
+
this.emit('service:error', { service: name, data });
|
563
|
+
});
|
564
|
+
|
565
|
+
// Forward log events
|
566
|
+
|
567
|
+
instance.logger.on('log', (logEvent) => {
|
568
|
+
this.emit('service:log', logEvent);
|
569
|
+
});
|
570
|
+
}
|
571
|
+
|
572
|
+
/**
|
573
|
+
* Sort services by dependencies using topological sort
|
574
|
+
*
|
575
|
+
* @private
|
576
|
+
*
|
577
|
+
* @returns {string[]} Service names in dependency order
|
578
|
+
* @throws {Error} If circular dependencies are detected
|
579
|
+
*/
|
580
|
+
_topologicalSort() {
|
581
|
+
const sorted = [];
|
582
|
+
const visited = new Set();
|
583
|
+
const visiting = new Set();
|
584
|
+
|
585
|
+
const visit = (name) => {
|
586
|
+
if (visited.has(name)) return;
|
587
|
+
if (visiting.has(name)) {
|
588
|
+
throw new Error(`Circular dependency detected involving '${name}'`);
|
589
|
+
}
|
590
|
+
|
591
|
+
visiting.add(name);
|
592
|
+
|
593
|
+
const entry = this.services.get(name);
|
594
|
+
if (entry) {
|
595
|
+
for (const dep of entry.dependencies) {
|
596
|
+
visit(dep);
|
597
|
+
}
|
598
|
+
}
|
599
|
+
|
600
|
+
visiting.delete(name);
|
601
|
+
visited.add(name);
|
602
|
+
sorted.push(name);
|
603
|
+
};
|
604
|
+
|
605
|
+
// Visit all services
|
606
|
+
for (const name of this.services.keys()) {
|
607
|
+
visit(name);
|
608
|
+
}
|
609
|
+
|
610
|
+
return sorted;
|
611
|
+
}
|
612
|
+
}
|
613
|
+
|
614
|
+
export default ServiceManager;
|