@hkdigital/lib-core 0.5.92 → 0.5.93
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.
- package/README.md +63 -9
- package/dist/browser/info/device.js +9 -7
- package/dist/logging/README.md +15 -53
- package/dist/services/PATTERNS.md +476 -0
- package/dist/services/PLUGINS.md +520 -0
- package/dist/services/README.md +156 -229
- package/package.json +1 -1
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# Service Manager Plugins
|
|
2
|
+
|
|
3
|
+
Plugin system for extending ServiceManager functionality, primarily for
|
|
4
|
+
dynamic configuration resolution.
|
|
5
|
+
|
|
6
|
+
**See also:**
|
|
7
|
+
- **API Reference**: [README.md](./README.md) - ServiceBase and
|
|
8
|
+
ServiceManager API
|
|
9
|
+
- **Patterns**: [PATTERNS.md](./PATTERNS.md) - Service implementation
|
|
10
|
+
patterns
|
|
11
|
+
- **Architecture**: [docs/setup/services-logging.md](../../docs/setup/services-logging.md)
|
|
12
|
+
- Integration examples
|
|
13
|
+
|
|
14
|
+
## Plugin System Overview
|
|
15
|
+
|
|
16
|
+
ServiceManager supports plugins to extend its functionality. Plugins
|
|
17
|
+
can:
|
|
18
|
+
|
|
19
|
+
- Resolve service configurations dynamically
|
|
20
|
+
- Transform configuration before services receive it
|
|
21
|
+
- React to service lifecycle events
|
|
22
|
+
- Add custom validation logic
|
|
23
|
+
|
|
24
|
+
## ConfigPlugin
|
|
25
|
+
|
|
26
|
+
The most common plugin for resolving service configuration from a
|
|
27
|
+
pre-parsed configuration object. Perfect for environment variables,
|
|
28
|
+
config files, or any structured configuration source.
|
|
29
|
+
|
|
30
|
+
### Basic Usage with Environment Variables
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import { ServiceManager } from '$lib/services/index.js';
|
|
34
|
+
|
|
35
|
+
import ConfigPlugin from '$lib/services/manager-plugins/ConfigPlugin.js';
|
|
36
|
+
|
|
37
|
+
import { getPrivateEnv } from '$lib/util/sveltekit/env-private.js';
|
|
38
|
+
|
|
39
|
+
// Load and auto-group environment variables
|
|
40
|
+
const envConfig = getPrivateEnv();
|
|
41
|
+
//
|
|
42
|
+
// Example:
|
|
43
|
+
//
|
|
44
|
+
// DATABASE_HOST=localhost
|
|
45
|
+
// DATABASE_PORT=5432
|
|
46
|
+
// DATABASE_NAME=myapp
|
|
47
|
+
// REDIS_HOST=cache-server
|
|
48
|
+
// REDIS_PORT=6379
|
|
49
|
+
// JWT_SECRET=mysecret
|
|
50
|
+
// =>
|
|
51
|
+
// {
|
|
52
|
+
// database: { host: 'localhost', port: 5432, name: 'myapp' },
|
|
53
|
+
// redis: { host: 'cache-server', port: 6379 },
|
|
54
|
+
// jwtSecret: 'mysecret'
|
|
55
|
+
// }
|
|
56
|
+
//
|
|
57
|
+
|
|
58
|
+
// Create plugin with grouped config
|
|
59
|
+
const configPlugin = new ConfigPlugin(envConfig);
|
|
60
|
+
|
|
61
|
+
// Attach to ServiceManager
|
|
62
|
+
const manager = new ServiceManager();
|
|
63
|
+
manager.attachPlugin(configPlugin);
|
|
64
|
+
|
|
65
|
+
// Register services with config labels (not config objects)
|
|
66
|
+
manager.register('database', DatabaseService, 'database'); // Uses envConfig.database
|
|
67
|
+
manager.register('cache', RedisService, 'redis'); // Uses envConfig.redis
|
|
68
|
+
|
|
69
|
+
await manager.startAll();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Key concept:**
|
|
73
|
+
Instead of passing configuration objects directly, pass config **labels**
|
|
74
|
+
(strings) that the plugin resolves to actual configuration.
|
|
75
|
+
|
|
76
|
+
### Configuration Sources
|
|
77
|
+
|
|
78
|
+
The plugin constructor accepts an object with configuration data, which
|
|
79
|
+
can come from any source.
|
|
80
|
+
|
|
81
|
+
**Environment variables:**
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
import { getPrivateEnv } from '$lib/util/sveltekit/env-private.js';
|
|
85
|
+
|
|
86
|
+
const envConfig = getPrivateEnv();
|
|
87
|
+
const plugin = new ConfigPlugin(envConfig);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Config files:**
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
import configData from './config.json' with { type: 'json' };
|
|
94
|
+
|
|
95
|
+
const plugin = new ConfigPlugin(configData);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Combine multiple sources:**
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
// Combine multiple config sources
|
|
102
|
+
const config = {
|
|
103
|
+
...getPrivateEnv(), // Environment variables
|
|
104
|
+
...(await loadConfigFile()), // Config file
|
|
105
|
+
database: {
|
|
106
|
+
// Override specific settings
|
|
107
|
+
...envConfig.database,
|
|
108
|
+
connectionTimeout: 5000
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const plugin = new ConfigPlugin(config);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Methods
|
|
116
|
+
|
|
117
|
+
#### replaceAllConfigs()
|
|
118
|
+
|
|
119
|
+
Replace all configurations and clean up unused ones.
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
// Update entire configuration set
|
|
123
|
+
await configPlugin.replaceAllConfigs(newConfig);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Use case:**
|
|
127
|
+
When you need to reload configuration from a file or external source.
|
|
128
|
+
|
|
129
|
+
#### replaceConfig()
|
|
130
|
+
|
|
131
|
+
Replace configuration for a specific label.
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
// Update single service configuration
|
|
135
|
+
await configPlugin.replaceConfig('database', newDatabaseConfig);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Use case:**
|
|
139
|
+
When you need to update a specific service's configuration without
|
|
140
|
+
affecting others.
|
|
141
|
+
|
|
142
|
+
#### cleanupConfigs()
|
|
143
|
+
|
|
144
|
+
Clean up configurations not used by any service.
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
// Remove unused configurations
|
|
148
|
+
await configPlugin.cleanupConfigs();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Use case:**
|
|
152
|
+
Memory optimization when configuration object is large and contains
|
|
153
|
+
many unused entries.
|
|
154
|
+
|
|
155
|
+
## Live Configuration Updates
|
|
156
|
+
|
|
157
|
+
The ConfigPlugin supports pushing configuration updates to running
|
|
158
|
+
services without restart.
|
|
159
|
+
|
|
160
|
+
### Basic Live Update
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
// Replace config for a specific label and notify all affected services
|
|
164
|
+
const updatedServices = await configPlugin.replaceConfig('database', {
|
|
165
|
+
host: 'new-host.example.com',
|
|
166
|
+
port: 5433,
|
|
167
|
+
maxConnections: 50
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Returns array of service names that were updated
|
|
171
|
+
console.log(`Updated ${updatedServices.length} services`);
|
|
172
|
+
// => "Updated 2 services" (e.g., ['user-service', 'order-service'])
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**What happens:**
|
|
176
|
+
1. Plugin updates the stored configuration for the label
|
|
177
|
+
2. Plugin finds all services using that config label
|
|
178
|
+
3. Each service's `configure()` method is called with new config
|
|
179
|
+
4. Services intelligently apply changes (see patterns below)
|
|
180
|
+
|
|
181
|
+
### Service Requirements for Live Updates
|
|
182
|
+
|
|
183
|
+
For services to support live configuration updates, they must:
|
|
184
|
+
|
|
185
|
+
1. **Implement intelligent `_configure()` logic** that handles both
|
|
186
|
+
initial setup and reconfiguration
|
|
187
|
+
2. **Check for meaningful changes** between old and new config
|
|
188
|
+
3. **Apply changes without full restart** when possible
|
|
189
|
+
|
|
190
|
+
**Example implementation:**
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
class DatabaseService extends ServiceBase {
|
|
194
|
+
// eslint-disable-next-line no-unused-vars
|
|
195
|
+
async _configure(newConfig, oldConfig = null) {
|
|
196
|
+
if (!oldConfig) {
|
|
197
|
+
// Initial configuration
|
|
198
|
+
this.connectionString = newConfig.connectionString;
|
|
199
|
+
this.maxConnections = newConfig.maxConnections || 10;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Live reconfiguration - handle changes intelligently
|
|
204
|
+
if (oldConfig.connectionString !== newConfig.connectionString) {
|
|
205
|
+
// Connection changed - need to reconnect
|
|
206
|
+
await this.connection?.close();
|
|
207
|
+
this.connectionString = newConfig.connectionString;
|
|
208
|
+
|
|
209
|
+
if (this.state === 'running') {
|
|
210
|
+
this.connection = await createConnection(this.connectionString);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (oldConfig.maxConnections !== newConfig.maxConnections) {
|
|
215
|
+
// Pool size changed - update without reconnect
|
|
216
|
+
this.maxConnections = newConfig.maxConnections;
|
|
217
|
+
await this.connection?.setMaxConnections(this.maxConnections);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Live Update Patterns
|
|
224
|
+
|
|
225
|
+
**Hot-reload safe properties:**
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
async _configure(newConfig, oldConfig = null) {
|
|
229
|
+
// These can be updated without restart
|
|
230
|
+
this.timeout = newConfig.timeout || 5000;
|
|
231
|
+
this.retryAttempts = newConfig.retryAttempts || 3;
|
|
232
|
+
this.logLevel = newConfig.logLevel || 'INFO';
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Properties requiring reconnect:**
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
async _configure(newConfig, oldConfig = null) {
|
|
240
|
+
if (!oldConfig ||
|
|
241
|
+
oldConfig.host !== newConfig.host ||
|
|
242
|
+
oldConfig.port !== newConfig.port) {
|
|
243
|
+
|
|
244
|
+
// Close existing connection
|
|
245
|
+
await this.connection?.close();
|
|
246
|
+
|
|
247
|
+
// Update config
|
|
248
|
+
this.host = newConfig.host;
|
|
249
|
+
this.port = newConfig.port;
|
|
250
|
+
|
|
251
|
+
// Reconnect if running
|
|
252
|
+
if (this.state === 'running') {
|
|
253
|
+
this.connection = await this.#connect();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**No-op for unchanged config:**
|
|
260
|
+
|
|
261
|
+
```javascript
|
|
262
|
+
async _configure(newConfig, oldConfig = null) {
|
|
263
|
+
if (oldConfig &&
|
|
264
|
+
oldConfig.apiKey === newConfig.apiKey &&
|
|
265
|
+
oldConfig.endpoint === newConfig.endpoint) {
|
|
266
|
+
// Nothing changed, skip reconfiguration
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Apply changes
|
|
271
|
+
this.apiKey = newConfig.apiKey;
|
|
272
|
+
this.endpoint = newConfig.endpoint;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Advanced Usage
|
|
277
|
+
|
|
278
|
+
### Multiple Services Sharing Configuration
|
|
279
|
+
|
|
280
|
+
Multiple services can share the same configuration label.
|
|
281
|
+
|
|
282
|
+
```javascript
|
|
283
|
+
const config = {
|
|
284
|
+
database: {
|
|
285
|
+
host: 'localhost',
|
|
286
|
+
port: 5432,
|
|
287
|
+
name: 'myapp'
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const plugin = new ConfigPlugin(config);
|
|
292
|
+
manager.attachPlugin(plugin);
|
|
293
|
+
|
|
294
|
+
// Both services use the same config
|
|
295
|
+
manager.register('user-db', UserDatabaseService, 'database');
|
|
296
|
+
manager.register('order-db', OrderDatabaseService, 'database');
|
|
297
|
+
|
|
298
|
+
// Update affects both services
|
|
299
|
+
await plugin.replaceConfig('database', {
|
|
300
|
+
host: 'new-host',
|
|
301
|
+
port: 5432,
|
|
302
|
+
name: 'myapp'
|
|
303
|
+
});
|
|
304
|
+
// Both user-db and order-db reconnect to new host
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Configuration with Defaults
|
|
308
|
+
|
|
309
|
+
Provide base configuration and let services apply defaults.
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
const config = {
|
|
313
|
+
database: {
|
|
314
|
+
host: 'localhost',
|
|
315
|
+
port: 5432
|
|
316
|
+
// maxConnections not specified
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const plugin = new ConfigPlugin(config);
|
|
321
|
+
manager.attachPlugin(plugin);
|
|
322
|
+
|
|
323
|
+
manager.register('database', DatabaseService, 'database');
|
|
324
|
+
|
|
325
|
+
// Service applies defaults in _configure()
|
|
326
|
+
class DatabaseService extends ServiceBase {
|
|
327
|
+
async _configure(newConfig, oldConfig = null) {
|
|
328
|
+
this.host = newConfig.host;
|
|
329
|
+
this.port = newConfig.port;
|
|
330
|
+
this.maxConnections = newConfig.maxConnections || 10; // Default
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Environment-Specific Configuration
|
|
336
|
+
|
|
337
|
+
Use environment variables to switch configurations.
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
import { getPrivateEnv } from '$lib/util/sveltekit/env-private.js';
|
|
341
|
+
|
|
342
|
+
const env = getPrivateEnv();
|
|
343
|
+
|
|
344
|
+
const config = {
|
|
345
|
+
database: {
|
|
346
|
+
host: env.DATABASE_HOST || 'localhost',
|
|
347
|
+
port: env.DATABASE_PORT || 5432,
|
|
348
|
+
name: env.DATABASE_NAME || 'myapp',
|
|
349
|
+
ssl: env.NODE_ENV === 'production'
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const plugin = new ConfigPlugin(config);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Error Handling
|
|
357
|
+
|
|
358
|
+
### Invalid Configuration Labels
|
|
359
|
+
|
|
360
|
+
When a service requests a non-existent config label:
|
|
361
|
+
|
|
362
|
+
```javascript
|
|
363
|
+
manager.register('cache', CacheService, 'redis'); // 'redis' not in config
|
|
364
|
+
|
|
365
|
+
// Plugin will pass through the label as-is to the service
|
|
366
|
+
// Service's _configure() receives 'redis' string instead of object
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Best practice:**
|
|
370
|
+
Validate configuration in service's `_configure()`:
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
async _configure(newConfig, oldConfig = null) {
|
|
374
|
+
if (typeof newConfig === 'string') {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Configuration label '${newConfig}' not found in ConfigPlugin`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Continue with valid config object
|
|
381
|
+
this.host = newConfig.host;
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Configuration Validation
|
|
386
|
+
|
|
387
|
+
Validate configuration before applying:
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
async _configure(newConfig, oldConfig = null) {
|
|
391
|
+
// Validate required fields
|
|
392
|
+
if (!newConfig.host || !newConfig.port) {
|
|
393
|
+
throw new Error('Database config requires host and port');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Validate types
|
|
397
|
+
if (typeof newConfig.port !== 'number') {
|
|
398
|
+
throw new Error('Database port must be a number');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Validate ranges
|
|
402
|
+
if (newConfig.port < 1 || newConfig.port > 65535) {
|
|
403
|
+
throw new Error('Database port must be between 1 and 65535');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Apply validated config
|
|
407
|
+
this.host = newConfig.host;
|
|
408
|
+
this.port = newConfig.port;
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Best Practices
|
|
413
|
+
|
|
414
|
+
1. **Use config labels consistently** - Match label names to service
|
|
415
|
+
names when possible
|
|
416
|
+
2. **Group related config** - Use prefixes (DATABASE_*, REDIS_*) for
|
|
417
|
+
auto-grouping
|
|
418
|
+
3. **Validate config in services** - Don't assume ConfigPlugin has all
|
|
419
|
+
labels
|
|
420
|
+
4. **Implement intelligent reconfiguration** - Check what changed
|
|
421
|
+
before taking action
|
|
422
|
+
5. **Test live updates** - Ensure services handle config changes
|
|
423
|
+
gracefully
|
|
424
|
+
6. **Provide sensible defaults** - Make services work with minimal
|
|
425
|
+
config
|
|
426
|
+
7. **Document required fields** - Make it clear what each service needs
|
|
427
|
+
8. **Use environment variables** - Keep secrets out of code
|
|
428
|
+
|
|
429
|
+
## Creating Custom Plugins
|
|
430
|
+
|
|
431
|
+
Custom plugins can extend ServiceManager functionality. Plugin
|
|
432
|
+
interface:
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
class CustomPlugin {
|
|
436
|
+
constructor(options) {
|
|
437
|
+
this.options = options;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Called when plugin is attached to ServiceManager
|
|
442
|
+
*
|
|
443
|
+
* @param {ServiceManager} manager
|
|
444
|
+
*/
|
|
445
|
+
attach(manager) {
|
|
446
|
+
this.manager = manager;
|
|
447
|
+
// Setup listeners, initialization, etc.
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Called when ServiceManager resolves config for a service
|
|
452
|
+
*
|
|
453
|
+
* @param {string} serviceName
|
|
454
|
+
* @param {*} config
|
|
455
|
+
*
|
|
456
|
+
* @returns {*} Resolved config
|
|
457
|
+
*/
|
|
458
|
+
resolveConfig(serviceName, config) {
|
|
459
|
+
// Transform or resolve config
|
|
460
|
+
return config;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Called when plugin is detached
|
|
465
|
+
*/
|
|
466
|
+
detach() {
|
|
467
|
+
// Cleanup
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Example - Validation Plugin:**
|
|
473
|
+
|
|
474
|
+
```javascript
|
|
475
|
+
class ValidationPlugin {
|
|
476
|
+
constructor(schemas) {
|
|
477
|
+
this.schemas = schemas; // Map of service name -> validation schema
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
attach(manager) {
|
|
481
|
+
this.manager = manager;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
resolveConfig(serviceName, config) {
|
|
485
|
+
const schema = this.schemas[serviceName];
|
|
486
|
+
|
|
487
|
+
if (schema) {
|
|
488
|
+
const result = schema.safeParse(config);
|
|
489
|
+
|
|
490
|
+
if (!result.success) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Invalid config for ${serviceName}: ${result.error.message}`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return result.data;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return config;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
detach() {
|
|
503
|
+
this.manager = null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Usage
|
|
508
|
+
import { v } from '$lib/valibot/valibot.js';
|
|
509
|
+
|
|
510
|
+
const schemas = {
|
|
511
|
+
database: v.object({
|
|
512
|
+
host: v.string(),
|
|
513
|
+
port: v.number(),
|
|
514
|
+
name: v.string()
|
|
515
|
+
})
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const plugin = new ValidationPlugin(schemas);
|
|
519
|
+
manager.attachPlugin(plugin);
|
|
520
|
+
```
|