@hkdigital/lib-core 0.4.18 → 0.4.20

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 (33) hide show
  1. package/dist/services/README.md +254 -47
  2. package/dist/services/manager-plugins/ConfigPlugin.d.ts +68 -0
  3. package/dist/services/manager-plugins/ConfigPlugin.js +329 -0
  4. package/dist/services/service-base/ServiceBase.d.ts +59 -22
  5. package/dist/services/service-base/ServiceBase.js +139 -61
  6. package/dist/services/service-base/constants.d.ts +30 -14
  7. package/dist/services/service-base/constants.js +42 -14
  8. package/dist/services/service-base/typedef.d.ts +15 -9
  9. package/dist/services/service-base/typedef.js +33 -11
  10. package/dist/services/service-manager/ServiceManager.d.ts +25 -5
  11. package/dist/services/service-manager/ServiceManager.js +103 -20
  12. package/dist/services/service-manager/constants.js +0 -1
  13. package/dist/services/service-manager/typedef.d.ts +36 -14
  14. package/dist/services/service-manager/typedef.js +30 -13
  15. package/dist/util/sveltekit/env/README.md +424 -0
  16. package/dist/util/sveltekit/env/all.d.ts +54 -0
  17. package/dist/util/sveltekit/env/all.js +97 -0
  18. package/dist/util/sveltekit/env/parsers.d.ts +135 -0
  19. package/dist/util/sveltekit/env/parsers.js +257 -0
  20. package/dist/util/sveltekit/env/private.d.ts +56 -0
  21. package/dist/util/sveltekit/env/private.js +87 -0
  22. package/dist/util/sveltekit/env/public.d.ts +52 -0
  23. package/dist/util/sveltekit/env/public.js +82 -0
  24. package/dist/util/sveltekit/env-all.d.ts +1 -0
  25. package/dist/util/sveltekit/env-all.js +19 -0
  26. package/dist/util/sveltekit/env-private.d.ts +1 -0
  27. package/dist/util/sveltekit/env-private.js +18 -0
  28. package/dist/util/sveltekit/env-public.d.ts +1 -0
  29. package/dist/util/sveltekit/env-public.js +18 -0
  30. package/package.json +1 -1
  31. package/dist/util/index.js__ +0 -20
  32. package/dist/util/sveltekit/index.d.ts +0 -0
  33. package/dist/util/sveltekit/index.js +0 -0
