@hkdigital/lib-sveltekit 0.2.20 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/README.md +135 -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/DropZoneArea.svelte +119 -119
  64. package/dist/components/drag-drop/DropZoneList.svelte +125 -125
  65. package/dist/components/drag-drop/{DropZone.svelte → Dropzone.svelte} +258 -258
  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/util.js +74 -74
  86. package/dist/components/layout/index.js +1 -1
  87. package/dist/components/panels/index.js +1 -1
  88. package/dist/components/panels/panel/Panel.svelte +43 -43
  89. package/dist/components/rows/index.js +3 -3
  90. package/dist/components/rows/panel-grid-row/PanelGridRow.svelte +104 -104
  91. package/dist/components/rows/panel-row-2/PanelRow2.svelte +40 -40
  92. package/dist/components/tab-bar/HkTabBar.state.svelte.js +149 -149
  93. package/dist/components/tab-bar/HkTabBar.svelte +74 -74
  94. package/dist/components/tab-bar/HkTabBarSelector.state.svelte.js +93 -93
  95. package/dist/components/tab-bar/HkTabBarSelector.svelte +49 -49
  96. package/dist/components/tab-bar/index.js +17 -17
  97. package/dist/components/tab-bar/typedef.js +11 -11
  98. package/dist/config/imagetools-config.js +189 -189
  99. package/dist/config/imagetools.d.ts +72 -72
  100. package/dist/constants/bases.js +13 -13
  101. package/dist/constants/errors/api.js +9 -9
  102. package/dist/constants/errors/generic.js +5 -5
  103. package/dist/constants/errors/index.js +3 -3
  104. package/dist/constants/errors/jwt.js +5 -5
  105. package/dist/constants/http/headers.js +6 -6
  106. package/dist/constants/http/index.js +2 -2
  107. package/dist/constants/http/methods.js +14 -14
  108. package/dist/constants/index.js +3 -3
  109. package/dist/constants/mime/application.js +5 -5
  110. package/dist/constants/mime/audio.js +13 -13
  111. package/dist/constants/mime/image.js +3 -3
  112. package/dist/constants/mime/index.js +4 -4
  113. package/dist/constants/mime/text.js +2 -2
  114. package/dist/constants/regexp/index.js +31 -31
  115. package/dist/constants/regexp/inspiratie.js__ +95 -95
  116. package/dist/constants/regexp/text.js +49 -49
  117. package/dist/constants/regexp/user.js +32 -32
  118. package/dist/constants/regexp/web.js +3 -3
  119. package/dist/constants/state-labels/drag-states.js +6 -6
  120. package/dist/constants/state-labels/drop-states.js +6 -6
  121. package/dist/constants/state-labels/input-states.js +11 -11
  122. package/dist/constants/state-labels/submit-states.js +4 -4
  123. package/dist/constants/time.js +28 -28
  124. package/dist/css/utilities.css +43 -43
  125. package/dist/design/design-config.js +73 -73
  126. package/dist/design/tailwind-theme-extend.js +158 -158
  127. package/dist/features/button-group/ButtonGroup.svelte +82 -82
  128. package/dist/features/button-group/typedef.js +10 -10
  129. package/dist/features/compare-left-right/CompareLeftRight.svelte +179 -179
  130. package/dist/features/compare-left-right/index.js +1 -1
  131. package/dist/features/game-box/GameBox.svelte +577 -577
  132. package/dist/features/game-box/gamebox.util.js +83 -83
  133. package/dist/features/hk-app-layout/HkAppLayout.state.svelte.js +25 -25
  134. package/dist/features/hk-app-layout/HkAppLayout.svelte +251 -251
  135. package/dist/features/image-box/ImageBox.svelte +210 -210
  136. package/dist/features/image-box/index.js +5 -5
  137. package/dist/features/image-box/typedef.js +32 -32
  138. package/dist/features/index.js +23 -23
  139. package/dist/features/presenter/ImageSlide.svelte +64 -64
  140. package/dist/features/presenter/Presenter.state.svelte.js +638 -638
  141. package/dist/features/presenter/Presenter.svelte +142 -142
  142. package/dist/features/presenter/constants.js +7 -7
  143. package/dist/features/presenter/index.js +10 -10
  144. package/dist/features/presenter/typedef.js +106 -106
  145. package/dist/features/presenter/util.js +210 -210
  146. package/dist/features/virtual-viewport/VirtualViewport.svelte +196 -196
  147. package/dist/logging/adapters/console.js +114 -114
  148. package/dist/logging/adapters/pino.js +60 -60
  149. package/dist/logging/constants.js +1 -1
  150. package/dist/logging/factories/client.js +21 -21
  151. package/dist/logging/factories/server.js +22 -22
  152. package/dist/logging/factories/universal.js +23 -23
  153. package/dist/logging/index.js +8 -8
  154. package/dist/schemas/index.js +1 -1
  155. package/dist/schemas/validate-url.js +180 -180
  156. package/dist/server/index.js +1 -1
  157. package/dist/server/logger.js +94 -94
  158. package/dist/states/index.js +1 -1
  159. package/dist/states/navigation.svelte.js +55 -55
  160. package/dist/stores/index.js +1 -1
  161. package/dist/stores/theme.js +80 -80
  162. package/dist/themes/hkdev/components/blocks/text-block.css +34 -41
  163. package/dist/themes/hkdev/components/boxes/game-box.css +11 -12
  164. package/dist/themes/hkdev/components/buttons/button-icon-steeze.css +22 -22
  165. package/dist/themes/hkdev/components/buttons/button-text.css +32 -32
  166. package/dist/themes/hkdev/components/buttons/button.css +146 -146
  167. package/dist/themes/hkdev/components/buttons/skip-button.css +5 -6
  168. package/dist/themes/hkdev/components/drag-drop/draggable.css +73 -73
  169. package/dist/themes/hkdev/components/drag-drop/drop-zone.css +58 -48
  170. package/dist/themes/hkdev/components/icons/icon-steeze.css +16 -22
  171. package/dist/themes/hkdev/components/inputs/text-input.css +102 -104
  172. package/dist/themes/hkdev/components/panels/panel.css +25 -27
  173. package/dist/themes/hkdev/components/rows/panel-grid-row.css +4 -6
  174. package/dist/themes/hkdev/components/rows/panel-row-2.css +5 -7
  175. package/dist/themes/hkdev/components.css +29 -53
  176. package/dist/themes/hkdev/debug.css +1 -1
  177. package/dist/themes/hkdev/global/layout.css +32 -39
  178. package/dist/themes/hkdev/global/on-colors.css +32 -53
  179. package/dist/themes/hkdev/globals.css +4 -11
  180. package/dist/themes/hkdev/responsive.css +12 -12
  181. package/dist/themes/hkdev/theme-ext.js +12 -15
  182. package/dist/themes/hkdev/theme.css +219 -0
  183. package/dist/themes/index.d.ts +1 -1
  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/expect/arrays.js +47 -47
  205. package/dist/util/expect/index.js +259 -259
  206. package/dist/util/expect/primitives.js +55 -55
  207. package/dist/util/expect/url.js +60 -60
  208. package/dist/util/function/index.js +218 -218
  209. package/dist/util/geo/index.js +26 -26
  210. package/dist/util/http/caching.js +263 -263
  211. package/dist/util/http/errors.js +97 -97
  212. package/dist/util/http/headers.js +75 -75
  213. package/dist/util/http/http-request.js +578 -578
  214. package/dist/util/http/index.js +22 -22
  215. package/dist/util/http/json-request.js +224 -224
  216. package/dist/util/http/mocks.js +65 -65
  217. package/dist/util/http/response.js +294 -294
  218. package/dist/util/http/typedef.js +93 -93
  219. package/dist/util/http/url.js +52 -52
  220. package/dist/util/image/index.js +86 -86
  221. package/dist/util/index.js +2 -2
  222. package/dist/util/is/index.js +140 -140
  223. package/dist/util/iterate/index.js +234 -234
  224. package/dist/util/object/index.js +1361 -1361
  225. package/dist/util/singleton/index.js +97 -97
  226. package/dist/util/string/array-path.js +75 -75
  227. package/dist/util/string/convert.js +54 -54
  228. package/dist/util/string/fs.js +226 -226
  229. package/dist/util/string/index.js +5 -5
  230. package/dist/util/string/interpolate.js +61 -61
  231. package/dist/util/string/pad.js +10 -10
  232. package/dist/util/svelte/index.js +4 -4
  233. package/dist/util/svelte/loading/loading-tracker.svelte.js +108 -108
  234. package/dist/util/svelte/observe/index.js +49 -49
  235. package/dist/util/svelte/state-context/index.js +117 -117
  236. package/dist/util/svelte/wait/index.js +38 -38
  237. package/dist/util/sveltekit/index.js +1 -1
  238. package/dist/util/sveltekit/route-folders/index.js +101 -101
  239. package/dist/util/time/index.js +323 -323
  240. package/dist/util/unique/index.js +249 -249
  241. package/dist/valibot/date.js__ +10 -10
  242. package/dist/valibot/index.js +9 -9
  243. package/dist/valibot/url.js +95 -95
  244. package/dist/valibot/user.js +23 -23
  245. package/dist/zod/all.js +33 -33
  246. package/dist/zod/generic.js +11 -11
  247. package/dist/zod/javascript.js +32 -32
  248. package/dist/zod/user.js +16 -16
  249. package/dist/zod/web.js +52 -52
  250. package/package.json +132 -129
  251. package/dist/components/layout/grid-layers/GridLayers.svelte__heightFrom__ +0 -372
  252. package/dist/themes/hkdev/theme.d.ts +0 -234
  253. package/dist/themes/hkdev/theme.js +0 -235
  254. package/dist/util/http/test-data__/content-length-test-hkdigital-small.V4HfZyBQ.avif +0 -0
