@hkdigital/lib-core 0.4.19 → 0.4.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.
@@ -11,21 +11,32 @@ The services module provides two main components:
11
11
 
12
12
  All services follow a standardized state machine with proper error handling, logging, and health monitoring.
13
13
 
14
- ## Service States
15
-
16
- Services transition through these states during their lifecycle:
17
-
18
- - `created` - Service instantiated but not configured
19
- - `configuring` - Currently running configuration
20
- - `configured` - Ready to start
21
- - `starting` - Currently starting up
22
- - `running` - Operational and healthy
23
- - `stopping` - Currently shutting down
24
- - `stopped` - Cleanly stopped
25
- - `destroying` - Being destroyed and cleaned up
26
- - `destroyed` - Completely destroyed
27
- - `error` - Failed and non-operational
28
- - `recovering` - Attempting recovery from error
14
+ ## Service states
15
+
16
+ Services transition through these states during their lifecycle. Use these constants from `$lib/services/service-base/constants.js`:
17
+
18
+ - `STATE_CREATED` - Service instantiated but not configured
19
+ - `STATE_CONFIGURING` - Currently running configuration
20
+ - `STATE_CONFIGURED` - Ready to start
21
+ - `STATE_STARTING` - Currently starting up
22
+ - `STATE_RUNNING` - Operational and healthy
23
+ - `STATE_STOPPING` - Currently shutting down
24
+ - `STATE_STOPPED` - Cleanly stopped
25
+ - `STATE_DESTROYING` - Being destroyed and cleaned up
26
+ - `STATE_DESTROYED` - Completely destroyed
27
+ - `STATE_ERROR` - Failed and non-operational
28
+ - `STATE_RECOVERING` - Attempting recovery from error
29
+
30
+ ```javascript
31
+ import {
32
+ STATE_RUNNING,
33
+ STATE_ERROR
34
+ } from '$lib/services/service-base/constants.js';
35
+
36
+ if (service.state === STATE_RUNNING) {
37
+ // Service is operational
38
+ }
39
+ ```
29
40
 
30
41
  ## ServiceBase
31
42
 
