@hkdigital/lib-sveltekit 0.2.21 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/README.md +149 -135
  2. package/dist/assets/autospuiten/car-paint-picker.js +41 -41
  3. package/dist/assets/autospuiten/labels.js +7 -7
  4. package/dist/classes/cache/IndexedDbCache.js +1407 -1407
  5. package/dist/classes/cache/MemoryResponseCache.js +138 -138
  6. package/dist/classes/cache/index.js +5 -5
  7. package/dist/classes/cache/typedef.js +41 -41
  8. package/dist/classes/data/IterableTree.js +243 -243
  9. package/dist/classes/data/Selector.js +190 -190
  10. package/dist/classes/data/index.js +2 -2
  11. package/dist/classes/events/EventEmitter.js +275 -275
  12. package/dist/classes/events/index.js +2 -2
  13. package/dist/classes/index.js +4 -4
  14. package/dist/classes/logging/Logger.js +210 -210
  15. package/dist/classes/logging/constants.js +16 -16
  16. package/dist/classes/logging/index.js +4 -4
  17. package/dist/classes/logging/typedef.js +17 -17
  18. package/dist/classes/promise/HkPromise.js +377 -377
  19. package/dist/classes/promise/index.js +1 -1
  20. package/dist/classes/services/ServiceBase.js +463 -463
  21. package/dist/classes/services/ServiceManager.js +614 -614
  22. package/dist/classes/services/index.js +5 -5
  23. package/dist/classes/services/service-states.js +205 -205
  24. package/dist/classes/services/typedef.js +179 -179
  25. package/dist/classes/stores/SubscribersCount.js +107 -107
  26. package/dist/classes/stores/index.js +1 -1
  27. package/dist/classes/streams/LogTransformStream.js +19 -19
  28. package/dist/classes/streams/ServerEventsStore.js +110 -110
  29. package/dist/classes/streams/TimeStampSource.js +26 -26
  30. package/dist/classes/streams/index.js +3 -3
  31. package/dist/classes/svelte/audio/AudioLoader.svelte.js +58 -58
  32. package/dist/classes/svelte/audio/AudioScene.svelte.js +324 -324
  33. package/dist/classes/svelte/audio/mocks.js +35 -35
  34. package/dist/classes/svelte/finite-state-machine/FiniteStateMachine.svelte.js +133 -133
  35. package/dist/classes/svelte/finite-state-machine/index.js +1 -1
  36. package/dist/classes/svelte/image/ImageLoader.svelte.js +45 -45
  37. package/dist/classes/svelte/image/ImageScene.svelte.js +249 -249
  38. package/dist/classes/svelte/image/ImageVariantsLoader.svelte.js +152 -152
  39. package/dist/classes/svelte/image/index.js +4 -4
  40. package/dist/classes/svelte/image/mocks.js +35 -35
  41. package/dist/classes/svelte/image/typedef.js +8 -8
  42. package/dist/classes/svelte/index.js +14 -14
  43. package/dist/classes/svelte/loading-state-machine/LoadingStateMachine.svelte.js +109 -109
  44. package/dist/classes/svelte/loading-state-machine/constants.js +16 -16
  45. package/dist/classes/svelte/loading-state-machine/index.js +3 -3
  46. package/dist/classes/svelte/network-loader/NetworkLoader.svelte.js +338 -338
  47. package/dist/classes/svelte/network-loader/constants.js +3 -3
  48. package/dist/classes/svelte/network-loader/index.js +3 -3
  49. package/dist/classes/svelte/network-loader/mocks.js +30 -30
  50. package/dist/classes/svelte/network-loader/typedef.js +8 -8
  51. package/dist/components/area/HkArea.svelte +49 -49
  52. package/dist/components/area/HkGridArea.svelte +77 -77
  53. package/dist/components/area/index.js +2 -2
  54. package/dist/components/buttons/button/Button.svelte +82 -82
  55. package/dist/components/buttons/button-icon-steeze/SteezeIconButton.svelte +30 -30
  56. package/dist/components/buttons/button-text/TextButton.svelte +21 -21
  57. package/dist/components/buttons/index.js +3 -3
  58. package/dist/components/debug/debug-panel-design-scaling/DebugPanelDesignScaling.svelte +146 -146
  59. package/dist/components/debug/index.js +1 -1
  60. package/dist/components/drag-drop/DragController.js +44 -44
  61. package/dist/components/drag-drop/DragDropContext.svelte +111 -111
  62. package/dist/components/drag-drop/Draggable.svelte +519 -519
  63. package/dist/components/drag-drop/{Dropzone.svelte → DropZone.svelte} +258 -258
  64. package/dist/components/drag-drop/DropZoneArea.svelte +119 -119
  65. package/dist/components/drag-drop/DropZoneList.svelte +125 -125
  66. package/dist/components/drag-drop/actions.js +26 -26
  67. package/dist/components/drag-drop/drag-state.svelte.js +322 -322
  68. package/dist/components/drag-drop/index.js +7 -7
  69. package/dist/components/drag-drop/util.js +85 -85
  70. package/dist/components/hkdev/blocks/TextBlock.svelte +46 -46
  71. package/dist/components/hkdev/buttons/CheckButton.svelte +62 -62
  72. package/dist/components/icons/HkIcon.svelte +86 -86
  73. package/dist/components/icons/HkTabIcon.svelte +116 -116
  74. package/dist/components/icons/SteezeIcon.svelte +97 -97
  75. package/dist/components/icons/index.js +6 -6
  76. package/dist/components/icons/typedef.js +16 -16
  77. package/dist/components/index.js +2 -2
  78. package/dist/components/inputs/index.js +1 -1
  79. package/dist/components/inputs/text-input/TestTextInput.svelte__ +102 -102
  80. package/dist/components/inputs/text-input/TextInput.svelte +223 -223
  81. package/dist/components/inputs/text-input/TextInput.svelte___ +83 -83
  82. package/dist/components/inputs/text-input/assets/IconInvalid.svelte +14 -14
  83. package/dist/components/inputs/text-input/assets/IconValid.svelte +12 -12
  84. package/dist/components/layout/grid-layers/GridLayers.svelte +63 -63
  85. package/dist/components/layout/grid-layers/GridLayers.svelte__heightFrom__ +372 -0
  86. package/dist/components/layout/grid-layers/util.js +74 -74
  87. package/dist/components/layout/index.js +1 -1
  88. package/dist/components/panels/index.js +1 -1
  89. package/dist/components/panels/panel/Panel.svelte +43 -43
  90. package/dist/components/rows/index.js +3 -3
  91. package/dist/components/rows/panel-grid-row/PanelGridRow.svelte +104 -104
  92. package/dist/components/rows/panel-row-2/PanelRow2.svelte +40 -40
  93. package/dist/components/tab-bar/HkTabBar.state.svelte.js +149 -149
  94. package/dist/components/tab-bar/HkTabBar.svelte +74 -74
  95. package/dist/components/tab-bar/HkTabBarSelector.state.svelte.js +93 -93
  96. package/dist/components/tab-bar/HkTabBarSelector.svelte +49 -49
  97. package/dist/components/tab-bar/index.js +17 -17
  98. package/dist/components/tab-bar/typedef.js +11 -11
  99. package/dist/config/imagetools-config.js +189 -189
  100. package/dist/config/imagetools.d.ts +72 -72
  101. package/dist/constants/bases.js +13 -13
  102. package/dist/constants/errors/api.js +9 -9
  103. package/dist/constants/errors/generic.js +5 -5
  104. package/dist/constants/errors/index.js +3 -3
  105. package/dist/constants/errors/jwt.js +5 -5
  106. package/dist/constants/http/headers.js +6 -6
  107. package/dist/constants/http/index.js +2 -2
  108. package/dist/constants/http/methods.js +14 -14
  109. package/dist/constants/index.js +3 -3
  110. package/dist/constants/mime/application.js +5 -5
  111. package/dist/constants/mime/audio.js +13 -13
  112. package/dist/constants/mime/image.js +3 -3
  113. package/dist/constants/mime/index.js +4 -4
  114. package/dist/constants/mime/text.js +2 -2
  115. package/dist/constants/regexp/index.js +31 -31
  116. package/dist/constants/regexp/inspiratie.js__ +95 -95
  117. package/dist/constants/regexp/text.js +49 -49
  118. package/dist/constants/regexp/user.js +32 -32
  119. package/dist/constants/regexp/web.js +3 -3
  120. package/dist/constants/state-labels/drag-states.js +6 -6
  121. package/dist/constants/state-labels/drop-states.js +6 -6
  122. package/dist/constants/state-labels/input-states.js +11 -11
  123. package/dist/constants/state-labels/submit-states.js +4 -4
  124. package/dist/constants/time.js +28 -28
  125. package/dist/css/utilities.css +43 -43
  126. package/dist/design/design-config.js +73 -73
  127. package/dist/design/tailwind-theme-extend.js +158 -158
  128. package/dist/features/button-group/ButtonGroup.svelte +82 -82
  129. package/dist/features/button-group/typedef.js +10 -10
  130. package/dist/features/compare-left-right/CompareLeftRight.svelte +179 -179
  131. package/dist/features/compare-left-right/index.js +1 -1
  132. package/dist/features/game-box/GameBox.svelte +577 -577
  133. package/dist/features/game-box/gamebox.util.js +83 -83
  134. package/dist/features/hk-app-layout/HkAppLayout.state.svelte.js +25 -25
  135. package/dist/features/hk-app-layout/HkAppLayout.svelte +251 -251
  136. package/dist/features/image-box/ImageBox.svelte +210 -210
  137. package/dist/features/image-box/index.js +5 -5
  138. package/dist/features/image-box/typedef.js +32 -32
  139. package/dist/features/index.js +23 -23
  140. package/dist/features/presenter/ImageSlide.svelte +64 -64
  141. package/dist/features/presenter/Presenter.state.svelte.js +638 -638
  142. package/dist/features/presenter/Presenter.svelte +142 -142
  143. package/dist/features/presenter/constants.js +7 -7
  144. package/dist/features/presenter/index.js +10 -10
  145. package/dist/features/presenter/typedef.js +106 -106
  146. package/dist/features/presenter/util.js +210 -210
  147. package/dist/features/virtual-viewport/VirtualViewport.svelte +196 -196
  148. package/dist/logging/adapters/console.js +114 -114
  149. package/dist/logging/adapters/pino.js +60 -60
  150. package/dist/logging/constants.js +1 -1
  151. package/dist/logging/factories/client.js +21 -21
  152. package/dist/logging/factories/server.js +22 -22
  153. package/dist/logging/factories/universal.js +23 -23
  154. package/dist/logging/index.js +8 -8
  155. package/dist/schemas/index.js +1 -1
  156. package/dist/schemas/validate-url.js +180 -180
  157. package/dist/server/index.js +1 -1
  158. package/dist/server/logger.js +94 -94
  159. package/dist/states/index.js +1 -1
  160. package/dist/states/navigation.svelte.js +55 -55
  161. package/dist/stores/index.js +1 -1
  162. package/dist/stores/theme.js +80 -80
  163. package/dist/themes/hkdev/components/blocks/text-block.css +34 -34
  164. package/dist/themes/hkdev/components/boxes/game-box.css +11 -11
  165. package/dist/themes/hkdev/components/buttons/button-icon-steeze.css +22 -22
  166. package/dist/themes/hkdev/components/buttons/button-text.css +32 -32
  167. package/dist/themes/hkdev/components/buttons/button.css +146 -146
  168. package/dist/themes/hkdev/components/buttons/skip-button.css +5 -5
  169. package/dist/themes/hkdev/components/drag-drop/draggable.css +73 -73
  170. package/dist/themes/hkdev/components/drag-drop/drop-zone.css +58 -58
  171. package/dist/themes/hkdev/components/icons/icon-steeze.css +15 -15
  172. package/dist/themes/hkdev/components/inputs/text-input.css +102 -102
  173. package/dist/themes/hkdev/components/panels/panel.css +25 -25
  174. package/dist/themes/hkdev/components/rows/panel-grid-row.css +4 -4
  175. package/dist/themes/hkdev/components/rows/panel-row-2.css +5 -5
  176. package/dist/themes/hkdev/components.css +29 -29
  177. package/dist/themes/hkdev/debug.css +1 -1
  178. package/dist/themes/hkdev/global/layout.css +32 -32
  179. package/dist/themes/hkdev/global/on-colors.css +32 -32
  180. package/dist/themes/hkdev/globals.css +3 -3
  181. package/dist/themes/hkdev/responsive.css +12 -12
  182. package/dist/themes/hkdev/theme-ext.js +12 -12
  183. package/dist/themes/hkdev/theme.css +218 -218
  184. package/dist/themes/index.js +1 -1
  185. package/dist/typedef/context.js +6 -6
  186. package/dist/typedef/drag.js +25 -25
  187. package/dist/typedef/drop.js +12 -12
  188. package/dist/typedef/image.js +38 -38
  189. package/dist/typedef/index.js +4 -4
  190. package/dist/util/array/index.js +436 -436
  191. package/dist/util/bases/base58.js +262 -262
  192. package/dist/util/bases/index.js +1 -1
  193. package/dist/util/compare/index.js +247 -247
  194. package/dist/util/css/css-vars.js +83 -83
  195. package/dist/util/css/index.js +1 -1
  196. package/dist/util/design-system/components/states.js +22 -22
  197. package/dist/util/design-system/css/clamp.js +66 -66
  198. package/dist/util/design-system/css/root-design-vars.js +102 -102
  199. package/dist/util/design-system/index.js +5 -5
  200. package/dist/util/design-system/layout/scaling.js +228 -228
  201. package/dist/util/design-system/skeleton.js +208 -208
  202. package/dist/util/design-system/tailwind.js +288 -288
  203. package/dist/util/env/index.js +9 -9
  204. package/dist/util/exceptions/index.d.ts +11 -0
  205. package/dist/util/exceptions/index.js +17 -0
  206. package/dist/util/expect/arrays.js +47 -47
  207. package/dist/util/expect/index.js +259 -259
  208. package/dist/util/expect/primitives.js +55 -55
  209. package/dist/util/expect/url.js +60 -60
  210. package/dist/util/function/index.js +218 -218
  211. package/dist/util/geo/index.js +26 -26
  212. package/dist/util/http/caching.js +263 -263
  213. package/dist/util/http/errors.js +97 -97
  214. package/dist/util/http/headers.js +75 -75
  215. package/dist/util/http/http-request.js +578 -578
  216. package/dist/util/http/index.js +22 -22
  217. package/dist/util/http/json-request.js +224 -224
  218. package/dist/util/http/mocks.js +65 -65
  219. package/dist/util/http/response.js +294 -294
  220. package/dist/util/http/test-data__/content-length-test-hkdigital-small.V4HfZyBQ.avif +0 -0
  221. package/dist/util/http/typedef.js +93 -93
  222. package/dist/util/http/url.js +52 -52
  223. package/dist/util/image/index.js +86 -86
  224. package/dist/util/index.d.ts +1 -0
  225. package/dist/util/index.js +3 -2
  226. package/dist/util/is/index.js +140 -140
  227. package/dist/util/iterate/index.js +234 -234
  228. package/dist/util/object/index.js +1361 -1361
  229. package/dist/util/singleton/index.js +97 -97
  230. package/dist/util/string/array-path.js +75 -75
  231. package/dist/util/string/convert.js +54 -54
  232. package/dist/util/string/fs.js +226 -226
  233. package/dist/util/string/index.js +5 -5
  234. package/dist/util/string/interpolate.js +61 -61
  235. package/dist/util/string/pad.js +10 -10
  236. package/dist/util/svelte/index.js +4 -4
  237. package/dist/util/svelte/loading/loading-tracker.svelte.js +108 -108
  238. package/dist/util/svelte/observe/index.js +49 -49
  239. package/dist/util/svelte/state-context/index.js +117 -117
  240. package/dist/util/svelte/wait/index.js +38 -38
  241. package/dist/util/sveltekit/index.js +1 -1
  242. package/dist/util/sveltekit/route-folders/index.js +101 -101
  243. package/dist/util/time/index.js +323 -323
  244. package/dist/util/unique/index.js +249 -249
  245. package/dist/valibot/date.js__ +10 -10
  246. package/dist/valibot/index.js +9 -9
  247. package/dist/valibot/url.js +95 -95
  248. package/dist/valibot/user.js +23 -23
  249. package/dist/zod/all.js +33 -33
  250. package/dist/zod/generic.js +11 -11
  251. package/dist/zod/javascript.js +32 -32
  252. package/dist/zod/user.js +16 -16
  253. package/dist/zod/web.js +52 -52
  254. package/package.json +133 -132
@@ -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;