@@ -1,463 +1,463 @@
1
- /**
2
- * @fileoverview Base service class with lifecycle management, health checks,
3
- * and integrated logging.
4
- *
5
- * ServiceBase provides a standardized lifecycle for all services with states,
6
- * events, logging, and error handling. Services extend this class and override
7
- * the protected methods to implement their specific functionality.
8
- *
9
- * @example
10
- * // Basic service implementation
11
- * import { ServiceBase } from './ServiceBase.js';
12
- *
13
- * class DatabaseService extends ServiceBase {
14
- * async _init(config) {
15
- * this.connectionString = config.connectionString;
16
- * }
17
- *
18
- * async _start() {
19
- * this.connection = await createConnection(this.connectionString);
20
- * }
21
- *
22
- * async _stop() {
23
- * await this.connection?.close();
24
- * }
25
- * }
26
- *
27
- * // Usage
28
- * const db = new DatabaseService('database');
29
- * await db.initialize({ connectionString: 'postgres://...' });
30
- * await db.start();
31
- *
32
- * // Listen to events
33
- * db.on('healthChanged', ({ healthy }) => {
34
- * console.log(`Database is ${healthy ? 'healthy' : 'unhealthy'}`);
35
- * });
36
- *
37
- * @example
38
- * // Service with recovery and health checks
39
- * class ApiService extends ServiceBase {
40
- * async _recover() {
41
- * // Custom recovery logic
42
- * await this.reconnect();
43
- * }
44
- *
45
- * async _healthCheck() {
46
- * const start = Date.now();
47
- * await this.ping();
48
- * return { latency: Date.now() - start };
49
- * }
50
- * }
51
- */
52
-
53
- import { EventEmitter } from '../events';
54
- import { Logger, INFO } from '../logging';
55
-
56
- import {
57
- CREATED,
58
- INITIALIZING,
59
- INITIALIZED,
60
- STARTING,
61
- RUNNING,
62
- STOPPING,
63
- STOPPED,
64
- DESTROYING,
65
- DESTROYED,
66
- ERROR as ERROR_STATE,
67
- RECOVERING
68
- } from './service-states.js';
69
-
70
- /**
71
- * @typedef {import('./typedef.js').ServiceConfig} ServiceConfig
72
- * @typedef {import('./typedef.js').ServiceOptions} ServiceOptions
73
- * @typedef {import('./typedef.js').StopOptions} StopOptions
74
- * @typedef {import('./typedef.js').HealthStatus} HealthStatus
75
- * @typedef {import('./typedef.js').StateChangeEvent} StateChangeEvent
76
- * @typedef {import('./typedef.js').HealthChangeEvent} HealthChangeEvent
77
- * @typedef {import('./typedef.js').ServiceErrorEvent} ServiceErrorEvent
78
- */
79
-
80
- /**
81
- * Base class for all services with lifecycle management
82
- * @extends EventEmitter
83
- */
84
- export class ServiceBase extends EventEmitter {
85
- /**
86
- * Create a new service instance
87
- *
88
- * @param {string} name - Service name
89
- * @param {ServiceOptions} [options={}] - Service options
90
- */
91
- constructor(name, options = {}) {
92
- super();
93
-
94
- /** @type {string} */
95
- this.name = name;
96
-
97
- /** @type {string} */
98
- this.state = CREATED;
99
-
100
- /** @type {boolean} */
101
- this.healthy = false;
102
-
103
- /** @type {Error|null} */
104
- this.error = null;
105
-
106
- /** @type {Logger} */
107
- this.logger = new Logger(name, options.logLevel || INFO);
108
-
109
- /** @private @type {number} */
110
- this._shutdownTimeout = options.shutdownTimeout || 5000;
111
- }
112
-
113
- /**
114
- * Initialize the service with configuration
115
- *
116
- * @param {ServiceConfig} [config={}] - Service-specific configuration
117
- *
118
- * @returns {Promise<boolean>} True if initialization succeeded
119
- */
120
- async initialize(config = {}) {
121
- if (this.state !== CREATED &&
122
- this.state !== STOPPED &&
123
- this.state !== DESTROYED) {
124
- this.logger.warn(`Cannot initialize from state: ${this.state}`);
125
- return false;
126
- }
127
-
128
- try {
129
- this._setState(INITIALIZING);
130
- this.logger.debug('Initializing service', { config });
131
-
132
- await this._init(config);
133
-
134
- this._setState(INITIALIZED);
135
- this.logger.info('Service initialized');
136
- return true;
137
- } catch (error) {
138
- this._setError('initialization', error);
139
- return false;
140
- }
141
- }
142
-
143
- /**
144
- * Start the service
145
- *
146
- * @returns {Promise<boolean>} True if the service started successfully
147
- */
148
- async start() {
149
- if (this.state !== INITIALIZED && this.state !== STOPPED) {
150
- this.logger.warn(`Cannot start from state: ${this.state}`);
151
- return false;
152
- }
153
-
154
- try {
155
- this._setState(STARTING);
156
- this.logger.debug('Starting service');
157
-
158
- await this._start();
159
-
160
- this._setState(RUNNING);
161
- this._setHealthy(true);
162
- this.logger.info('Service started');
163
- return true;
164
- } catch (error) {
165
- this._setError('startup', error);
166
- return false;
167
- }
168
- }
169
-
170
- /**
171
- * Stop the service with optional timeout
172
- *
173
- * @param {StopOptions} [options={}] - Stop options
174
- *
175
- * @returns {Promise<boolean>} True if the service stopped successfully
176
- */
177
- async stop(options = {}) {
178
- if (this.state !== RUNNING && this.state !== ERROR_STATE) {
179
- this.logger.warn(`Cannot stop from state: ${this.state}`);
180
- return true; // Already stopped
181
- }
182
-
183
- const timeout = options.timeout ?? this._shutdownTimeout;
184
-
185
- try {
186
- this._setState(STOPPING);
187
- this._setHealthy(false);
188
- this.logger.debug('Stopping service');
189
-
190
- // Wrap _stop in a timeout
191
- const stopPromise = this._stop();
192
-
193
- if (timeout > 0) {
194
- await Promise.race([
195
- stopPromise,
196
- new Promise((_, reject) =>
197
- setTimeout(() => reject(new Error('Shutdown timeout')), timeout)
198
- )
199
- ]);
200
- } else {
201
- await stopPromise;
202
- }
203
-
204
- this._setState(STOPPED);
205
- this.logger.info('Service stopped');
206
- return true;
207
- } catch (error) {
208
- if (error.message === 'Shutdown timeout' && options.force) {
209
- this.logger.warn('Forced shutdown after timeout');
210
- this._setState(STOPPED);
211
- return true;
212
- }
213
- this._setError('shutdown', error);
214
- return false;
215
- }
216
- }
217
-
218
- /**
219
- * Recover the service from error state
220
- *
221
- * @returns {Promise<boolean>} True if recovery succeeded
222
- */
223
- async recover() {
224
- if (this.state !== ERROR_STATE) {
225
- this.logger.warn(
226
- `Can only recover from ERROR state, current: ${this.state}`
227
- );
228
- return false;
229
- }
230
-
231
- try {
232
- this._setState(RECOVERING);
233
- this.logger.info('Attempting recovery');
234
-
235
- // Try custom recovery first
236
- if (this._recover) {
237
- await this._recover();
238
- this._setState(RUNNING);
239
- this._setHealthy(true);
240
- } else {
241
- // Default: restart
242
- this._setState(STOPPED);
243
- await this.start();
244
- }
245
-
246
- this.error = null;
247
- this.logger.info('Recovery successful');
248
- return true;
249
- } catch (error) {
250
- this._setError('recovery', error);
251
- return false;
252
- }
253
- }
254
-
255
- /**
256
- * Destroy the service and cleanup resources
257
- *
258
- * @returns {Promise<boolean>} True if destruction succeeded
259
- */
260
- async destroy() {
261
- if (this.state === DESTROYED) {
262
- return true;
263
- }
264
-
265
- try {
266
- if (this.state === RUNNING) {
267
- await this.stop();
268
- }
269
-
270
- this._setState(DESTROYING);
271
- this.logger.debug('Destroying service');
272
-
273
- if (this._destroy) {
274
- await this._destroy();
275
- }
276
-
277
- this._setState(DESTROYED);
278
- this._setHealthy(false);
279
- this.logger.info('Service destroyed');
280
-
281
- // Cleanup
282
- this.removeAllListeners();
283
- this.logger.removeAllListeners();
284
-
285
- return true;
286
- } catch (error) {
287
- this._setError('destruction', error);
288
- return false;
289
- }
290
- }
291
-
292
- /**
293
- * Get the current health status of the service
294
- *
295
- * @returns {Promise<HealthStatus>} Health status object
296
- */
297
- async getHealth() {
298
- const baseHealth = {
299
- name: this.name,
300
- state: this.state,
301
- healthy: this.healthy,
302
- error: this.error?.message
303
- };
304
-
305
- if (this._healthCheck) {
306
- try {
307
- const customHealth = await this._healthCheck();
308
- return { ...baseHealth, ...customHealth };
309
- } catch (error) {
310
- return {
311
- ...baseHealth,
312
- healthy: false,
313
- checkError: error.message
314
- };
315
- }
316
- }
317
-
318
- return baseHealth;
319
- }
320
-
321
- /**
322
- * Set the service log level
323
- *
324
- * @param {string} level - New log level
325
- *
326
- * @returns {boolean} True if the level was set successfully
327
- */
328
- setLogLevel(level) {
329
- return this.logger.setLevel(level);
330
- }
331
-
332
- // Protected methods to override in subclasses
333
-
334
- /**
335
- * Initialize the service (override in subclass)
336
- *
337
- * @protected
338
- * @param {ServiceConfig} config - Service configuration
339
- *
340
- * @returns {Promise<void>}
341
- */
342
- async _init(config) {
343
- // Override in subclass
344
- }
345
-
346
- /**
347
- * Start the service (override in subclass)
348
- *
349
- * @protected
350
- *
351
- * @returns {Promise<void>}
352
- */
353
- async _start() {
354
- // Override in subclass
355
- }
356
-
357
- /**
358
- * Stop the service (override in subclass)
359
- *
360
- * @protected
361
- *
362
- * @returns {Promise<void>}
363
- */
364
- async _stop() {
365
- // Override in subclass
366
- }
367
-
368
- /**
369
- * Destroy the service (optional override)
370
- *
371
- * @protected
372
- *
373
- * @returns {Promise<void>}
374
- */
375
- async _destroy() {
376
- // Override in subclass if needed
377
- }
378
-
379
- /**
380
- * Recover from error state (optional override)
381
- *
382
- * @protected
383
- *
384
- * @returns {Promise<void>}
385
- */
386
- async _recover() {
387
- // Override in subclass if custom recovery needed
388
- // Default behavior is stop + start
389
- }
390
-
391
- /**
392
- * Perform health check (optional override)
393
- *
394
- * @protected
395
- *
396
- * @returns {Promise<Object>} Additional health information
397
- */
398
- async _healthCheck() {
399
- // Override in subclass if health checks needed
400
- return {};
401
- }
402
-
403
- // Private methods
404
-
405
- /**
406
- * Set the service state and emit event
407
- *
408
- * @private
409
- * @param {string} newState - New state value
410
- */
411
- _setState(newState) {
412
- const oldState = this.state;
413
- this.state = newState;
414
-
415
- this.emit('stateChanged', {
416
- oldState,
417
- newState
418
- });
419
- }
420
-
421
- /**
422
- * Set the health status and emit event if changed
423
- *
424
- * @private
425
- * @param {boolean} healthy - New health status
426
- */
427
- _setHealthy(healthy) {
428
- const wasHealthy = this.healthy;
429
- this.healthy = healthy;
430
-
431
- if (wasHealthy !== healthy) {
432
- this.emit('healthChanged', {
433
- healthy,
434
- wasHealthy
435
- });
436
- }
437
- }
438
-
439
- /**
440
- * Set error state and emit error event
441
- *
442
- * @private
443
- * @param {string} operation - Operation that failed
444
- * @param {Error} error - Error that occurred
445
- */
446
- _setError(operation, error) {
447
- this.error = error;
448
- this._setState(ERROR_STATE);
449
- this._setHealthy(false);
450
-
451
- this.logger.error(`${operation} failed`, {
452
- error: error.message,
453
- stack: error.stack
454
- });
455
-
456
- this.emit('error', {
457
- operation,
458
- error
459
- });
460
- }
461
- }
462
-
463
- export default ServiceBase;
1
+ /**
2
+ * @fileoverview Base service class with lifecycle management, health checks,
3
+ * and integrated logging.
4
+ *
5
+ * ServiceBase provides a standardized lifecycle for all services with states,
6
+ * events, logging, and error handling. Services extend this class and override
7
+ * the protected methods to implement their specific functionality.
8
+ *
9
+ * @example
10
+ * // Basic service implementation
11
+ * import { ServiceBase } from './ServiceBase.js';
12
+ *
13
+ * class DatabaseService extends ServiceBase {
14
+ * async _init(config) {
15
+ * this.connectionString = config.connectionString;
16
+ * }
17
+ *
18
+ * async _start() {
19
+ * this.connection = await createConnection(this.connectionString);
20
+ * }
21
+ *
22
+ * async _stop() {
23
+ * await this.connection?.close();
24
+ * }
25
+ * }
26
+ *
27
+ * // Usage
28
+ * const db = new DatabaseService('database');
29
+ * await db.initialize({ connectionString: 'postgres://...' });
30
+ * await db.start();
31
+ *
32
+ * // Listen to events
33
+ * db.on('healthChanged', ({ healthy }) => {
34
+ * console.log(`Database is ${healthy ? 'healthy' : 'unhealthy'}`);
35
+ * });
36
+ *
37
+ * @example
38
+ * // Service with recovery and health checks
39
+ * class ApiService extends ServiceBase {
40
+ * async _recover() {
41
+ * // Custom recovery logic
42
+ * await this.reconnect();
43
+ * }
44
+ *
45
+ * async _healthCheck() {
46
+ * const start = Date.now();
47
+ * await this.ping();
48
+ * return { latency: Date.now() - start };
49
+ * }
50
+ * }
51
+ */
52
+
53
+ import { EventEmitter } from '../events';
54
+ import { Logger, INFO } from '../logging';
55
+
56
+ import {
57
+ CREATED,
58
+ INITIALIZING,
59
+ INITIALIZED,
60
+ STARTING,
61
+ RUNNING,
62
+ STOPPING,
63
+ STOPPED,
64
+ DESTROYING,
65
+ DESTROYED,
66
+ ERROR as ERROR_STATE,
67
+ RECOVERING
68
+ } from './service-states.js';
69
+
70
+ /**
71
+ * @typedef {import('./typedef.js').ServiceConfig} ServiceConfig
72
+ * @typedef {import('./typedef.js').ServiceOptions} ServiceOptions
73
+ * @typedef {import('./typedef.js').StopOptions} StopOptions
74
+ * @typedef {import('./typedef.js').HealthStatus} HealthStatus
75
+ * @typedef {import('./typedef.js').StateChangeEvent} StateChangeEvent
76
+ * @typedef {import('./typedef.js').HealthChangeEvent} HealthChangeEvent
77
+ * @typedef {import('./typedef.js').ServiceErrorEvent} ServiceErrorEvent
78
+ */
79
+
80
+ /**
81
+ * Base class for all services with lifecycle management
82
+ * @extends EventEmitter
83
+ */
84
+ export class ServiceBase extends EventEmitter {
85
+ /**
86
+ * Create a new service instance
87
+ *
88
+ * @param {string} name - Service name
89
+ * @param {ServiceOptions} [options={}] - Service options
90
+ */
91
+ constructor(name, options = {}) {
92
+ super();
93
+
94
+ /** @type {string} */
95
+ this.name = name;
96
+
97
+ /** @type {string} */
98
+ this.state = CREATED;
99
+
100
+ /** @type {boolean} */
101
+ this.healthy = false;
102
+
103
+ /** @type {Error|null} */
104
+ this.error = null;
105
+
106
+ /** @type {Logger} */
107
+ this.logger = new Logger(name, options.logLevel || INFO);
108
+
109
+ /** @private @type {number} */
110
+ this._shutdownTimeout = options.shutdownTimeout || 5000;
111
+ }
112
+
113
+ /**
114
+ * Initialize the service with configuration
115
+ *
116
+ * @param {ServiceConfig} [config={}] - Service-specific configuration
117
+ *
118
+ * @returns {Promise<boolean>} True if initialization succeeded
119
+ */
120
+ async initialize(config = {}) {
121
+ if (this.state !== CREATED &&
122
+ this.state !== STOPPED &&
123
+ this.state !== DESTROYED) {
124
+ this.logger.warn(`Cannot initialize from state: ${this.state}`);
125
+ return false;
126
+ }
127
+
128
+ try {
129
+ this._setState(INITIALIZING);
130
+ this.logger.debug('Initializing service', { config });
131
+
132
+ await this._init(config);
133
+
134
+ this._setState(INITIALIZED);
135
+ this.logger.info('Service initialized');
136
+ return true;
137
+ } catch (error) {
138
+ this._setError('initialization', error);
139
+ return false;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Start the service
145
+ *
146
+ * @returns {Promise<boolean>} True if the service started successfully
147
+ */
148
+ async start() {
149
+ if (this.state !== INITIALIZED && this.state !== STOPPED) {
150
+ this.logger.warn(`Cannot start from state: ${this.state}`);
151
+ return false;
152
+ }
153
+
154
+ try {
155
+ this._setState(STARTING);
156
+ this.logger.debug('Starting service');
157
+
158
+ await this._start();
159
+
160
+ this._setState(RUNNING);
161
+ this._setHealthy(true);
162
+ this.logger.info('Service started');
163
+ return true;
164
+ } catch (error) {
165
+ this._setError('startup', error);
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Stop the service with optional timeout
172
+ *
173
+ * @param {StopOptions} [options={}] - Stop options
174
+ *
175
+ * @returns {Promise<boolean>} True if the service stopped successfully
176
+ */
177
+ async stop(options = {}) {
178
+ if (this.state !== RUNNING && this.state !== ERROR_STATE) {
179
+ this.logger.warn(`Cannot stop from state: ${this.state}`);
180
+ return true; // Already stopped
181
+ }
182
+
183
+ const timeout = options.timeout ?? this._shutdownTimeout;
184
+
185
+ try {
186
+ this._setState(STOPPING);
187
+ this._setHealthy(false);
188
+ this.logger.debug('Stopping service');
189
+
190
+ // Wrap _stop in a timeout
191
+ const stopPromise = this._stop();
192
+
193
+ if (timeout > 0) {
194
+ await Promise.race([
195
+ stopPromise,
196
+ new Promise((_, reject) =>
197
+ setTimeout(() => reject(new Error('Shutdown timeout')), timeout)
198
+ )
199
+ ]);
200
+ } else {
201
+ await stopPromise;
202
+ }
203
+
204
+ this._setState(STOPPED);
205
+ this.logger.info('Service stopped');
206
+ return true;
207
+ } catch (error) {
208
+ if (error.message === 'Shutdown timeout' && options.force) {
209
+ this.logger.warn('Forced shutdown after timeout');
210
+ this._setState(STOPPED);
211
+ return true;
212
+ }
213
+ this._setError('shutdown', error);
214
+ return false;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Recover the service from error state
220
+ *
221
+ * @returns {Promise<boolean>} True if recovery succeeded
222
+ */
223
+ async recover() {
224
+ if (this.state !== ERROR_STATE) {
225
+ this.logger.warn(
226
+ `Can only recover from ERROR state, current: ${this.state}`
227
+ );
228
+ return false;
229
+ }
230
+
231
+ try {
232
+ this._setState(RECOVERING);
233
+ this.logger.info('Attempting recovery');
234
+
235
+ // Try custom recovery first
236
+ if (this._recover) {
237
+ await this._recover();
238
+ this._setState(RUNNING);
239
+ this._setHealthy(true);
240
+ } else {
241
+ // Default: restart
242
+ this._setState(STOPPED);
243
+ await this.start();
244
+ }
245
+
246
+ this.error = null;
247
+ this.logger.info('Recovery successful');
248
+ return true;
249
+ } catch (error) {
250
+ this._setError('recovery', error);
251
+ return false;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Destroy the service and cleanup resources
257
+ *
258
+ * @returns {Promise<boolean>} True if destruction succeeded
259
+ */
260
+ async destroy() {
261
+ if (this.state === DESTROYED) {
262
+ return true;
263
+ }
264
+
265
+ try {
266
+ if (this.state === RUNNING) {
267
+ await this.stop();
268
+ }
269
+
270
+ this._setState(DESTROYING);
271
+ this.logger.debug('Destroying service');
272
+
273
+ if (this._destroy) {
274
+ await this._destroy();
275
+ }
276
+
277
+ this._setState(DESTROYED);
278
+ this._setHealthy(false);
279
+ this.logger.info('Service destroyed');
280
+
281
+ // Cleanup
282
+ this.removeAllListeners();
283
+ this.logger.removeAllListeners();
284
+
285
+ return true;
286
+ } catch (error) {
287
+ this._setError('destruction', error);
288
+ return false;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Get the current health status of the service
294
+ *
295
+ * @returns {Promise<HealthStatus>} Health status object
296
+ */
297
+ async getHealth() {
298
+ const baseHealth = {
299
+ name: this.name,
300
+ state: this.state,
301
+ healthy: this.healthy,
302
+ error: this.error?.message
303
+ };
304
+
305
+ if (this._healthCheck) {
306
+ try {
307
+ const customHealth = await this._healthCheck();
308
+ return { ...baseHealth, ...customHealth };
309
+ } catch (error) {
310
+ return {
311
+ ...baseHealth,
312
+ healthy: false,
313
+ checkError: error.message
314
+ };
315
+ }
316
+ }
317
+
318
+ return baseHealth;
319
+ }
320
+
321
+ /**
322
+ * Set the service log level
323
+ *
324
+ * @param {string} level - New log level
325
+ *
326
+ * @returns {boolean} True if the level was set successfully
327
+ */
328
+ setLogLevel(level) {
329
+ return this.logger.setLevel(level);
330
+ }
331
+
332
+ // Protected methods to override in subclasses
333
+
334
+ /**
335
+ * Initialize the service (override in subclass)
336
+ *
337
+ * @protected
338
+ * @param {ServiceConfig} config - Service configuration
339
+ *
340
+ * @returns {Promise<void>}
341
+ */
342
+ async _init(config) {
343
+ // Override in subclass
344
+ }
345
+
346
+ /**
347
+ * Start the service (override in subclass)
348
+ *
349
+ * @protected
350
+ *
351
+ * @returns {Promise<void>}
352
+ */
353
+ async _start() {
354
+ // Override in subclass
355
+ }
356
+
357
+ /**
358
+ * Stop the service (override in subclass)
359
+ *
360
+ * @protected
361
+ *
362
+ * @returns {Promise<void>}
363
+ */
364
+ async _stop() {
365
+ // Override in subclass
366
+ }
367
+
368
+ /**
369
+ * Destroy the service (optional override)
370
+ *
371
+ * @protected
372
+ *
373
+ * @returns {Promise<void>}
374
+ */
375
+ async _destroy() {
376
+ // Override in subclass if needed
377
+ }
378
+
379
+ /**
380
+ * Recover from error state (optional override)
381
+ *
382
+ * @protected
383
+ *
384
+ * @returns {Promise<void>}
385
+ */
386
+ async _recover() {
387
+ // Override in subclass if custom recovery needed
388
+ // Default behavior is stop + start
389
+ }
390
+
391
+ /**
392
+ * Perform health check (optional override)
393
+ *
394
+ * @protected
395
+ *
396
+ * @returns {Promise<Object>} Additional health information
397
+ */
398
+ async _healthCheck() {
399
+ // Override in subclass if health checks needed
400
+ return {};
401
+ }
402
+
403
+ // Private methods
404
+
405
+ /**
406
+ * Set the service state and emit event
407
+ *
408
+ * @private
409
+ * @param {string} newState - New state value
410
+ */
411
+ _setState(newState) {
412
+ const oldState = this.state;
413
+ this.state = newState;
414
+
415
+ this.emit('stateChanged', {
416
+ oldState,
417
+ newState
418
+ });
419
+ }
420
+
421
+ /**
422
+ * Set the health status and emit event if changed
423
+ *
424
+ * @private
425
+ * @param {boolean} healthy - New health status
426
+ */
427
+ _setHealthy(healthy) {
428
+ const wasHealthy = this.healthy;
429
+ this.healthy = healthy;
430
+
431
+ if (wasHealthy !== healthy) {
432
+ this.emit('healthChanged', {
433
+ healthy,
434
+ wasHealthy
435
+ });
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Set error state and emit error event
441
+ *
442
+ * @private
443
+ * @param {string} operation - Operation that failed
444
+ * @param {Error} error - Error that occurred
445
+ */
446
+ _setError(operation, error) {
447
+ this.error = error;
448
+ this._setState(ERROR_STATE);
449
+ this._setHealthy(false);
450
+
451
+ this.logger.error(`${operation} failed`, {
452
+ error: error.message,
453
+ stack: error.stack
454
+ });
455
+
456
+ this.emit('error', {
457
+ operation,
458
+ error
459
+ });
460
+ }
461
+ }
462
+
463
+ export default ServiceBase;