@@ -46,7 +57,6 @@ import { ServiceBase } from '$lib/services/index.js';
46
57
  class DatabaseService extends ServiceBase {
47
58
  // eslint-disable-next-line no-unused-vars
48
59
  async _configure(newConfig, oldConfig = null) {
49
-
50
60
  if (!oldConfig) {
51
61
  // Initial configuration
52
62
 
@@ -106,7 +116,7 @@ db.on('healthChanged', ({ healthy }) => {
106
116
  // Reconfigure at runtime
107
117
  await db.configure({
108
118
  connectionString: 'postgres://localhost/myapp',
109
- maxConnections: 50 // Hot-reloaded without restart
119
+ maxConnections: 50 // Hot-reloaded without restart
110
120
  });
111
121
  ```
112
122
 
@@ -119,11 +129,22 @@ await db.configure({
119
129
  - `_recover()` - Custom recovery logic (optional)
120
130
  - `_healthCheck()` - Return health status (optional)
121
131
 
122
- ### Events
132
+ ### Service events
133
+
134
+ ServiceBase emits these events (constants from `$lib/services/service-base/constants.js`):
123
135
 
124
- - `stateChanged` - Service state transitions
125
- - `healthChanged` - Health status changes
126
- - `error` - Service errors
136
+ - `EVENT_STATE_CHANGED` - Service state transitions
137
+ - `EVENT_TARGET_STATE_CHANGED` - Target state changes
138
+ - `EVENT_HEALTH_CHANGED` - Health status changes
139
+ - `EVENT_ERROR` - Service errors
140
+
141
+ ```javascript
142
+ import { EVENT_STATE_CHANGED } from '$lib/services/service-base/constants.js';
143
+
144
+ service.on(EVENT_STATE_CHANGED, ({ state, previousState }) => {
145
+ console.log(`Service transitioned from ${previousState} to ${state}`);
146
+ });
147
+ ```
127
148
 
128
149
  ## ServiceManager
129
150
 
@@ -143,6 +164,7 @@ Manages multiple services with dependency resolution and coordinated lifecycle o
143
164
 
144
165
  ```javascript
145
166
  import { ServiceManager } from '$hklib-core/services/index.js';
167
+
146
168
  import DatabaseService from './services/DatabaseService.js';
147
169
  import AuthService from './services/AuthService.js';
148
170
 
@@ -156,11 +178,16 @@ manager.register('database', DatabaseService, {
156
178
  connectionString: 'postgres://localhost/myapp'
157
179
  });
158
180
 
159
- manager.register('auth', AuthService, {
160
- secret: process.env.JWT_SECRET
161
- }, {
162
- dependencies: ['database'] // auth depends on database
163
- });
181
+ manager.register(
182
+ 'auth',
183
+ AuthService,
184
+ {
185
+ secret: process.env.JWT_SECRET
186
+ },
187
+ {
188
+ dependencies: ['database'] // auth depends on database
189
+ }
190
+ );
164
191
 
165
192
  // Start all services in dependency order
166
193
  await manager.startAll();
@@ -175,19 +202,27 @@ await manager.stopAll();
175
202
  ### Service Registration
176
203
 
177
204
  ```javascript
178
- manager.register(name, ServiceClass, config, options);
205
+ manager.register(name, ServiceClass, serviceConfigOrLabel, options);
179
206
  ```
180
207
 
181
208
  - `name` - Unique service identifier
182
209
  - `ServiceClass` - Class extending ServiceBase
183
- - `config` - Service-specific configuration
210
+ - `serviceConfigOrLabel` - Service configuration object (`Object<string, *>`) or config label string
184
211
  - `options.dependencies` - Array of service names this service depends on
212
+ - `options.startupPriority` - Higher priority services start first (default: 0)
185
213
 
186
214
  ### Health Monitoring
187
215
 
188
216
  ```javascript
217
+ import {
218
+ SERVICE_HEALTH_CHANGED,
219
+ SERVICE_ERROR,
220
+ SERVICE_STATE_CHANGED,
221
+ SERVICE_LOG
222
+ } from '$lib/services/service-manager/constants.js';
223
+
189
224
  // Listen for health changes
190
- manager.on('service:healthChanged', ({ service, healthy }) => {
225
+ manager.on(SERVICE_HEALTH_CHANGED, ({ service, healthy }) => {
191
226
  if (!healthy) {
192
227
  console.error(`Service ${service} became unhealthy`);
193
228
  }
@@ -202,11 +237,20 @@ const systemHealth = await manager.checkHealth();
202
237
 
203
238
  ### Error Handling and Recovery
204
239
 
240
+ ### ServiceManager events
241
+
242
+ ServiceManager emits these events (constants from `$lib/services/service-manager/constants.js`):
243
+
244
+ - `SERVICE_STATE_CHANGED` - Service state changes
245
+ - `SERVICE_HEALTH_CHANGED` - Service health changes
246
+ - `SERVICE_ERROR` - Service errors
247
+ - `SERVICE_LOG` - Service log messages
248
+
205
249
  ```javascript
206
250
  // Listen for service errors
207
- manager.on('service:error', async ({ service, error }) => {
251
+ manager.on(SERVICE_ERROR, async ({ service, error }) => {
208
252
  console.log(`Service ${service} failed:`, error.message);
209
-
253
+
210
254
  // Attempt automatic recovery
211
255
  await manager.recoverService(service);
212
256
  });
@@ -215,9 +259,9 @@ manager.on('service:error', async ({ service, error }) => {
215
259
  await manager.recoverService('database');
216
260
  ```
217
261
 
218
- ## Configuration Plugins
262
+ ## Plugins
219
263
 
220
- ServiceManager supports plugins that can resolve service configuration dynamically. This is particularly useful for environment-based configuration.
264
+ ServiceManager supports plugins e.g. to resolve service configurations dynamically.
221
265
 
222
266
  ### ConfigPlugin
223
267
 
@@ -227,17 +271,29 @@ The most common plugin for resolving service configuration from a pre-parsed con
227
271
 
228
272
  ```javascript
229
273
  import { ServiceManager } from '$lib/services/index.js';
230
- import ConfigPlugin from '$lib/services/service-manager-plugins/ConfigPlugin.js';
274
+
275
+ import ConfigPlugin from '$lib/services/manager-plugins/ConfigPlugin.js';
276
+
231
277
  import { getPrivateEnv } from '$lib/util/sveltekit/env-private.js';
232
278
 
233
279
  // Load and auto-group environment variables
234
280
  const envConfig = getPrivateEnv();
235
- // Example result:
281
+ //
282
+ // Example:
283
+ //
284
+ // DATABASE_HOST=localhost
285
+ // DATABASE_PORT=5432
286
+ // DATABASE_NAME=myapp
287
+ // REDIS_HOST=cache-server
288
+ // REDIS_PORT=6379
289
+ // JWT_SECRET=mysecret
290
+ // =>
236
291
  // {
237
292
  // database: { host: 'localhost', port: 5432, name: 'myapp' },
238
293
  // redis: { host: 'cache-server', port: 6379 },
239
294
  // jwtSecret: 'mysecret'
240
295
  // }
296
+ //
241
297
 
242
298
  // Create plugin with grouped config
243
299
  const configPlugin = new ConfigPlugin(envConfig);
@@ -247,46 +303,23 @@ const manager = new ServiceManager();
247
303
  manager.attachPlugin(configPlugin);
248
304
 
249
305
  // 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
306
+ manager.register('database', DatabaseService, 'database'); // Uses envConfig.database
307
+ manager.register('cache', RedisService, 'redis'); // Uses envConfig.redis
252
308
 
253
309
  await manager.startAll();
254
310
  ```
255
311
 
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
- ```
312
+ #### Configuration
281
313
 
282
- #### Advanced Configuration Sources
314
+ The plugin constructor accepts an object with configuration data, which can come from any source. E.g. the environment or a configuration file.
283
315
 
284
316
  ```javascript
285
317
  // Combine multiple config sources
286
318
  const config = {
287
- ...getPrivateEnv(), // Environment variables
288
- ...await loadConfigFile(), // Config file
289
- database: { // Override specific settings
319
+ ...getPrivateEnv(), // Environment variables
320
+ ...(await loadConfigFile()), // Config file
321
+ database: {
322
+ // Override specific settings
290
323
  ...envConfig.database,
291
324
  connectionTimeout: 5000
292
325
  }
@@ -295,41 +328,17 @@ const config = {
295
328
  const plugin = new ConfigPlugin(config);
296
329
  ```
297
330
 
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
331
+ ### Methods
307
332
 
308
333
  ```javascript
309
- // Server-side only (private + public env vars)
310
- import { getAllEnv } from '$lib/util/sveltekit/env-all.js';
311
- const config = getAllEnv();
334
+ // Replace all configurations and clean up unused ones
335
+ await configPlugin.replaceAllConfigs(newConfig);
312
336
 
313
- // Server-side only (private env vars only)
314
- import { getPrivateEnv } from '$lib/util/sveltekit/env-private.js';
315
- const config = getPrivateEnv();
337
+ // Replace configuration for a specific label
338
+ await configPlugin.replaceConfig('database', newDatabaseConfig);
316
339
 
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();
340
+ // Clean up configurations not used by any service
341
+ await configPlugin.cleanupConfigs();
333
342
  ```
334
343
 
335
344
  ### Live Configuration Updates
@@ -337,8 +346,8 @@ const currentConfig = configPlugin.getConfigObject();
337
346
  The ConfigPlugin supports pushing configuration updates to running services:
338
347
 
339
348
  ```javascript
340
- // Update config for a specific label and notify all affected services
341
- const updatedServices = await configPlugin.updateConfigLabel('database', {
349
+ // Replace config for a specific label and notify all affected services
350
+ const updatedServices = await configPlugin.replaceConfig('database', {
342
351
  host: 'new-host.example.com',
343
352
  port: 5433,
344
353
  maxConnections: 50
@@ -348,15 +357,6 @@ const updatedServices = await configPlugin.updateConfigLabel('database', {
348
357
  console.log(`Updated ${updatedServices.length} services`);
349
358
  ```
350
359
 
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
360
  #### Service Requirements for Live Updates
361
361
 
362
362
  For services to support live configuration updates, they must:
@@ -381,7 +381,7 @@ class DatabaseService extends ServiceBase {
381
381
  // Connection changed - need to reconnect
382
382
  await this.connection?.close();
383
383
  this.connectionString = newConfig.connectionString;
384
-
384
+
385
385
  if (this.state === 'running') {
386
386
  this.connection = await createConnection(this.connectionString);
387
387
  }
@@ -396,12 +396,6 @@ class DatabaseService extends ServiceBase {
396
396
  }
397
397
  ```
398
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
-
405
399
  ## Best Practices
406
400
 
407
401
  1. **Always extend ServiceBase** for consistent lifecycle management
@@ -411,15 +405,3 @@ This enables powerful scenarios like:
411
405
  5. **Declare dependencies explicitly** when registering with ServiceManager
412
406
  6. **Handle errors gracefully** and implement recovery where appropriate
413
407
  7. **Use descriptive service names** for better logging and debugging
414
-
415
- ## Testing
416
-
417
- Services include comprehensive test suites demonstrating:
418
-
419
- - Lifecycle state transitions
420
- - Error handling and recovery
421
- - Dependency resolution
422
- - Health monitoring
423
- - Event emission
424
-
425
- Run tests with your project's test command to ensure service reliability.
@@ -1,3 +1,4 @@
1
+ /** @typedef {import('../service-manager/typedef.js').ServiceEntry} ServiceEntry */
1
2
  /**
2
3
  * Plugin that resolves service configuration from a configuration object
3
4
  */
@@ -5,9 +6,9 @@ export default class ConfigPlugin {
5
6
  /**
6
7
  * Create a new object configuration plugin
7
8
  *
8
- * @param {Object<string, *>} configObject - Pre-parsed configuration object
9
+ * @param {Object<string, *>} allConfigs - Pre-parsed configuration object
9
10
  */
10
- constructor(configObject: {
11
+ constructor(allConfigs: {
11
12
  [x: string]: any;
12
13
  });
13
14
  /** @type {string} */
@@ -15,54 +16,42 @@ export default class ConfigPlugin {
15
16
  /** @type {import('../service-manager/ServiceManager.js').ServiceManager|null} */
16
17
  manager: import("../service-manager/ServiceManager.js").ServiceManager | null;
17
18
  /** @type {Object<string, *>} */
18
- configObject: {
19
+ allConfigs: {
19
20
  [x: string]: any;
20
21
  };
21
22
  /**
22
23
  * Resolve service configuration from the configuration object
23
24
  *
24
- * @param {string} serviceName - Name of the service being configured
25
- * @param {import('../service-manager/typedef.js').ServiceEntry} serviceEntry
26
- * Service registration entry
27
- * @param {*} currentConfig - Current config (could be object from previous plugins)
25
+ * @param {string} serviceName
26
+ * @param {ServiceEntry} serviceEntry - Service registration entry
27
+ * @param {*} currentConfig
28
+ * Current config (could be object from previous plugins)
28
29
  *
29
30
  * @returns {Promise<Object|undefined>}
30
31
  * Resolved config object, or undefined to use currentConfig as-is
31
32
  */
32
- _getServiceConfig(serviceName: string, serviceEntry: import("../service-manager/typedef.js").ServiceEntry, currentConfig: any): Promise<any | undefined>;
33
+ resolveServiceConfig(serviceName: string, serviceEntry: ServiceEntry, currentConfig: any): Promise<any | undefined>;
33
34
  /**
34
- * Update the configuration object
35
+ * Replace the entire configuration object and clean up unused configs
35
36
  *
36
37
  * @param {Object<string, *>} newConfigObject - New configuration object
37
38
  */
38
- updateConfigObject(newConfigObject: {
39
+ replaceAllConfigs(newConfigObject: {
39
40
  [x: string]: any;
40
- }): void;
41
+ }): Promise<void>;
41
42
  /**
42
- * Merge additional configuration into the existing object
43
- *
44
- * @param {Object<string, *>} additionalConfig - Additional configuration
45
- */
46
- mergeConfig(additionalConfig: {
47
- [x: string]: any;
48
- }): void;
49
- /**
50
- * Get the current configuration object
51
- *
52
- * @returns {Object<string, *>} Current configuration object
53
- */
54
- getConfigObject(): {
55
- [x: string]: any;
56
- };
57
- /**
58
- * Update config for a specific label and push to affected services
43
+ * Replace a config for a specific label and push to affected services
59
44
  *
60
45
  * @param {string} configLabel - Config label to update
61
46
  * @param {*} newConfig - New configuration value
62
47
  *
63
48
  * @returns {Promise<string[]>} Array of service names that were updated
64
49
  */
65
- updateConfigLabel(configLabel: string, newConfig: any): Promise<string[]>;
50
+ replaceConfig(configLabel: string, newConfig: any): Promise<string[]>;
51
+ /**
52
+ * Remove all unused configurations (configs not referenced by any service)
53
+ */
54
+ cleanupConfigs(): Promise<void>;
66
55
  /**
67
56
  * Attach plugin to ServiceManager
68
57
  *
@@ -76,3 +65,4 @@ export default class ConfigPlugin {
76
65
  detach(): void;
77
66
  #private;
78
67
  }
68
+ export type ServiceEntry = import("../service-manager/typedef.js").ServiceEntry;
@@ -8,12 +8,12 @@
8
8
  * // Basic usage with config object
9
9
  * import ConfigPlugin from './ConfigPlugin.js';
10
10
  *
11
- * const configObject = {
11
+ * const allConfigs = {
12
12
  * 'database': { host: 'localhost', port: 5432 },
13
13
  * 'auth': { secret: 'my-secret', algorithm: 'HS256' }
14
14
  * };
15
15
  *
16
- * const objectPlugin = new ConfigPlugin(configObject);
16
+ * const objectPlugin = new ConfigPlugin(allConfigs);
17
17
  * manager.attachPlugin(objectPlugin);
18
18
  *
19
19
  * @example
@@ -41,20 +41,21 @@
41
41
 
42
42
  import { SERVICE_STATE_CHANGED } from '../service-manager/constants.js';
43
43
 
44
+ /** @typedef {import('../service-manager/typedef.js').ServiceEntry} ServiceEntry */
45
+
44
46
  /**
45
47
  * Plugin that resolves service configuration from a configuration object
46
48
  */
47
49
  export default class ConfigPlugin {
48
-
49
50
  /** @type {Map<string, *>} */
50
51
  #pendingConfigUpdates;
51
52
 
52
53
  /**
53
54
  * Create a new object configuration plugin
54
55
  *
55
- * @param {Object<string, *>} configObject - Pre-parsed configuration object
56
+ * @param {Object<string, *>} allConfigs - Pre-parsed configuration object
56
57
  */
57
- constructor(configObject) {
58
+ constructor(allConfigs) {
58
59
  /** @type {string} */
59
60
  this.name = 'object-config';
60
61
 
@@ -62,7 +63,7 @@ export default class ConfigPlugin {
62
63
  this.manager = null;
63
64
 
64
65
  /** @type {Object<string, *>} */
65
- this.configObject = configObject || {};
66
+ this.allConfigs = allConfigs || {};
66
67
 
67
68
  this.#pendingConfigUpdates = new Map();
68
69
  }
@@ -70,25 +71,32 @@ export default class ConfigPlugin {
70
71
  /**
71
72
  * Resolve service configuration from the configuration object
72
73
  *
73
- * @param {string} serviceName - Name of the service being configured
74
- * @param {import('../service-manager/typedef.js').ServiceEntry} serviceEntry
75
- * Service registration entry
76
- * @param {*} currentConfig - Current config (could be object from previous plugins)
74
+ * @param {string} serviceName
75
+ * @param {ServiceEntry} serviceEntry - Service registration entry
76
+ * @param {*} currentConfig
77
+ * Current config (could be object from previous plugins)
77
78
  *
78
79
  * @returns {Promise<Object|undefined>}
79
80
  * Resolved config object, or undefined to use currentConfig as-is
80
81
  */
81
82
  // eslint-disable-next-line no-unused-vars
82
- async _getServiceConfig(serviceName, serviceEntry, currentConfig) {
83
- // Only handle string config labels from original registration
84
- if (typeof serviceEntry.config !== 'string') {
83
+ async resolveServiceConfig(serviceName, serviceEntry, currentConfig) {
84
+ // console.log(`ConfigPlugin.resolveServiceConfig called for '${serviceName}'`);
85
+ // console.log('ServiceEntry:', serviceEntry);
86
+ // console.log('AllConfigs:', this.allConfigs);
87
+
88
+ const configLabel = serviceEntry.serviceConfigOrLabel;
89
+ // console.log('Config label:', configLabel, 'Type:', typeof configLabel);
90
+
91
+ if (typeof configLabel !== 'string') {
92
+ // console.log('Config label is not string, returning undefined');
93
+ // Expected config label
85
94
  return undefined;
86
95
  }
87
96
 
88
- const configLabel = serviceEntry.config;
89
-
90
97
  // Simple object lookup
91
- const config = this.configObject[configLabel];
98
+ const config = this.allConfigs[configLabel];
99
+ // console.log(`Looking up config for label '${configLabel}':`, config);
92
100
 
93
101
  if (config !== undefined) {
94
102
  this.manager?.logger?.debug(
@@ -98,65 +106,69 @@ export default class ConfigPlugin {
98
106
  typeof config === 'object' ? Object.keys(config) : 'primitive'
99
107
  }
100
108
  );
109
+ // console.log(`Resolved config for '${serviceName}':`, config);
110
+ } else {
111
+ // console.log(`No config found for label '${configLabel}'`);
101
112
  }
102
113
 
103
114
  return config;
104
115
  }
105
116
 
106
117
  /**
107
- * Update the configuration object
118
+ * Replace the entire configuration object and clean up unused configs
108
119
  *
109
120
  * @param {Object<string, *>} newConfigObject - New configuration object
110
121
  */
111
- updateConfigObject(newConfigObject) {
112
- this.configObject = newConfigObject || {};
113
- this.manager?.logger?.debug('Updated configuration object');
114
- }
122
+ async replaceAllConfigs(newConfigObject) {
123
+ await this.cleanupConfigs();
115
124
 
116
- /**
117
- * Merge additional configuration into the existing object
118
- *
119
- * @param {Object<string, *>} additionalConfig - Additional configuration
120
- */
121
- mergeConfig(additionalConfig) {
122
- this.configObject = { ...this.configObject, ...additionalConfig };
123
- this.manager?.logger?.debug('Merged additional configuration');
124
- }
125
+ // Apply new configs to all services that have config labels
126
+ const updatedServices = [];
127
+ for (const [configLabel, newConfig] of Object.entries(
128
+ newConfigObject || {}
129
+ )) {
130
+ const services = await this.replaceConfig(configLabel, newConfig);
131
+ updatedServices.push(...services);
132
+ }
125
133
 
126
- /**
127
- * Get the current configuration object
128
- *
129
- * @returns {Object<string, *>} Current configuration object
130
- */
131
- getConfigObject() {
132
- return { ...this.configObject };
134
+ this.manager?.logger?.debug(
135
+ `Replaced all configurations, updated ${updatedServices.length} services`
136
+ );
133
137
  }
134
138
 
135
139
  /**
136
- * Update config for a specific label and push to affected services
140
+ * Replace a config for a specific label and push to affected services
137
141
  *
138
142
  * @param {string} configLabel - Config label to update
139
143
  * @param {*} newConfig - New configuration value
140
144
  *
141
145
  * @returns {Promise<string[]>} Array of service names that were updated
142
146
  */
143
- async updateConfigLabel(configLabel, newConfig) {
147
+ async replaceConfig(configLabel, newConfig) {
144
148
  // Update the config object
145
- this.configObject[configLabel] = newConfig;
149
+ this.allConfigs[configLabel] = newConfig;
146
150
 
147
151
  // Store as pending update
148
152
  this.#pendingConfigUpdates.set(configLabel, newConfig);
149
153
 
150
154
  const updatedServices = [];
151
155
 
152
- // Find all services using this config label
153
- for (const [serviceName, serviceEntry] of this.manager.services) {
154
- if (serviceEntry.config === configLabel && serviceEntry.instance) {
156
+ // Find all services using this config label using helper function
157
+ const servicesByLabel = this.#servicesByConfigLabel();
158
+ const serviceNames = servicesByLabel.get(configLabel) || [];
159
+
160
+ for (const serviceName of serviceNames) {
161
+ const serviceEntry = this.manager.services.get(serviceName);
162
+ if (serviceEntry && serviceEntry.instance) {
155
163
  try {
156
164
  // Try to apply config - ServiceBase.configure() will handle state validation
157
- await this.#applyConfigToService(serviceName, serviceEntry, newConfig);
165
+ await this.#applyConfigToService(
166
+ serviceName,
167
+ serviceEntry,
168
+ newConfig
169
+ );
158
170
  updatedServices.push(serviceName);
159
-
171
+
160
172
  // Remove from pending since it was applied
161
173
  this.#pendingConfigUpdates.delete(configLabel);
162
174
  } catch (error) {
@@ -175,6 +187,60 @@ export default class ConfigPlugin {
175
187
  return updatedServices;
176
188
  }
177
189
 
190
+ /**
191
+ * Get services organized by their config labels
192
+ *
193
+ * @returns {Map<string, string[]>}
194
+ * Map where keys are config labels and values are arrays of service names
195
+ */
196
+ #servicesByConfigLabel() {
197
+ const servicesByLabel = new Map();
198
+
199
+ for (const [serviceName, serviceEntry] of this.manager.services) {
200
+ const configLabel = serviceEntry.serviceConfigOrLabel;
201
+
202
+ if (typeof configLabel === 'string') {
203
+ if (!servicesByLabel.has(configLabel)) {
204
+ servicesByLabel.set(configLabel, []);
205
+ }
206
+
207
+ servicesByLabel.get(configLabel).push(serviceName);
208
+ }
209
+ }
210
+
211
+ return servicesByLabel;
212
+ }
213
+
214
+ /**
215
+ * Remove all unused configurations (configs not referenced by any service)
216
+ */
217
+ async cleanupConfigs() {
218
+ const usedConfigLabels = new Set();
219
+
220
+ // Collect all config labels used by registered services
221
+ for (const [, serviceEntry] of this.manager.services) {
222
+ const configLabel = serviceEntry.serviceConfigOrLabel;
223
+
224
+ if (typeof configLabel === 'string') {
225
+ usedConfigLabels.add(configLabel);
226
+ }
227
+ }
228
+
229
+ // Remove configs that aren't used by any service
230
+ const configKeys = Object.keys(this.allConfigs);
231
+ let removedCount = 0;
232
+
233
+ for (const key of configKeys) {
234
+ if (!usedConfigLabels.has(key)) {
235
+ delete this.allConfigs[key];
236
+ removedCount++;
237
+ }
238
+ }
239
+
240
+ this.manager?.logger?.debug(
241
+ `Cleaned up ${removedCount} unused configurations`
242
+ );
243
+ }
178
244
 
179
245
  /**
180
246
  * Apply configuration to a specific service
@@ -187,9 +253,7 @@ export default class ConfigPlugin {
187
253
  async #applyConfigToService(serviceName, serviceEntry, newConfig) {
188
254
  await serviceEntry.instance.configure(newConfig);
189
255
 
190
- this.manager.logger.info(
191
- `Updated config for service '${serviceName}'`
192
- );
256
+ this.manager.logger.info(`Updated config for service '${serviceName}'`);
193
257
  }
194
258
 
195
259
  /**
@@ -201,18 +265,24 @@ export default class ConfigPlugin {
201
265
  */
202
266
  async #processPendingUpdates(serviceName, serviceInstance) {
203
267
  const serviceEntry = this.manager.services.get(serviceName);
204
- if (!serviceEntry || typeof serviceEntry.config !== 'string') {
268
+
269
+ const configLabel = serviceEntry?.serviceConfigOrLabel;
270
+
271
+ if (typeof configLabel !== 'string') {
205
272
  return;
206
273
  }
207
274
 
208
- const configLabel = serviceEntry.config;
209
275
  if (this.#pendingConfigUpdates.has(configLabel)) {
210
276
  const pendingConfig = this.#pendingConfigUpdates.get(configLabel);
211
-
277
+
212
278
  try {
213
- await this.#applyConfigToService(serviceName, serviceEntry, pendingConfig);
279
+ await this.#applyConfigToService(
280
+ serviceName,
281
+ serviceEntry,
282
+ pendingConfig
283
+ );
214
284
  this.#pendingConfigUpdates.delete(configLabel);
215
-
285
+
216
286
  this.manager.logger.info(
217
287
  `Applied pending config update for service '${serviceName}' (label: ${configLabel})`
218
288
  );
@@ -239,16 +309,19 @@ export default class ConfigPlugin {
239
309
 
240
310
  this.manager = manager;
241
311
 
242
- const configKeys = Object.keys(this.configObject).length;
312
+ const configKeys = Object.keys(this.allConfigs).length;
243
313
 
244
314
  this.manager.logger.info(
245
315
  `ConfigPlugin attached with ${configKeys} config keys`
246
316
  );
247
317
 
248
318
  // Listen for service state changes to process pending updates
249
- this.manager.on(SERVICE_STATE_CHANGED, async ({ service, state, instance }) => {
250
- await this.#processPendingUpdates(service, instance);
251
- });
319
+ this.manager.on(
320
+ SERVICE_STATE_CHANGED,
321
+ async ({ service, state, instance }) => {
322
+ await this.#processPendingUpdates(service, instance);
323
+ }
324
+ );
252
325
  }
253
326
 
254
327
  /**
@@ -258,7 +331,7 @@ export default class ConfigPlugin {
258
331
  if (this.manager) {
259
332
  // Clear pending updates
260
333
  this.#pendingConfigUpdates.clear();
261
-
334
+
262
335
  this.manager.logger.info('ConfigPlugin detached');
263
336
  this.manager = null;
264
337
  }
@@ -124,4 +124,3 @@ export const EVENT_HEALTH_CHANGED = 'healthChanged';
124
124
  * Event emitted when service encounters an error
125
125
  */
126
126
  export const EVENT_ERROR = 'error';
127
-
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * @fileoverview Type definitions for ServiceBase class.
3
- *
3
+ *
4
4
  * This file contains all TypeScript/JSDoc type definitions used by
5
5
  * the ServiceBase class and service implementations.
6
- *
6
+ *
7
7
  * @example
8
8
  * // In your service implementation
9
9
  * import { ServiceBase } from './ServiceBase.js';
10
- *
10
+ *
11
11
  * class MyService extends ServiceBase {
12
12
  * async _configure(newConfig, oldConfig) {
13
13
  * }
14
- *
14
+ *
15
15
  * async _healthCheck() {
16
16
  * // Return type is HealthStatus
17
17
  * return { latency: 10 };
@@ -3,6 +3,7 @@
3
3
  * @typedef {import('./typedef.js').ServiceRegistrationOptions} ServiceRegistrationOptions
4
4
  * @typedef {import('./typedef.js').ServiceManagerConfig} ServiceManagerConfig
5
5
  * @typedef {import('./typedef.js').ServiceEntry} ServiceEntry
6
+ * @typedef {import('./typedef.js').ServiceConfigOrLabel} ServiceConfigOrLabel
6
7
  * @typedef {import('./typedef.js').HealthCheckResult} HealthCheckResult
7
8
  *
8
9
  * @typedef {import('../service-base/typedef.js').StopOptions} StopOptions
@@ -46,12 +47,13 @@ export class ServiceManager extends EventEmitter {
46
47
  *
47
48
  * @param {string} name - Unique service identifier
48
49
  * @param {ServiceConstructor} ServiceClass - Service class constructor
49
- * @param {*} [config={}] - Service configuration
50
+ * @param {ServiceConfigOrLabel} [serviceConfigOrLabel={}]
51
+ * Service configuration object or config label string
50
52
  * @param {ServiceRegistrationOptions} [options={}] - Registration options
51
53
  *
52
54
  * @throws {Error} If service name is already registered
53
55
  */
54
- register(name: string, ServiceClass: ServiceConstructor, config?: any, options?: ServiceRegistrationOptions): void;
56
+ register(name: string, ServiceClass: ServiceConstructor, serviceConfigOrLabel?: ServiceConfigOrLabel, options?: ServiceRegistrationOptions): void;
55
57
  /**
56
58
  * Get or create a service instance
57
59
  *
@@ -156,6 +158,7 @@ export type ServiceConstructor = import("./typedef.js").ServiceConstructor;
156
158
  export type ServiceRegistrationOptions = import("./typedef.js").ServiceRegistrationOptions;
157
159
  export type ServiceManagerConfig = import("./typedef.js").ServiceManagerConfig;
158
160
  export type ServiceEntry = import("./typedef.js").ServiceEntry;
161
+ export type ServiceConfigOrLabel = import("./typedef.js").ServiceConfigOrLabel;
159
162
  export type HealthCheckResult = import("./typedef.js").HealthCheckResult;
160
163
  export type StopOptions = import("../service-base/typedef.js").StopOptions;
161
164
  import { EventEmitter } from '../../generic/events.js';
@@ -78,6 +78,7 @@ import {
78
78
  * @typedef {import('./typedef.js').ServiceRegistrationOptions} ServiceRegistrationOptions
79
79
  * @typedef {import('./typedef.js').ServiceManagerConfig} ServiceManagerConfig
80
80
  * @typedef {import('./typedef.js').ServiceEntry} ServiceEntry
81
+ * @typedef {import('./typedef.js').ServiceConfigOrLabel} ServiceConfigOrLabel
81
82
  * @typedef {import('./typedef.js').HealthCheckResult} HealthCheckResult
82
83
  *
83
84
  * @typedef {import('../service-base/typedef.js').StopOptions} StopOptions
@@ -158,12 +159,13 @@ export class ServiceManager extends EventEmitter {
158
159
  *
159
160
  * @param {string} name - Unique service identifier
160
161
  * @param {ServiceConstructor} ServiceClass - Service class constructor
161
- * @param {*} [config={}] - Service configuration
162
+ * @param {ServiceConfigOrLabel} [serviceConfigOrLabel={}]
163
+ * Service configuration object or config label string
162
164
  * @param {ServiceRegistrationOptions} [options={}] - Registration options
163
165
  *
164
166
  * @throws {Error} If service name is already registered
165
167
  */
166
- register(name, ServiceClass, config = {}, options = {}) {
168
+ register(name, ServiceClass, serviceConfigOrLabel = {}, options = {}) {
167
169
  if (this.services.has(name)) {
168
170
  throw new Error(`Service '${name}' already registered`);
169
171
  }
@@ -172,11 +174,11 @@ export class ServiceManager extends EventEmitter {
172
174
  const entry = {
173
175
  ServiceClass,
174
176
  instance: null,
175
- config,
177
+ serviceConfigOrLabel: serviceConfigOrLabel,
176
178
  dependencies: options.dependencies || [],
177
179
  dependents: new Set(),
178
180
  tags: options.tags || [],
179
- priority: options.priority || 0
181
+ startupPriority: options.startupPriority || 0
180
182
  };
181
183
 
182
184
  // Track dependents
@@ -245,7 +247,7 @@ export class ServiceManager extends EventEmitter {
245
247
  if (!instance) return false;
246
248
 
247
249
  const entry = this.services.get(name);
248
- const config = await this.#resolveConfig(name, entry);
250
+ const config = await this.#resolveServiceConfig(name, entry);
249
251
 
250
252
  return await instance.configure(config);
251
253
  }
@@ -559,25 +561,29 @@ export class ServiceManager extends EventEmitter {
559
561
  *
560
562
  * @returns {Promise<*>} Resolved configuration object
561
563
  */
562
- async #resolveConfig(serviceName, serviceEntry) {
563
- let config = serviceEntry.config;
564
-
565
- // Let plugins resolve the config
566
- for (const plugin of this.#plugins.values()) {
567
- if (plugin._getServiceConfig) {
568
- const resolved = await plugin._getServiceConfig(
569
- serviceName,
570
- serviceEntry,
571
- config
572
- );
573
- if (resolved !== undefined) {
574
- config = resolved;
575
- break; // First plugin that resolves wins
564
+ async #resolveServiceConfig(serviceName, serviceEntry) {
565
+ let serviceConfigOrLabel = serviceEntry.serviceConfigOrLabel;
566
+
567
+ if (typeof serviceConfigOrLabel === 'string') {
568
+ const configLabel = serviceConfigOrLabel;
569
+
570
+ // Let plugins resolve the config
571
+ for (const plugin of this.#plugins.values()) {
572
+ if (plugin.resolveServiceConfig) {
573
+ const config = await plugin.resolveServiceConfig(
574
+ serviceName,
575
+ serviceEntry,
576
+ configLabel
577
+ );
578
+ if (config !== undefined) {
579
+ return config; // First plugin that resolves wins
580
+ }
576
581
  }
577
582
  }
583
+ } else {
584
+ const config = serviceConfigOrLabel;
585
+ return config;
578
586
  }
579
-
580
- return config || {};
581
587
  }
582
588
 
583
589
  /**
@@ -1,4 +1,3 @@
1
-
2
1
  // Log event names
3
2
  export const SERVICE_STATE_CHANGED = 'service:state-changed';
4
3
  export const SERVICE_HEALTH_CHANGED = 'service:health-changed';
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Service configuration - either a config object or a config label string
3
+ */
4
+ export type ServiceConfigOrLabel = {
5
+ [x: string]: any;
6
+ } | string;
1
7
  /**
2
8
  * Options for registering a service
3
9
  */
@@ -11,9 +17,9 @@ export type ServiceRegistrationOptions = {
11
17
  */
12
18
  tags?: string[];
13
19
  /**
14
- * - Startup priority (higher starts first)
20
+ * - Higher starts first
15
21
  */
16
- priority?: number;
22
+ startupPriority?: number;
17
23
  };
18
24
  /**
19
25
  * Configuration for ServiceManager
@@ -92,7 +98,7 @@ export type ServiceManagerPlugin = {
92
98
  /**
93
99
  * - Optional config resolution method
94
100
  */
95
- _getServiceConfig?: (arg0: string, arg1: ServiceEntry, arg2: any) => Promise<any | undefined>;
101
+ resolveServiceConfig?: (arg0: string, arg1: ServiceEntry, arg2: any) => Promise<any | undefined>;
96
102
  };
97
103
  /**
98
104
  * Internal service registry entry
@@ -106,24 +112,15 @@ export type ServiceEntry = {
106
112
  * - Service instance (lazy-created)
107
113
  */
108
114
  instance: import("../service-base/typedef.js").ServiceInstance | null;
109
- /**
110
- * - Service configuration
111
- */
112
- config: any;
113
- /**
114
- * - Service dependencies
115
- */
115
+ serviceConfigOrLabel: ServiceConfigOrLabel;
116
116
  dependencies: string[];
117
117
  /**
118
- * - Services that depend on this one
118
+ * - Services that depend on this service
119
119
  */
120
120
  dependents: Set<string>;
121
- /**
122
- * - Service tags
123
- */
124
121
  tags: string[];
125
122
  /**
126
123
  * - Startup priority
127
124
  */
128
- priority: number;
125
+ startupPriority: number;
129
126
  };
@@ -1,28 +1,28 @@
1
1
  /**
2
2
  * @fileoverview Type definitions for ServiceManager class.
3
- *
3
+ *
4
4
  * This file contains all TypeScript/JSDoc type definitions used by
5
5
  * the ServiceManager class and service registration.
6
- *
6
+ *
7
7
  * @example
8
8
  * // When using ServiceManager
9
9
  * import { ServiceManager } from './ServiceManager.js';
10
- *
10
+ *
11
11
  * // @ typedef {import('./typedef-service-manager.js').ServiceManagerConfig} ServiceManagerConfig
12
12
  * // @ typedef {import('./typedef-service-manager.js').ServiceRegistrationOptions} ServiceRegistrationOptions
13
- *
13
+ *
14
14
  * const config = {
15
15
  * environment: 'development',
16
16
  * stopTimeout: 5000
17
17
  * };
18
- *
18
+ *
19
19
  * const manager = new ServiceManager(config);
20
- *
20
+ *
21
21
  * const options = {
22
22
  * dependencies: ['database'],
23
23
  * tags: ['critical']
24
24
  * };
25
- *
25
+ *
26
26
  * manager.register('auth', AuthService, {}, options);
27
27
  */
28
28
 
@@ -30,13 +30,19 @@
30
30
  // PUBLIC TYPES
31
31
  // ============================================================================
32
32
 
33
+ /**
34
+ * Service configuration - either a config object or a config label string
35
+ *
36
+ * @typedef {Object<string, *>|string} ServiceConfigOrLabel
37
+ */
38
+
33
39
  /**
34
40
  * Options for registering a service
35
41
  *
36
42
  * @typedef {Object} ServiceRegistrationOptions
37
43
  * @property {string[]} [dependencies=[]] - Services this service depends on
38
44
  * @property {string[]} [tags=[]] - Tags for grouping services
39
- * @property {number} [priority=0] - Startup priority (higher starts first)
45
+ * @property {number} [startupPriority=0] - Higher starts first
40
46
  */
41
47
 
42
48
  /**
@@ -78,7 +84,7 @@
78
84
  * @property {import('./ServiceManager.js').ServiceManager|null} manager - ServiceManager reference
79
85
  * @property {function(import('./ServiceManager.js').ServiceManager): void} attach - Attach to ServiceManager
80
86
  * @property {function(): void} detach - Detach from ServiceManager
81
- * @property {function(string, ServiceEntry, *): Promise<*|undefined>} [_getServiceConfig] - Optional config resolution method
87
+ * @property {function(string, ServiceEntry, *): Promise<*|undefined>} [resolveServiceConfig] - Optional config resolution method
82
88
  */
83
89
 
84
90
  // ============================================================================
@@ -91,11 +97,11 @@
91
97
  * @typedef {Object} ServiceEntry
92
98
  * @property {ServiceConstructor} ServiceClass - Service class constructor
93
99
  * @property {import('../service-base/typedef.js').ServiceInstance|null} instance - Service instance (lazy-created)
94
- * @property {*} config - Service configuration
95
- * @property {string[]} dependencies - Service dependencies
96
- * @property {Set<string>} dependents - Services that depend on this one
97
- * @property {string[]} tags - Service tags
98
- * @property {number} priority - Startup priority
100
+ * @property {ServiceConfigOrLabel} serviceConfigOrLabel
101
+ * @property {string[]} dependencies
102
+ * @property {Set<string>} dependents - Services that depend on this service
103
+ * @property {string[]} tags
104
+ * @property {number} startupPriority - Startup priority
99
105
  */
100
106
 
101
107
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"