@hkdigital/lib-core 0.4.18 → 0.4.19

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 (32) hide show
  1. package/dist/services/README.md +234 -9
  2. package/dist/services/service-base/ServiceBase.d.ts +59 -22
  3. package/dist/services/service-base/ServiceBase.js +139 -61
  4. package/dist/services/service-base/constants.d.ts +30 -14
  5. package/dist/services/service-base/constants.js +43 -14
  6. package/dist/services/service-base/typedef.d.ts +15 -9
  7. package/dist/services/service-base/typedef.js +29 -7
  8. package/dist/services/service-manager/ServiceManager.d.ts +20 -3
  9. package/dist/services/service-manager/ServiceManager.js +93 -16
  10. package/dist/services/service-manager/typedef.d.ts +25 -0
  11. package/dist/services/service-manager/typedef.js +11 -0
  12. package/dist/services/service-manager-plugins/ConfigPlugin.d.ts +78 -0
  13. package/dist/services/service-manager-plugins/ConfigPlugin.js +266 -0
  14. package/dist/util/sveltekit/env/README.md +424 -0
  15. package/dist/util/sveltekit/env/all.d.ts +54 -0
  16. package/dist/util/sveltekit/env/all.js +97 -0
  17. package/dist/util/sveltekit/env/parsers.d.ts +135 -0
  18. package/dist/util/sveltekit/env/parsers.js +257 -0
  19. package/dist/util/sveltekit/env/private.d.ts +56 -0
  20. package/dist/util/sveltekit/env/private.js +87 -0
  21. package/dist/util/sveltekit/env/public.d.ts +52 -0
  22. package/dist/util/sveltekit/env/public.js +82 -0
  23. package/dist/util/sveltekit/env-all.d.ts +1 -0
  24. package/dist/util/sveltekit/env-all.js +19 -0
  25. package/dist/util/sveltekit/env-private.d.ts +1 -0
  26. package/dist/util/sveltekit/env-private.js +18 -0
  27. package/dist/util/sveltekit/env-public.d.ts +1 -0
  28. package/dist/util/sveltekit/env-public.js +18 -0
  29. package/package.json +1 -1
  30. package/dist/util/index.js__ +0 -20
  31. package/dist/util/sveltekit/index.d.ts +0 -0
  32. package/dist/util/sveltekit/index.js +0 -0
@@ -15,9 +15,9 @@ All services follow a standardized state machine with proper error handling, log
15
15
 
16
16
  Services transition through these states during their lifecycle:
17
17
 
18
- - `created` - Service instantiated but not initialized
19
- - `initializing` - Currently running initialization
20
- - `initialized` - Ready to start
18
+ - `created` - Service instantiated but not configured
19
+ - `configuring` - Currently running configuration
20
+ - `configured` - Ready to start
21
21
  - `starting` - Currently starting up
22
22
  - `running` - Operational and healthy
23
23
  - `stopping` - Currently shutting down
@@ -31,7 +31,8 @@ Services transition through these states during their lifecycle:
31
31
 
32
32
  Base class that all services should extend. Provides:
33
33
 
34
- - Standardized lifecycle methods (`initialize`, `start`, `stop`, `destroy`)
34
+ - Standardized lifecycle methods (`configure`, `start`, `stop`, `destroy`)
35
+ - Flexible configuration system with reconfiguration support
35
36
  - Health monitoring and recovery
36
37
  - Event emission for state changes
37
38
  - Integrated logging
@@ -43,8 +44,35 @@ Base class that all services should extend. Provides:
43
44
  import { ServiceBase } from '$lib/services/index.js';
44
45
 
