@hkdigital/lib-core 0.3.11 → 0.3.13

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