@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
@@ -9,7 +9,7 @@
9
9
  * import { ServiceBase } from './ServiceBase.js';
10
10
  *
11
11
  * class MyService extends ServiceBase {
12
- * async _init(config) {
12
+ * async _configure(newConfig, oldConfig) {
13
13
  * }
14
14
  *
15
15
  * async _healthCheck() {
@@ -23,6 +23,24 @@
23
23
  // PUBLIC TYPES
24
24
  // ============================================================================
25
25
 
26
+ /**
27
+ * All possible service states during lifecycle management
28
+ *
29
+ * @typedef {import('./constants.js').STATE_NOT_CREATED |
30
+ * import('./constants.js').STATE_CREATED |
31
+ * import('./constants.js').STATE_CONFIGURING |
32
+ * import('./constants.js').STATE_CONFIGURED |
33
+ * import('./constants.js').STATE_STARTING |
34
+ * import('./constants.js').STATE_RUNNING |
35
+ * import('./constants.js').STATE_STOPPING |
36
+ * import('./constants.js').STATE_STOPPED |
37
+ * import('./constants.js').STATE_DESTROYING |
38
+ * import('./constants.js').STATE_DESTROYED |
39
+ * import('./constants.js').STATE_ERROR |
40
+ * import('./constants.js').STATE_RECOVERING
41
+ * } ServiceState
42
+ */
43
+
26
44
  /**
27
45
  * Options for creating a service instance
28
46
  *
@@ -57,11 +75,11 @@
57
75
  *
58
76
  * @typedef {Object & Record<string, any>} ServiceInstance
59
77
  * @property {string} name - Service name
60
- * @property {string} state - Current state
78
+ * @property {ServiceState} state - Current state
61
79
  * @property {boolean} healthy - Health status
62
80
  * @property {Error|null} error - Last error
63
81
  * @property {import('../../logging/index.js').Logger} logger - Service logger
64
- * @property {(config?: *) => Promise<boolean>} initialize
82
+ * @property {(config?: *) => Promise<boolean>} configure
65
83
  * @property {() => Promise<boolean>} start
66
84
  * @property {(options?: StopOptions) => Promise<boolean>} stop
67
85
  * @property {() => Promise<boolean>} recover
@@ -79,13 +97,18 @@
79
97
  /**
80
98
  * @typedef {Object} StateChangeEvent
81
99
  * @property {string} service - Service name (added by ServiceManager)
82
- * @property {string} oldState - Previous state
83
- * @property {string} newState - New state
100
+ * @property {ServiceState} oldState - Previous state
101
+ * @property {ServiceState} newState - New state
102
+ */
103
+
104
+ /**
105
+ * @typedef {Object} TargetStateChangeEvent
106
+ * @property {ServiceState} oldTargetState - Previous target state
107
+ * @property {ServiceState} newTargetState - New target state
84
108
  */
85
109
 
86
110
  /**
87
111
  * @typedef {Object} HealthChangeEvent
88
- * @property {string} service - Service name (added by ServiceManager)
89
112
  * @property {boolean} healthy - Current health status
90
113
  * @property {boolean} [wasHealthy] - Previous health status
91
114
  */
@@ -94,7 +117,6 @@
94
117
  * Event emitted when service encounters an error
95
118
  *
96
119
  * @typedef {Object} ServiceErrorEvent
97
- * @property {string} service - Service name
98
120
  * @property {string} operation - Operation that failed
99
121
  * @property {Error} error - Error that occurred
100
122
  */
@@ -24,6 +24,23 @@ export class ServiceManager extends EventEmitter {
24
24
  logger: Logger;
25
25
  /** @type {ServiceManagerConfig} */
26
26
  config: ServiceManagerConfig;
27
+ /**
28
+ * Attach a plugin to the ServiceManager
29
+ *
30
+ * @param {import('./typedef.js').ServiceManagerPlugin} plugin
31
+ * Plugin instance
32
+ *
33
+ * @throws {Error} If plugin name is already registered
34
+ */
35
+ attachPlugin(plugin: import("./typedef.js").ServiceManagerPlugin): void;
36
+ /**
37
+ * Detach a plugin from the ServiceManager
38
+ *
39
+ * @param {string} pluginName - Name of the plugin to detach
40
+ *
41
+ * @returns {boolean} True if plugin was detached
42
+ */
43
+ detachPlugin(pluginName: string): boolean;
27
44
  /**
28
45
  * Register a service class with the manager
29
46
  *
@@ -45,13 +62,13 @@ export class ServiceManager extends EventEmitter {
45
62
  */
46
63
  get(name: string): import("../service-base/typedef.js").ServiceInstance | null;
47
64
  /**
48
- * Initialize a service
65
+ * Configure a service
49
66
  *
50
67
  * @param {string} name - Service name
51
68
  *
52
- * @returns {Promise<boolean>} True if initialization succeeded
69
+ * @returns {Promise<boolean>} True if configuration succeeded
53
70
  */
54
- initService(name: string): Promise<boolean>;
71
+ configureService(name: string): Promise<boolean>;
55
72
  /**
56
73
  * Start a service and its dependencies
57
74
  *
@@ -67,10 +67,10 @@ import { EventEmitter } from '../../generic/events.js';
67
67
  import { Logger, DEBUG, INFO, WARN } from '../../logging/index.js';
68
68
 
69
69
  import {
70
- NOT_CREATED,
71
- CREATED,
72
- RUNNING,
73
- DESTROYED
70
+ STATE_NOT_CREATED,
71
+ STATE_CREATED,
72
+ STATE_RUNNING,
73
+ STATE_DESTROYED
74
74
  } from '../service-base/constants.js';
75
75
 
76
76
  /**
@@ -88,6 +88,9 @@ import {
88
88
  * @extends EventEmitter
89
89
  */
90
90
  export class ServiceManager extends EventEmitter {
91
+ /** @type {Map<string, import('./typedef.js').ServiceManagerPlugin>} */
92
+ #plugins = new Map();
93
+
91
94
  /**
92
95
  * Create a new ServiceManager instance
93
96
  *
@@ -113,6 +116,43 @@ export class ServiceManager extends EventEmitter {
113
116
  this.#setupLogging();
114
117
  }
115
118
 
119
+ /**
120
+ * Attach a plugin to the ServiceManager
121
+ *
122
+ * @param {import('./typedef.js').ServiceManagerPlugin} plugin
123
+ * Plugin instance
124
+ *
125
+ * @throws {Error} If plugin name is already registered
126
+ */
127
+ attachPlugin(plugin) {
128
+ if (this.#plugins.has(plugin.name)) {
129
+ throw new Error(`Plugin '${plugin.name}' is already attached`);
130
+ }
131
+
132
+ this.#plugins.set(plugin.name, plugin);
133
+ plugin.attach(this);
134
+
135
+ this.logger.debug(`Attached plugin '${plugin.name}'`);
136
+ }
137
+
138
+ /**
139
+ * Detach a plugin from the ServiceManager
140
+ *
141
+ * @param {string} pluginName - Name of the plugin to detach
142
+ *
143
+ * @returns {boolean} True if plugin was detached
144
+ */
145
+ detachPlugin(pluginName) {
146
+ const plugin = this.#plugins.get(pluginName);
147
+ if (!plugin) return false;
148
+
149
+ plugin.detach();
150
+ this.#plugins.delete(pluginName);
151
+
152
+ this.logger.debug(`Detached plugin '${pluginName}'`);
153
+ return true;
154
+ }
155
+
116
156
  /**
117
157
  * Register a service class with the manager
118
158
  *
@@ -194,18 +234,20 @@ export class ServiceManager extends EventEmitter {
194
234
  }
195
235
 
196
236
  /**
197
- * Initialize a service
237
+ * Configure a service
198
238
  *
199
239
  * @param {string} name - Service name
200
240
  *
201
- * @returns {Promise<boolean>} True if initialization succeeded
241
+ * @returns {Promise<boolean>} True if configuration succeeded
202
242
  */
203
- async initService(name) {
243
+ async configureService(name) {
204
244
  const instance = this.get(name);
205
245
  if (!instance) return false;
206
246
 
207
247
  const entry = this.services.get(name);
208
- return await instance.initialize(entry.config);
248
+ const config = await this.#resolveConfig(name, entry);
249
+
250
+ return await instance.configure(config);
209
251
  }
210
252
 
211
253
  /**
@@ -240,10 +282,18 @@ export class ServiceManager extends EventEmitter {
240
282
  const instance = this.get(name);
241
283
  if (!instance) return false;
242
284
 
243
- // Initialize if needed
244
- if (instance.state === CREATED || instance.state === DESTROYED) {
245
- const initialized = await this.initService(name);
246
- if (!initialized) return false;
285
+ if (
286
+ instance.state === STATE_CREATED ||
287
+ instance.state === STATE_DESTROYED
288
+ ) {
289
+ // Service is not created or has been destroyed
290
+ // => configure needed
291
+
292
+ const configured = await this.configureService(name);
293
+
294
+ if (!configured) {
295
+ return false;
296
+ }
247
297
  }
248
298
 
249
299
  return await instance.start();
@@ -385,7 +435,6 @@ export class ServiceManager extends EventEmitter {
385
435
  return Object.fromEntries(results);
386
436
  }
387
437
 
388
-
389
438
  /**
390
439
  * Get health status for all services
391
440
  *
@@ -401,7 +450,7 @@ export class ServiceManager extends EventEmitter {
401
450
  } else {
402
451
  health[name] = {
403
452
  name,
404
- state: NOT_CREATED,
453
+ state: STATE_NOT_CREATED,
405
454
  healthy: false
406
455
  };
407
456
  }
@@ -419,7 +468,7 @@ export class ServiceManager extends EventEmitter {
419
468
  */
420
469
  async isRunning(name) {
421
470
  const instance = this.get(name);
422
- return instance ? instance.state === RUNNING : false;
471
+ return instance ? instance.state === STATE_RUNNING : false;
423
472
  }
424
473
 
425
474
  /**
@@ -472,7 +521,6 @@ export class ServiceManager extends EventEmitter {
472
521
  return services;
473
522
  }
474
523
 
475
-
476
524
  /**
477
525
  * Attach event listeners to forward service events
478
526
  *
@@ -503,6 +551,35 @@ export class ServiceManager extends EventEmitter {
503
551
 
504
552
  // Internal methods
505
553
 
554
+ /**
555
+ * Resolve service configuration using plugins
556
+ *
557
+ * @param {string} serviceName - Name of the service being configured
558
+ * @param {ServiceEntry} serviceEntry - Service registration entry
559
+ *
560
+ * @returns {Promise<*>} Resolved configuration object
561
+ */
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
576
+ }
577
+ }
578
+ }
579
+
580
+ return config || {};
581
+ }
582
+
506
583
  /**
507
584
  * Setup logging configuration based on config.dev
508
585
  */
@@ -69,6 +69,31 @@ export type HealthCheckResult = {
69
69
  * Service class constructor type
70
70
  */
71
71
  export type ServiceConstructor = new (name: string, options?: import("../service-base/typedef.js").ServiceOptions) => import("../service-base/typedef.js").ServiceInstance;
72
+ /**
73
+ * ServiceManager plugin interface
74
+ */
75
+ export type ServiceManagerPlugin = {
76
+ /**
77
+ * - Unique plugin identifier
78
+ */
79
+ name: string;
80
+ /**
81
+ * - ServiceManager reference
82
+ */
83
+ manager: import("./ServiceManager.js").ServiceManager | null;
84
+ /**
85
+ * - Attach to ServiceManager
86
+ */
87
+ attach: (arg0: import("./ServiceManager.js").ServiceManager) => void;
88
+ /**
89
+ * - Detach from ServiceManager
90
+ */
91
+ detach: () => void;
92
+ /**
93
+ * - Optional config resolution method
94
+ */
95
+ _getServiceConfig?: (arg0: string, arg1: ServiceEntry, arg2: any) => Promise<any | undefined>;
96
+ };
72
97
  /**
73
98
  * Internal service registry entry
74
99
  */
@@ -70,6 +70,17 @@
70
70
  * @typedef {new (name: string, options?: import('../service-base/typedef.js').ServiceOptions) => import('../service-base/typedef.js').ServiceInstance} ServiceConstructor
71
71
  */
72
72
 
73
+ /**
74
+ * ServiceManager plugin interface
75
+ *
76
+ * @typedef {Object} ServiceManagerPlugin
77
+ * @property {string} name - Unique plugin identifier
78
+ * @property {import('./ServiceManager.js').ServiceManager|null} manager - ServiceManager reference
79
+ * @property {function(import('./ServiceManager.js').ServiceManager): void} attach - Attach to ServiceManager
80
+ * @property {function(): void} detach - Detach from ServiceManager
81
+ * @property {function(string, ServiceEntry, *): Promise<*|undefined>} [_getServiceConfig] - Optional config resolution method
82
+ */
83
+
73
84
  // ============================================================================
74
85
  // INTERNAL TYPES
75
86
  // ============================================================================
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Plugin that resolves service configuration from a configuration object
3
+ */
4
+ export default class ConfigPlugin {
5
+ /**
6
+ * Create a new object configuration plugin
7
+ *
8
+ * @param {Object<string, *>} configObject - Pre-parsed configuration object
9
+ */
10
+ constructor(configObject: {
11
+ [x: string]: any;
12
+ });
13
+ /** @type {string} */
14
+ name: string;
15
+ /** @type {import('../service-manager/ServiceManager.js').ServiceManager|null} */
16
+ manager: import("../service-manager/ServiceManager.js").ServiceManager | null;
17
+ /** @type {Object<string, *>} */
18
+ configObject: {
19
+ [x: string]: any;
20
+ };
21
+ /**
22
+ * Resolve service configuration from the configuration object
23
+ *
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)
28
+ *
29
+ * @returns {Promise<Object|undefined>}
30
+ * Resolved config object, or undefined to use currentConfig as-is
31
+ */
32
+ _getServiceConfig(serviceName: string, serviceEntry: import("../service-manager/typedef.js").ServiceEntry, currentConfig: any): Promise<any | undefined>;
33
+ /**
34
+ * Update the configuration object
35
+ *
36
+ * @param {Object<string, *>} newConfigObject - New configuration object
37
+ */
38
+ updateConfigObject(newConfigObject: {
39
+ [x: string]: any;
40
+ }): void;
41
+ /**
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
59
+ *
60
+ * @param {string} configLabel - Config label to update
61
+ * @param {*} newConfig - New configuration value
62
+ *
63
+ * @returns {Promise<string[]>} Array of service names that were updated
64
+ */
65
+ updateConfigLabel(configLabel: string, newConfig: any): Promise<string[]>;
66
+ /**
67
+ * Attach plugin to ServiceManager
68
+ *
69
+ * @param {import('../service-manager/ServiceManager.js').ServiceManager} manager
70
+ * ServiceManager instance
71
+ */
72
+ attach(manager: import("../service-manager/ServiceManager.js").ServiceManager): void;
73
+ /**
74
+ * Detach plugin from ServiceManager
75
+ */
76
+ detach(): void;
77
+ #private;
78
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * @fileoverview Object-based configuration plugin for ServiceManager
3
+ *
4
+ * Resolves service configuration from a pre-provided configuration object
5
+ * using config labels that map to object properties or prefixed keys.
6
+ *
7
+ * @example
8
+ * // Basic usage with config object
9
+ * import ConfigPlugin from './ConfigPlugin.js';
10
+ *
11
+ * const configObject = {
12
+ * 'database': { host: 'localhost', port: 5432 },
13
+ * 'auth': { secret: 'my-secret', algorithm: 'HS256' }
14
+ * };
15
+ *
16
+ * const objectPlugin = new ConfigPlugin(configObject);
17
+ * manager.attachPlugin(objectPlugin);
18
+ *
19
+ * @example
20
+ * // With environment variables using utility
21
+ * import { allEnv } from '../../util/sveltekit/env.js';
22
+ * import ConfigPlugin from './ConfigPlugin.js';
23
+ *
24
+ * const envConfig = await allEnv();
25
+ * const envPlugin = new ConfigPlugin(envConfig, {
26
+ * prefixMap: {
27
+ * 'database': 'DATABASE',
28
+ * 'auth': 'JWT'
29
+ * }
30
+ * });
31
+ *
32
+ * @example
33
+ * // Mixed configuration sources
34
+ * const mixedConfig = {
35
+ * ...await allEnv(),
36
+ * 'custom-service': { specialOption: true },
37
+ * 'override-service': { host: 'custom-host' }
38
+ * };
39
+ * const mixedPlugin = new ConfigPlugin(mixedConfig);
40
+ */
41
+
42
+ import { SERVICE_STATE_CHANGED } from '../service-manager/constants.js';
43
+
44
+ /**
45
+ * Plugin that resolves service configuration from a configuration object
46
+ */
47
+ export default class ConfigPlugin {
48
+
49
+ /** @type {Map<string, *>} */
50
+ #pendingConfigUpdates;
51
+
52
+ /**
53
+ * Create a new object configuration plugin
54
+ *
55
+ * @param {Object<string, *>} configObject - Pre-parsed configuration object
56
+ */
57
+ constructor(configObject) {
58
+ /** @type {string} */
59
+ this.name = 'object-config';
60
+
61
+ /** @type {import('../service-manager/ServiceManager.js').ServiceManager|null} */
62
+ this.manager = null;
63
+
64
+ /** @type {Object<string, *>} */
65
+ this.configObject = configObject || {};
66
+
67
+ this.#pendingConfigUpdates = new Map();
68
+ }
69
+
70
+ /**
71
+ * Resolve service configuration from the configuration object
72
+ *
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)
77
+ *
78
+ * @returns {Promise<Object|undefined>}
79
+ * Resolved config object, or undefined to use currentConfig as-is
80
+ */
81
+ // 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') {
85
+ return undefined;
86
+ }
87
+
88
+ const configLabel = serviceEntry.config;
89
+
90
+ // Simple object lookup
91
+ const config = this.configObject[configLabel];
92
+
93
+ if (config !== undefined) {
94
+ this.manager?.logger?.debug(
95
+ `Resolved object config for '${serviceName}' (label: ${configLabel})`,
96
+ {
97
+ configKeys:
98
+ typeof config === 'object' ? Object.keys(config) : 'primitive'
99
+ }
100
+ );
101
+ }
102
+
103
+ return config;
104
+ }
105
+
106
+ /**
107
+ * Update the configuration object
108
+ *
109
+ * @param {Object<string, *>} newConfigObject - New configuration object
110
+ */
111
+ updateConfigObject(newConfigObject) {
112
+ this.configObject = newConfigObject || {};
113
+ this.manager?.logger?.debug('Updated configuration object');
114
+ }
115
+
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
+
126
+ /**
127
+ * Get the current configuration object
128
+ *
129
+ * @returns {Object<string, *>} Current configuration object
130
+ */
131
+ getConfigObject() {
132
+ return { ...this.configObject };
133
+ }
134
+
135
+ /**
136
+ * Update config for a specific label and push to affected services
137
+ *
138
+ * @param {string} configLabel - Config label to update
139
+ * @param {*} newConfig - New configuration value
140
+ *
141
+ * @returns {Promise<string[]>} Array of service names that were updated
142
+ */
143
+ async updateConfigLabel(configLabel, newConfig) {
144
+ // Update the config object
145
+ this.configObject[configLabel] = newConfig;
146
+
147
+ // Store as pending update
148
+ this.#pendingConfigUpdates.set(configLabel, newConfig);
149
+
150
+ const updatedServices = [];
151
+
152
+ // Find all services using this config label
153
+ for (const [serviceName, serviceEntry] of this.manager.services) {
154
+ if (serviceEntry.config === configLabel && serviceEntry.instance) {
155
+ try {
156
+ // Try to apply config - ServiceBase.configure() will handle state validation
157
+ await this.#applyConfigToService(serviceName, serviceEntry, newConfig);
158
+ updatedServices.push(serviceName);
159
+
160
+ // Remove from pending since it was applied
161
+ this.#pendingConfigUpdates.delete(configLabel);
162
+ } catch (error) {
163
+ // If configure() fails due to invalid state, it will be retried later
164
+ this.manager.logger.debug(
165
+ `Could not update config for service '${serviceName}' (${error.message}), will retry when service state allows`
166
+ );
167
+ }
168
+ }
169
+ }
170
+
171
+ this.manager.logger.debug(
172
+ `Config label '${configLabel}' updated, applied to ${updatedServices.length} services immediately`
173
+ );
174
+
175
+ return updatedServices;
176
+ }
177
+
178
+
179
+ /**
180
+ * Apply configuration to a specific service
181
+ *
182
+ * @param {string} serviceName - Name of the service
183
+ * @param {import('../service-manager/typedef.js').ServiceEntry} serviceEntry
184
+ * Service entry from manager
185
+ * @param {*} newConfig - New configuration to apply
186
+ */
187
+ async #applyConfigToService(serviceName, serviceEntry, newConfig) {
188
+ await serviceEntry.instance.configure(newConfig);
189
+
190
+ this.manager.logger.info(
191
+ `Updated config for service '${serviceName}'`
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Process pending config updates for services that can now be configured
197
+ *
198
+ * @param {string} serviceName - Name of the service that changed state
199
+ * @param {import('../service-base/ServiceBase.js').default} serviceInstance
200
+ * Service instance
201
+ */
202
+ async #processPendingUpdates(serviceName, serviceInstance) {
203
+ const serviceEntry = this.manager.services.get(serviceName);
204
+ if (!serviceEntry || typeof serviceEntry.config !== 'string') {
205
+ return;
206
+ }
207
+
208
+ const configLabel = serviceEntry.config;
209
+ if (this.#pendingConfigUpdates.has(configLabel)) {
210
+ const pendingConfig = this.#pendingConfigUpdates.get(configLabel);
211
+
212
+ try {
213
+ await this.#applyConfigToService(serviceName, serviceEntry, pendingConfig);
214
+ this.#pendingConfigUpdates.delete(configLabel);
215
+
216
+ this.manager.logger.info(
217
+ `Applied pending config update for service '${serviceName}' (label: ${configLabel})`
218
+ );
219
+ } catch (error) {
220
+ this.manager.logger.debug(
221
+ `Could not apply pending config for service '${serviceName}': ${error.message}`
222
+ );
223
+ }
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Attach plugin to ServiceManager
229
+ *
230
+ * @param {import('../service-manager/ServiceManager.js').ServiceManager} manager
231
+ * ServiceManager instance
232
+ */
233
+ attach(manager) {
234
+ if (this.manager) {
235
+ throw new Error(
236
+ `Plugin '${this.name}' is already attached to a ServiceManager`
237
+ );
238
+ }
239
+
240
+ this.manager = manager;
241
+
242
+ const configKeys = Object.keys(this.configObject).length;
243
+
244
+ this.manager.logger.info(
245
+ `ConfigPlugin attached with ${configKeys} config keys`
246
+ );
247
+
248
+ // 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
+ });
252
+ }
253
+
254
+ /**
255
+ * Detach plugin from ServiceManager
256
+ */
257
+ detach() {
258
+ if (this.manager) {
259
+ // Clear pending updates
260
+ this.#pendingConfigUpdates.clear();
261
+
262
+ this.manager.logger.info('ConfigPlugin detached');
263
+ this.manager = null;
264
+ }
265
+ }
266
+ }