45
46
  class DatabaseService extends ServiceBase {
46
- async _init(config) {
47
- this.connectionString = config.connectionString;
47
+ // eslint-disable-next-line no-unused-vars
48
+ async _configure(newConfig, oldConfig = null) {
49
+
50
+ if (!oldConfig) {
51
+ // Initial configuration
52
+
53
+ this.connectionString = newConfig.connectionString;
54
+ this.maxConnections = newConfig.maxConnections || 10;
55
+ return;
56
+ }
57
+
58
+ if (oldConfig.connectionString !== newConfig.connectionString) {
59
+ // Reconfiguration - handle changes intelligently
60
+
61
+ // Connection changed - need to reconnect
62
+ await this.connection?.close();
63
+
64
+ this.connectionString = newConfig.connectionString;
65
+ if (this.state === 'running') {
66
+ this.connection = await createConnection(this.connectionString);
67
+ }
68
+ }
69
+
70
+ if (oldConfig.maxConnections !== newConfig.maxConnections) {
71
+ // Pool size changed - update without reconnect
72
+ //
73
+ this.maxConnections = newConfig.maxConnections;
74
+ await this.connection?.setMaxConnections(this.maxConnections);
75
+ }
48
76
  }
49
77
 
50
78
  async _start() {
@@ -64,18 +92,27 @@ class DatabaseService extends ServiceBase {
64
92
 
65
93
  // Usage
66
94
  const db = new DatabaseService('database');
67
- await db.initialize({ connectionString: 'postgres://...' });
95
+ await db.configure({
96
+ connectionString: 'postgres://localhost/myapp',
97
+ maxConnections: 20
98
+ });
68
99
  await db.start();
69
100
 
70
101
  // Listen to events
71
102
  db.on('healthChanged', ({ healthy }) => {
72
103
  console.log(`Database is ${healthy ? 'healthy' : 'unhealthy'}`);
73
104
  });
105
+
106
+ // Reconfigure at runtime
107
+ await db.configure({
108
+ connectionString: 'postgres://localhost/myapp',
109
+ maxConnections: 50 // Hot-reloaded without restart
110
+ });
74
111
  ```
75
112
 
76
113
  ### Protected Methods to Override
77
114
 
78
- - `_init(config)` - Initialize service with configuration
115
+ - `_configure(newConfig, oldConfig = null)` - Configure service (handles both initial setup and reconfiguration)
79
116
  - `_start()` - Start the service
80
117
  - `_stop()` - Stop the service
81
118
  - `_destroy()` - Clean up resources (optional)
@@ -100,6 +137,7 @@ Manages multiple services with dependency resolution and coordinated lifecycle o
100
137
  - Health monitoring for all services
101
138
  - Centralized logging control
102
139
  - Service recovery management
140
+ - Plugin system for extending configuration resolution
103
141
 
104
142
  ### Usage
105
143
 
@@ -177,10 +215,197 @@ manager.on('service:error', async ({ service, error }) => {
177
215
  await manager.recoverService('database');
178
216
  ```
179
217
 
218
+ ## Configuration Plugins
219
+
220
+ ServiceManager supports plugins that can resolve service configuration dynamically. This is particularly useful for environment-based configuration.
221
+
222
+ ### ConfigPlugin
223
+
224
+ The most common plugin for resolving service configuration from a pre-parsed configuration object. Perfect for environment variables, config files, or any structured configuration source.
225
+
226
+ #### Basic Usage with Environment Variables
227
+
228
+ ```javascript
229
+ import { ServiceManager } from '$lib/services/index.js';
230
+ import ConfigPlugin from '$lib/services/service-manager-plugins/ConfigPlugin.js';
231
+ import { getPrivateEnv } from '$lib/util/sveltekit/env-private.js';
232
+
233
+ // Load and auto-group environment variables
234
+ const envConfig = getPrivateEnv();
235
+ // Example result:
236
+ // {
237
+ // database: { host: 'localhost', port: 5432, name: 'myapp' },
238
+ // redis: { host: 'cache-server', port: 6379 },
239
+ // jwtSecret: 'mysecret'
240
+ // }
241
+
242
+ // Create plugin with grouped config
243
+ const configPlugin = new ConfigPlugin(envConfig);
244
+
245
+ // Attach to ServiceManager
246
+ const manager = new ServiceManager();
247
+ manager.attachPlugin(configPlugin);
248
+
249
+ // Register services with config labels (not config objects)
250
+ manager.register('database', DatabaseService, 'database'); // Uses envConfig.database
251
+ manager.register('cache', RedisService, 'redis'); // Uses envConfig.redis
252
+
253
+ await manager.startAll();
254
+ ```
255
+
256
+ #### Environment Variable Grouping
257
+
258
+ The environment utilities automatically group related variables by prefix:
259
+
260
+ ```bash
261
+ # Environment variables:
262
+ DATABASE_HOST=localhost
263
+ DATABASE_PORT=5432
264
+ DATABASE_NAME=myapp
265
+ REDIS_HOST=cache-server
266
+ REDIS_PORT=6379
267
+ JWT_SECRET=mysecret
268
+ SINGLE_FLAG=true
269
+ ```
270
+
271
+ ```javascript
272
+ const envConfig = getPrivateEnv();
273
+ // Auto-groups into:
274
+ // {
275
+ // database: { host: 'localhost', port: 5432, name: 'myapp' },
276
+ // redis: { host: 'cache-server', port: 6379 },
277
+ // jwtSecret: 'mysecret',
278
+ // singleFlag: true
279
+ // }
280
+ ```
281
+
282
+ #### Advanced Configuration Sources
283
+
284
+ ```javascript
285
+ // Combine multiple config sources
286
+ const config = {
287
+ ...getPrivateEnv(), // Environment variables
288
+ ...await loadConfigFile(), // Config file
289
+ database: { // Override specific settings
290
+ ...envConfig.database,
291
+ connectionTimeout: 5000
292
+ }
293
+ };
294
+
295
+ const plugin = new ConfigPlugin(config);
296
+ ```
297
+
298
+ #### Plugin Benefits
299
+
300
+ 1. **Simple Service Registration** - Use string labels instead of complex config objects
301
+ 2. **Environment Integration** - Seamless SvelteKit environment variable integration
302
+ 3. **Dynamic Configuration** - Update config object for live updates
303
+ 4. **Clear Separation** - Configuration logic separate from service logic
304
+ 5. **Extensible** - Easy to add custom configuration sources
305
+
306
+ #### Available Environment Utilities
307
+
308
+ ```javascript
309
+ // Server-side only (private + public env vars)
310
+ import { getAllEnv } from '$lib/util/sveltekit/env-all.js';
311
+ const config = getAllEnv();
312
+
313
+ // Server-side only (private env vars only)
314
+ import { getPrivateEnv } from '$lib/util/sveltekit/env-private.js';
315
+ const config = getPrivateEnv();
316
+
317
+ // Client + server safe (public env vars only)
318
+ import { getPublicEnv } from '$lib/util/sveltekit/env-public.js';
319
+ const config = getPublicEnv();
320
+ ```
321
+
322
+ ### Plugin Methods
323
+
324
+ ```javascript
325
+ // Update configuration at runtime
326
+ configPlugin.updateConfigObject(newConfig);
327
+
328
+ // Merge additional configuration
329
+ configPlugin.mergeConfig({ additionalSettings: true });
330
+
331
+ // Get current configuration
332
+ const currentConfig = configPlugin.getConfigObject();
333
+ ```
334
+
335
+ ### Live Configuration Updates
336
+
337
+ The ConfigPlugin supports pushing configuration updates to running services:
338
+
339
+ ```javascript
340
+ // Update config for a specific label and notify all affected services
341
+ const updatedServices = await configPlugin.updateConfigLabel('database', {
342
+ host: 'new-host.example.com',
343
+ port: 5433,
344
+ maxConnections: 50
345
+ });
346
+
347
+ // Returns array of service names that were updated: ['user-service', 'order-service']
348
+ console.log(`Updated ${updatedServices.length} services`);
349
+ ```
350
+
351
+ #### How Live Updates Work
352
+
353
+ 1. **Updates the plugin's config object** for the specified label
354
+ 2. **Finds all running services** that use that config label
355
+ 3. **Calls `_configure(newConfig, oldConfig)`** on each service instance
356
+ 4. **Updates the resolved config** stored in the ServiceManager
357
+ 5. **Returns the list** of successfully updated service names
358
+ 6. **Handles errors gracefully** with detailed logging
359
+
360
+ #### Service Requirements for Live Updates
361
+
362
+ For services to support live configuration updates, they must:
363
+
364
+ 1. **Implement intelligent `_configure()` logic** that can handle both initial setup and reconfiguration
365
+ 2. **Check for meaningful changes** between old and new config
366
+ 3. **Apply changes without full restart** when possible
367
+
368
+ ```javascript
369
+ class DatabaseService extends ServiceBase {
370
+ // eslint-disable-next-line no-unused-vars
371
+ async _configure(newConfig, oldConfig = null) {
372
+ if (!oldConfig) {
373
+ // Initial configuration
374
+ this.connectionString = newConfig.connectionString;
375
+ this.maxConnections = newConfig.maxConnections || 10;
376
+ return;
377
+ }
378
+
379
+ // Live reconfiguration - handle changes intelligently
380
+ if (oldConfig.connectionString !== newConfig.connectionString) {
381
+ // Connection changed - need to reconnect
382
+ await this.connection?.close();
383
+ this.connectionString = newConfig.connectionString;
384
+
385
+ if (this.state === 'running') {
386
+ this.connection = await createConnection(this.connectionString);
387
+ }
388
+ }
389
+
390
+ if (oldConfig.maxConnections !== newConfig.maxConnections) {
391
+ // Pool size changed - update without reconnect
392
+ this.maxConnections = newConfig.maxConnections;
393
+ await this.connection?.setMaxConnections(this.maxConnections);
394
+ }
395
+ }
396
+ }
397
+ ```
398
+
399
+ This enables powerful scenarios like:
400
+ - **Runtime environment updates** without service restarts
401
+ - **A/B testing configuration changes** with instant rollback
402
+ - **Dynamic scaling** based on load conditions
403
+ - **Configuration management systems** that push updates to running applications
404
+
180
405
  ## Best Practices
181
406
 
182
407
  1. **Always extend ServiceBase** for consistent lifecycle management
183
- 2. **Keep initialization lightweight** - heavy work should be in `_start()`
408
+ 2. **Keep configuration lightweight** - heavy work should be in `_start()`
184
409
  3. **Implement proper cleanup** in `_stop()` to prevent resource leaks
185
410
  4. **Use health checks** for monitoring critical service functionality
186
411
  5. **Declare dependencies explicitly** when registering with ServiceManager
@@ -1,8 +1,7 @@
1
1
  /**
2
- * @typedef {import('./typedef.js').ServiceOptions} ServiceOptions
3
- * @typedef {import('./typedef.js').StopOptions} StopOptions
4
- * @typedef {import('./typedef.js').HealthStatus} HealthStatus
2
+ * @typedef {import('./typedef.js').ServiceState} ServiceState
5
3
  * @typedef {import('./typedef.js').StateChangeEvent} StateChangeEvent
4
+ * @typedef {import('./typedef.js').TargetStateChangeEvent} TargetStateChangeEvent
6
5
  * @typedef {import('./typedef.js').HealthChangeEvent} HealthChangeEvent
7
6
  * @typedef {import('./typedef.js').ServiceErrorEvent} ServiceErrorEvent
8
7
  */
@@ -15,13 +14,15 @@ export class ServiceBase extends EventEmitter {
15
14
  * Create a new service instance
16
15
  *
17
16
  * @param {string} name - Service name
18
- * @param {ServiceOptions} [options={}] - Service options
17
+ * @param {import('./typedef.js').ServiceOptions} [options={}] - Service options
19
18
  */
20
- constructor(name: string, options?: ServiceOptions);
19
+ constructor(name: string, options?: import("./typedef.js").ServiceOptions);
21
20
  /** @type {string} */
22
21
  name: string;
23
- /** @type {string} */
24
- state: string;
22
+ /** @type {ServiceState} */
23
+ state: ServiceState;
24
+ /** @type {ServiceState} */
25
+ targetState: ServiceState;
25
26
  /** @type {boolean} */
26
27
  healthy: boolean;
27
28
  /** @type {Error|null} */
@@ -31,13 +32,13 @@ export class ServiceBase extends EventEmitter {
31
32
  /** @private @type {number} */
32
33
  private _shutdownTimeout;
33
34
  /**
34
- * Initialize the service with configuration
35
+ * Configure the service with configuration
35
36
  *
36
37
  * @param {*} [config={}] - Service-specific configuration
37
38
  *
38
- * @returns {Promise<boolean>} True if initialization succeeded
39
+ * @returns {Promise<boolean>} True if configuration succeeded
39
40
  */
40
- initialize(config?: any): Promise<boolean>;
41
+ configure(config?: any): Promise<boolean>;
41
42
  /**
42
43
  * Start the service
43
44
  *
@@ -47,11 +48,11 @@ export class ServiceBase extends EventEmitter {
47
48
  /**
48
49
  * Stop the service with optional timeout
49
50
  *
50
- * @param {StopOptions} [options={}] - Stop options
51
+ * @param {import('./typedef.js').StopOptions} [options={}] - Stop options
51
52
  *
52
53
  * @returns {Promise<boolean>} True if the service stopped successfully
53
54
  */
54
- stop(options?: StopOptions): Promise<boolean>;
55
+ stop(options?: import("./typedef.js").StopOptions): Promise<boolean>;
55
56
  /**
56
57
  * Recover the service from error state
57
58
  *
@@ -67,9 +68,10 @@ export class ServiceBase extends EventEmitter {
67
68
  /**
68
69
  * Get the current health status of the service
69
70
  *
70
- * @returns {Promise<HealthStatus>} Health status object
71
+ * @returns {Promise<import('./typedef.js').HealthStatus>}
72
+ * Health status object
71
73
  */
72
- getHealth(): Promise<HealthStatus>;
74
+ getHealth(): Promise<import("./typedef.js").HealthStatus>;
73
75
  /**
74
76
  * Set the service log level
75
77
  *
@@ -79,14 +81,38 @@ export class ServiceBase extends EventEmitter {
79
81
  */
80
82
  setLogLevel(level: string): boolean;
81
83
  /**
82
- * Initialize the service (override in subclass)
84
+ * Configure the service (handles both initial config and reconfiguration)
83
85
  *
84
86
  * @protected
85
- * @param {*} config - Service configuration
87
+ * @param {any} newConfig - Configuration to apply
88
+ * @param {any} [oldConfig=null] - Previous config (null = initial setup)
86
89
  *
87
90
  * @returns {Promise<void>}
88
- */
89
- protected _init(config: any): Promise<void>;
91
+ *
92
+ * @remarks
93
+ * This method is called both for initial setup and reconfiguration.
94
+ * When oldConfig is provided, you should:
95
+ * 1. Compare oldConfig vs newConfig to determine changes
96
+ * 2. Clean up resources that need replacing
97
+ * 3. Apply only the changes that are necessary
98
+ * 4. Preserve resources that don't need changing
99
+ *
100
+ * @example
101
+ * async _configure(newConfig, oldConfig = null) {
102
+ * if (!oldConfig) {
103
+ * // Initial setup
104
+ * this.connection = new Connection(newConfig.url);
105
+ * return;
106
+ * }
107
+ *
108
+ * // Reconfiguration
109
+ * if (oldConfig.url !== newConfig.url) {
110
+ * await this.connection.close();
111
+ * this.connection = new Connection(newConfig.url);
112
+ * }
113
+ * }
114
+ */
115
+ protected _configure(newConfig: any, oldConfig?: any): Promise<void>;
90
116
  /**
91
117
  * Start the service (override in subclass)
92
118
  *
@@ -131,14 +157,24 @@ export class ServiceBase extends EventEmitter {
131
157
  * Set the service state and emit event
132
158
  *
133
159
  * @private
134
- * @param {string} newState - New state value
160
+ * @param {ServiceState} newState - New state value
161
+ * @emits {StateChangeEvent} EVENT_STATE_CHANGED
135
162
  */
136
163
  private _setState;
164
+ /**
165
+ * Set the service target state and emit event
166
+ *
167
+ * @private
168
+ * @param {ServiceState} newTargetState - New target state value
169
+ * @emits {TargetStateChangeEvent} EVENT_TARGET_STATE_CHANGED
170
+ */
171
+ private _setTargetState;
137
172
  /**
138
173
  * Set the health status and emit event if changed
139
174
  *
140
175
  * @private
141
176
  * @param {boolean} healthy - New health status
177
+ * @emits {HealthChangeEvent} EVENT_HEALTH_CHANGED
142
178
  */
143
179
  private _setHealthy;
144
180
  /**
@@ -147,14 +183,15 @@ export class ServiceBase extends EventEmitter {
147
183
  * @private
148
184
  * @param {string} operation - Operation that failed
149
185
  * @param {Error} error - Error that occurred
186
+ * @emits {ServiceErrorEvent} EVENT_ERROR
150
187
  */
151
188
  private _setError;
189
+ #private;
152
190
  }
153
191
  export default ServiceBase;
154
- export type ServiceOptions = import("./typedef.js").ServiceOptions;
155
- export type StopOptions = import("./typedef.js").StopOptions;
156
- export type HealthStatus = import("./typedef.js").HealthStatus;
192
+ export type ServiceState = import("./typedef.js").ServiceState;
157
193
  export type StateChangeEvent = import("./typedef.js").StateChangeEvent;
194
+ export type TargetStateChangeEvent = import("./typedef.js").TargetStateChangeEvent;
158
195
  export type HealthChangeEvent = import("./typedef.js").HealthChangeEvent;
159
196
  export type ServiceErrorEvent = import("./typedef.js").ServiceErrorEvent;
160
197
  import { EventEmitter } from '../../generic/events.js';