@@ -0,0 +1,329 @@
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 allConfigs = {
12
+ * 'database': { host: 'localhost', port: 5432 },
13
+ * 'auth': { secret: 'my-secret', algorithm: 'HS256' }
14
+ * };
15
+ *
16
+ * const objectPlugin = new ConfigPlugin(allConfigs);
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
+ /** @typedef {import('../service-manager/typedef.js').ServiceEntry} ServiceEntry */
45
+
46
+ /**
47
+ * Plugin that resolves service configuration from a configuration object
48
+ */
49
+ export default class ConfigPlugin {
50
+ /** @type {Map<string, *>} */
51
+ #pendingConfigUpdates;
52
+
53
+ /**
54
+ * Create a new object configuration plugin
55
+ *
56
+ * @param {Object<string, *>} allConfigs - Pre-parsed configuration object
57
+ */
58
+ constructor(allConfigs) {
59
+ /** @type {string} */
60
+ this.name = 'object-config';
61
+
62
+ /** @type {import('../service-manager/ServiceManager.js').ServiceManager|null} */
63
+ this.manager = null;
64
+
65
+ /** @type {Object<string, *>} */
66
+ this.allConfigs = allConfigs || {};
67
+
68
+ this.#pendingConfigUpdates = new Map();
69
+ }
70
+
71
+ /**
72
+ * Resolve service configuration from the configuration object
73
+ *
74
+ * @param {string} serviceName
75
+ * @param {ServiceEntry} serviceEntry - Service registration entry
76
+ * @param {*} currentConfig
77
+ * Current config (could be object from previous plugins)
78
+ *
79
+ * @returns {Promise<Object|undefined>}
80
+ * Resolved config object, or undefined to use currentConfig as-is
81
+ */
82
+ // eslint-disable-next-line no-unused-vars
83
+ async resolveServiceConfig(serviceName, serviceEntry, currentConfig) {
84
+ const configLabel = serviceEntry.serviceConfigOrLabel;
85
+
86
+ if (typeof configLabel !== 'string') {
87
+ // Expected config label
88
+ return undefined;
89
+ }
90
+
91
+ // Simple object lookup
92
+ const config = this.allConfigs[configLabel];
93
+
94
+ if (config !== undefined) {
95
+ this.manager?.logger?.debug(
96
+ `Resolved object config for '${serviceName}' (label: ${configLabel})`,
97
+ {
98
+ configKeys:
99
+ typeof config === 'object' ? Object.keys(config) : 'primitive'
100
+ }
101
+ );
102
+ }
103
+
104
+ return config;
105
+ }
106
+
107
+ /**
108
+ * Replace the entire configuration object and clean up unused configs
109
+ *
110
+ * @param {Object<string, *>} newConfigObject - New configuration object
111
+ */
112
+ async replaceAllConfigs(newConfigObject) {
113
+ await this.cleanupConfigs();
114
+
115
+ // Apply new configs to all services that have config labels
116
+ const updatedServices = [];
117
+ for (const [configLabel, newConfig] of Object.entries(
118
+ newConfigObject || {}
119
+ )) {
120
+ const services = await this.replaceConfig(configLabel, newConfig);
121
+ updatedServices.push(...services);
122
+ }
123
+
124
+ this.manager?.logger?.debug(
125
+ `Replaced all configurations, updated ${updatedServices.length} services`
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Replace a config for a specific label and push to affected services
131
+ *
132
+ * @param {string} configLabel - Config label to update
133
+ * @param {*} newConfig - New configuration value
134
+ *
135
+ * @returns {Promise<string[]>} Array of service names that were updated
136
+ */
137
+ async replaceConfig(configLabel, newConfig) {
138
+ // Update the config object
139
+ this.allConfigs[configLabel] = newConfig;
140
+
141
+ // Store as pending update
142
+ this.#pendingConfigUpdates.set(configLabel, newConfig);
143
+
144
+ const updatedServices = [];
145
+
146
+ // Find all services using this config label using helper function
147
+ const servicesByLabel = this.#servicesByConfigLabel();
148
+ const serviceNames = servicesByLabel.get(configLabel) || [];
149
+
150
+ for (const serviceName of serviceNames) {
151
+ const serviceEntry = this.manager.services.get(serviceName);
152
+ if (serviceEntry && serviceEntry.instance) {
153
+ try {
154
+ // Try to apply config - ServiceBase.configure() will handle state validation
155
+ await this.#applyConfigToService(
156
+ serviceName,
157
+ serviceEntry,
158
+ newConfig
159
+ );
160
+ updatedServices.push(serviceName);
161
+
162
+ // Remove from pending since it was applied
163
+ this.#pendingConfigUpdates.delete(configLabel);
164
+ } catch (error) {
165
+ // If configure() fails due to invalid state, it will be retried later
166
+ this.manager.logger.debug(
167
+ `Could not update config for service '${serviceName}' (${error.message}), will retry when service state allows`
168
+ );
169
+ }
170
+ }
171
+ }
172
+
173
+ this.manager.logger.debug(
174
+ `Config label '${configLabel}' updated, applied to ${updatedServices.length} services immediately`
175
+ );
176
+
177
+ return updatedServices;
178
+ }
179
+
180
+ /**
181
+ * Get services organized by their config labels
182
+ *
183
+ * @returns {Map<string, string[]>}
184
+ * Map where keys are config labels and values are arrays of service names
185
+ */
186
+ #servicesByConfigLabel() {
187
+ const servicesByLabel = new Map();
188
+
189
+ for (const [serviceName, serviceEntry] of this.manager.services) {
190
+ const configLabel = serviceEntry.serviceConfigOrLabel;
191
+
192
+ if (typeof configLabel === 'string') {
193
+ if (!servicesByLabel.has(configLabel)) {
194
+ servicesByLabel.set(configLabel, []);
195
+ }
196
+
197
+ servicesByLabel.get(configLabel).push(serviceName);
198
+ }
199
+ }
200
+
201
+ return servicesByLabel;
202
+ }
203
+
204
+ /**
205
+ * Remove all unused configurations (configs not referenced by any service)
206
+ */
207
+ async cleanupConfigs() {
208
+ const usedConfigLabels = new Set();
209
+
210
+ // Collect all config labels used by registered services
211
+ for (const [, serviceEntry] of this.manager.services) {
212
+ const configLabel = serviceEntry.serviceConfigOrLabel;
213
+
214
+ if (typeof configLabel === 'string') {
215
+ usedConfigLabels.add(configLabel);
216
+ }
217
+ }
218
+
219
+ // Remove configs that aren't used by any service
220
+ const configKeys = Object.keys(this.allConfigs);
221
+ let removedCount = 0;
222
+
223
+ for (const key of configKeys) {
224
+ if (!usedConfigLabels.has(key)) {
225
+ delete this.allConfigs[key];
226
+ removedCount++;
227
+ }
228
+ }
229
+
230
+ this.manager?.logger?.debug(
231
+ `Cleaned up ${removedCount} unused configurations`
232
+ );
233
+ }
234
+
235
+ /**
236
+ * Apply configuration to a specific service
237
+ *
238
+ * @param {string} serviceName - Name of the service
239
+ * @param {import('../service-manager/typedef.js').ServiceEntry} serviceEntry
240
+ * Service entry from manager
241
+ * @param {*} newConfig - New configuration to apply
242
+ */
243
+ async #applyConfigToService(serviceName, serviceEntry, newConfig) {
244
+ await serviceEntry.instance.configure(newConfig);
245
+
246
+ this.manager.logger.info(`Updated config for service '${serviceName}'`);
247
+ }
248
+
249
+ /**
250
+ * Process pending config updates for services that can now be configured
251
+ *
252
+ * @param {string} serviceName - Name of the service that changed state
253
+ * @param {import('../service-base/ServiceBase.js').default} serviceInstance
254
+ * Service instance
255
+ */
256
+ async #processPendingUpdates(serviceName, serviceInstance) {
257
+ const serviceEntry = this.manager.services.get(serviceName);
258
+
259
+ const configLabel = serviceEntry?.serviceConfigOrLabel;
260
+
261
+ if (typeof configLabel !== 'string') {
262
+ return;
263
+ }
264
+
265
+ if (this.#pendingConfigUpdates.has(configLabel)) {
266
+ const pendingConfig = this.#pendingConfigUpdates.get(configLabel);
267
+
268
+ try {
269
+ await this.#applyConfigToService(
270
+ serviceName,
271
+ serviceEntry,
272
+ pendingConfig
273
+ );
274
+ this.#pendingConfigUpdates.delete(configLabel);
275
+
276
+ this.manager.logger.info(
277
+ `Applied pending config update for service '${serviceName}' (label: ${configLabel})`
278
+ );
279
+ } catch (error) {
280
+ this.manager.logger.debug(
281
+ `Could not apply pending config for service '${serviceName}': ${error.message}`
282
+ );
283
+ }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Attach plugin to ServiceManager
289
+ *
290
+ * @param {import('../service-manager/ServiceManager.js').ServiceManager} manager
291
+ * ServiceManager instance
292
+ */
293
+ attach(manager) {
294
+ if (this.manager) {
295
+ throw new Error(
296
+ `Plugin '${this.name}' is already attached to a ServiceManager`
297
+ );
298
+ }
299
+
300
+ this.manager = manager;
301
+
302
+ const configKeys = Object.keys(this.allConfigs).length;
303
+
304
+ this.manager.logger.info(
305
+ `ConfigPlugin attached with ${configKeys} config keys`
306
+ );
307
+
308
+ // Listen for service state changes to process pending updates
309
+ this.manager.on(
310
+ SERVICE_STATE_CHANGED,
311
+ async ({ service, state, instance }) => {
312
+ await this.#processPendingUpdates(service, instance);
313
+ }
314
+ );
315
+ }
316
+
317
+ /**
318
+ * Detach plugin from ServiceManager
319
+ */
320
+ detach() {
321
+ if (this.manager) {
322
+ // Clear pending updates
323
+ this.#pendingConfigUpdates.clear();
324
+
325
+ this.manager.logger.info('ConfigPlugin detached');
326
+ this.manager = null;
327
+ }
328
+ }
329
+ }
@@ -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';