@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.
@@ -1,19 +1,37 @@
1
1
  # Services
2
2
 
3
- A comprehensive service management system providing standardized lifecycle management, health monitoring, and dependency orchestration for application services.
3
+ A comprehensive service management system providing standardized
4
+ lifecycle management, health monitoring, and dependency orchestration
5
+ for application services.
6
+
7
+ **See also:**
8
+ - **Architecture**: [docs/setup/services-logging.md](../../docs/setup/services-logging.md)
9
+ - How services and logging work together
10
+ - **Patterns**: [PATTERNS.md](./PATTERNS.md) - Service access patterns
11
+ and best practices
12
+ - **Plugins**: [PLUGINS.md](./PLUGINS.md) - ConfigPlugin and plugin
13
+ system
14
+ - **Logging**: [src/lib/logging/README.md](../logging/README.md) -
15
+ Logging system integration
16
+ - **Main README**: [README.md](../../README.md) - Library overview and
17
+ setup
4
18
 
5
19
  ## Overview
6
20
 
7
21
  The services module provides two main components:
8
22
 
9
- - **ServiceBase** - Base class for implementing services with lifecycle management
10
- - **ServiceManager** - Orchestrates multiple services with dependency resolution
23
+ - **ServiceBase** - Base class for implementing services with lifecycle
24
+ management
25
+ - **ServiceManager** - Orchestrates multiple services with dependency
26
+ resolution
11
27
 
12
- All services follow a standardized state machine with proper error handling, logging, and health monitoring.
28
+ All services follow a standardized state machine with proper error
29
+ handling, logging, and health monitoring.
13
30
 
14
- ## Service states
31
+ ## Service States
15
32
 
16
- Services transition through these states during their lifecycle. Use these constants from `$lib/services/service-base/constants.js`:
33
+ Services transition through these states during their lifecycle. Use
34
+ these constants from `$lib/services/service-base/constants.js`:
17
35
 
18
36
  - `STATE_CREATED` - Service instantiated but not configured
19
37
  - `STATE_CONFIGURING` - Currently running configuration
@@ -42,7 +60,8 @@ if (service.state === STATE_RUNNING) {
42
60
 
43
61
  Base class that all services should extend. Provides:
44
62
 
45
- - Standardized lifecycle methods (`configure`, `start`, `stop`, `destroy`)
63
+ - Standardized lifecycle methods (`configure`, `start`, `stop`,
64
+ `destroy`)
46
65
  - Flexible configuration system with reconfiguration support
47
66
  - Health monitoring and recovery
48
67
  - Event emission for state changes
@@ -59,19 +78,17 @@ class DatabaseService extends ServiceBase {
59
78
  async _configure(newConfig, oldConfig = null) {
60
79
  if (!oldConfig) {
61
80
  // Initial configuration
62
-
63
81
  this.connectionString = newConfig.connectionString;
64
82
  this.maxConnections = newConfig.maxConnections || 10;
65
83
  return;
66
84
  }
67
85
 
86
+ // Reconfiguration - handle changes intelligently
68
87
  if (oldConfig.connectionString !== newConfig.connectionString) {
69
- // Reconfiguration - handle changes intelligently
70
-
71
88
  // Connection changed - need to reconnect
72
89
  await this.connection?.close();
73
-
74
90
  this.connectionString = newConfig.connectionString;
91
+
75
92
  if (this.state === 'running') {
76
93
  this.connection = await createConnection(this.connectionString);
77
94
  }
@@ -79,7 +96,6 @@ class DatabaseService extends ServiceBase {
79
96
 
80
97
  if (oldConfig.maxConnections !== newConfig.maxConnections) {
81
98
  // Pool size changed - update without reconnect
82
- //
83
99
  this.maxConnections = newConfig.maxConnections;
84
100
  await this.connection?.setMaxConnections(this.maxConnections);
85
101
  }
@@ -122,16 +138,23 @@ await db.configure({
122
138
 
123
139
  ### Protected Methods to Override
124
140
 
125
- - `_configure(newConfig, oldConfig = null)` - Configure service (handles both initial setup and reconfiguration)
141
+ Implement these methods to define service behavior:
142
+
143
+ - `_configure(newConfig, oldConfig = null)` - Configure service
144
+ (handles both initial setup and reconfiguration)
126
145
  - `_start()` - Start the service
127
146
  - `_stop()` - Stop the service
128
147
  - `_destroy()` - Clean up resources (optional)
129
148
  - `_recover()` - Custom recovery logic (optional)
130
149
  - `_healthCheck()` - Return health status (optional)
131
150
 
132
- ### Service events
151
+ **See [PATTERNS.md](./PATTERNS.md) for implementation patterns and best
152
+ practices.**
153
+
154
+ ### Service Events
133
155
 
134
- ServiceBase emits these events (constants from `$lib/services/service-base/constants.js`):
156
+ ServiceBase emits these events (constants from
157
+ `$lib/services/service-base/constants.js`):
135
158
 
136
159
  - `EVENT_STATE_CHANGED` - Service state transitions
137
160
  - `EVENT_TARGET_STATE_CHANGED` - Target state changes
@@ -139,7 +162,8 @@ ServiceBase emits these events (constants from `$lib/services/service-base/const
139
162
  - `EVENT_ERROR` - Service errors
140
163
 
141
164
  ```javascript
142
- import { EVENT_STATE_CHANGED } from '$lib/services/service-base/constants.js';
165
+ import { EVENT_STATE_CHANGED }
166
+ from '$lib/services/service-base/constants.js';
143
167
 
144
168
  service.on(EVENT_STATE_CHANGED, ({ state, previousState }) => {
145
169
  console.log(`Service transitioned from ${previousState} to ${state}`);
@@ -148,7 +172,8 @@ service.on(EVENT_STATE_CHANGED, ({ state, previousState }) => {
148
172
 
149
173
  ## ServiceManager
150
174
 
151
- Manages multiple services with dependency resolution and coordinated lifecycle operations.
175
+ Manages multiple services with dependency resolution and coordinated
176
+ lifecycle operations.
152
177
 
153
178
  ### Features
154
179
 
@@ -160,7 +185,7 @@ Manages multiple services with dependency resolution and coordinated lifecycle o
160
185
  - Service recovery management
161
186
  - Plugin system for extending configuration resolution
162
187
 
163
- ### Usage
188
+ ### Basic Usage
164
189
 
165
190
  ```javascript
166
191
  import { ServiceManager } from '@hkdigital/lib-core/services/index.js';
@@ -199,121 +224,63 @@ const health = await manager.checkHealth();
199
224
  await manager.stopAll();
200
225
  ```
201
226
 
202
- ### Service Access Patterns
203
-
204
- Services receive helpful utilities in their constructor options for accessing other services:
227
+ ### Service Registration
205
228
 
206
229
  ```javascript
207
- /**
208
- * Example service that depends on other services
209
- */
210
- class AuthService extends ServiceBase {
211
- /** @type {(<T>(serviceName: string) => T)} */
212
- #getService;
230
+ manager.register(name, ServiceClass, serviceConfigOrLabel, options);
231
+ ```
232
+
233
+ **Parameters:**
234
+ - `name` - Unique service identifier
235
+ - `ServiceClass` - Class extending ServiceBase
236
+ - `serviceConfigOrLabel` - Service configuration object
237
+ (`Object<string, *>`) or config label string (when using ConfigPlugin)
238
+ - `options.dependencies` - Array of service names this service depends
239
+ on
240
+ - `options.startupPriority` - Higher priority services start first
241
+ (default: 0)
213
242
 
214
- /** @type {() => import('@hkdigital/lib-core/services/index.js').ServiceManager} */
215
- #getManager;
243
+ ### Service Access
216
244
 
217
- constructor(serviceName, options) {
218
- super(serviceName, options);
219
-
220
- // Store service access utilities as private methods
221
- this.#getService = options.getService; // Bound getService function
222
- this.#getManager = options.getManager; // Function to get manager (lazy)
223
- }
224
-
225
- async authenticateUser(credentials) {
226
- // Access other services with full type safety and error checking
227
- const database = this.#getService('database');
228
- const user = await database.findUser(credentials.username);
229
-
230
- // Access manager for advanced operations when needed
231
- const manager = this.#getManager();
232
- const health = await manager.checkHealth();
233
-
234
- return user;
235
- }
236
- }
237
- ```
245
+ ServiceManager provides methods to access registered services:
238
246
 
239
- **Best Practice Pattern:**
247
+ ```javascript
248
+ // Permissive - returns undefined if not found/created
249
+ const service = manager.get('optional-service');
250
+ if (service) {
251
+ // Use service safely
252
+ }
240
253
 
241
- The recommended approach is to store service access functions as **private methods** using the hash prefix. This pattern:
254
+ // Strict - throws error if not found/created
255
+ const service = manager.getService('required-service');
256
+ ```
242
257
 
243
- - **Keeps the API clean** - No public getService/getManager methods exposed
244
- - **Prevents serialization issues** - Private fields don't serialize to JSON
245
- - **Enforces proper encapsulation** - Service dependencies stay internal
246
- - **Provides type safety** - Full generic support with `this.#getService<DatabaseService>('database')`
258
+ **Within services**, use constructor utilities for type-safe access:
247
259
 
248
260
  ```javascript
249
- /**
250
- * Unified service for tracking complete player data including progress and
251
- * profile matches
252
- */
253
- export default class PlayerService extends ServiceBase {
254
-
261
+ class AuthService extends ServiceBase {
255
262
  /** @type {(<T>(serviceName: string) => T)} */
256
263
  #getService;
257
264
 
258
- /**
259
- * @param {string} serviceName
260
- * @param {import('@hkdigital/lib-core/services/typedef.js').ServiceOptions} [options]
261
- */
262
265
  constructor(serviceName, options) {
263
266
  super(serviceName, options);
264
-
265
- this.#getService = options?.getService;
267
+ this.#getService = options.getService;
266
268
  }
267
269
 
268
- async getPlayerProfile(playerId) {
269
- // Access dependent services cleanly
270
+ async authenticate(credentials) {
270
271
  const database = this.#getService('database');
271
- const analytics = this.#getService('analytics');
272
-
273
- const profile = await database.getPlayer(playerId);
274
- const stats = await analytics.getPlayerStats(playerId);
275
-
276
- return { ...profile, stats };
272
+ return await database.findUser(credentials.username);
277
273
  }
278
274
  }
279
275
  ```
280
276
 
281
- **Service Access Methods:**
282
-
283
- ```javascript
284
- // ServiceManager provides two access patterns:
285
-
286
- // 1. Permissive - returns undefined if not found/created
287
- const service = manager.get('optional-service');
288
- if (service) {
289
- // Use service safely
290
- }
291
-
292
- // 2. Strict - throws error if not found/created
293
- const service = manager.getService('required-service'); // Throws if missing
294
- ```
295
-
296
- **Benefits of constructor utilities:**
297
-
298
- - **Lightweight** - Functions don't serialize, keeping services serialization-safe
299
- - **Lazy access** - Manager is only accessed when needed
300
- - **Type safety** - Full generic support with `getService<DatabaseService>('database')`
301
- - **Error handling** - Clear errors when services are missing
302
-
303
- ### Service Registration
304
-
305
- ```javascript
306
- manager.register(name, ServiceClass, serviceConfigOrLabel, options);
307
- ```
308
-
309
- - `name` - Unique service identifier
310
- - `ServiceClass` - Class extending ServiceBase
311
- - `serviceConfigOrLabel` - Service configuration object (`Object<string, *>`) or config label string
312
- - `options.dependencies` - Array of service names this service depends on
313
- - `options.startupPriority` - Higher priority services start first (default: 0)
277
+ **See [PATTERNS.md](./PATTERNS.md) for detailed service access
278
+ patterns.**
314
279
 
315
280
  ### Health Monitoring
316
281
 
282
+ Monitor service health individually or system-wide:
283
+
317
284
  ```javascript
318
285
  import {
319
286
  SERVICE_HEALTH_CHANGED,
@@ -338,6 +305,21 @@ const systemHealth = await manager.checkHealth();
338
305
 
339
306
  ### Error Handling and Recovery
340
307
 
308
+ ServiceManager provides automatic error handling and recovery:
309
+
310
+ ```javascript
311
+ // Listen for service errors
312
+ manager.on(SERVICE_ERROR, async ({ service, error }) => {
313
+ console.log(`Service ${service} failed:`, error.message);
314
+
315
+ // Attempt automatic recovery
316
+ await manager.recoverService(service);
317
+ });
318
+
319
+ // Manual recovery
320
+ await manager.recoverService('database');
321
+ ```
322
+
341
323
  ### Logging Configuration
342
324
 
343
325
  ServiceManager provides centralized logging control for all services:
@@ -369,31 +351,19 @@ manager.setServiceLogLevel({
369
351
  manager.setServiceLogLevel('database:info,auth:debug');
370
352
  ```
371
353
 
372
- ### ServiceManager events
354
+ ### ServiceManager Events
373
355
 
374
- ServiceManager emits these events (constants from `$lib/services/service-manager/constants.js`):
356
+ ServiceManager emits these events (constants from
357
+ `$lib/services/service-manager/constants.js`):
375
358
 
376
359
  - `SERVICE_STATE_CHANGED` - Service state changes
377
360
  - `SERVICE_HEALTH_CHANGED` - Service health changes
378
361
  - `SERVICE_ERROR` - Service errors
379
362
  - `SERVICE_LOG` - Service log messages
380
363
 
381
- ```javascript
382
- // Listen for service errors
383
- manager.on(SERVICE_ERROR, async ({ service, error }) => {
384
- console.log(`Service ${service} failed:`, error.message);
385
-
386
- // Attempt automatic recovery
387
- await manager.recoverService(service);
388
- });
389
-
390
- // Manual recovery
391
- await manager.recoverService('database');
392
- ```
393
-
394
364
  ### Log Event Forwarding
395
365
 
396
- Forward all service log events to a centralised logger:
366
+ Forward all service log events to a centralized logger:
397
367
 
398
368
  ```javascript
399
369
  import { ServiceManager } from '$lib/services/index.js';
@@ -417,149 +387,106 @@ await manager.startAll();
417
387
  unsubscribe();
418
388
  ```
419
389
 
390
+ **See [docs/setup/services-logging.md](../../docs/setup/services-logging.md)
391
+ for complete integration examples.**
392
+
420
393
  ## Plugins
421
394
 
422
- ServiceManager supports plugins e.g. to resolve service configurations dynamically.
395
+ ServiceManager supports plugins to extend functionality, primarily for
396
+ dynamic configuration resolution.
423
397
 
424
398
  ### ConfigPlugin
425
399
 
426
- The most common plugin for resolving service configuration from a pre-parsed configuration object. Perfect for environment variables, config files, or any structured configuration source.
427
-
428
- #### Basic Usage with Environment Variables
400
+ The most common plugin for resolving service configuration from
401
+ environment variables or config files:
429
402
 
430
403
  ```javascript
431
404
  import { ServiceManager } from '$lib/services/index.js';
432
-
433
405
  import ConfigPlugin from '$lib/services/manager-plugins/ConfigPlugin.js';
434
-
435
406
  import { getPrivateEnv } from '$lib/util/sveltekit/env-private.js';
436
407
 
437
- // Load and auto-group environment variables
408
+ // Load environment config
438
409
  const envConfig = getPrivateEnv();
439
- //
440
- // Example:
441
- //
442
- // DATABASE_HOST=localhost
443
- // DATABASE_PORT=5432
444
- // DATABASE_NAME=myapp
445
- // REDIS_HOST=cache-server
446
- // REDIS_PORT=6379
447
- // JWT_SECRET=mysecret
448
- // =>
449
- // {
450
- // database: { host: 'localhost', port: 5432, name: 'myapp' },
451
- // redis: { host: 'cache-server', port: 6379 },
452
- // jwtSecret: 'mysecret'
453
- // }
454
- //
455
-
456
- // Create plugin with grouped config
457
- const configPlugin = new ConfigPlugin(envConfig);
458
410
 
459
- // Attach to ServiceManager
411
+ // Create and attach plugin
412
+ const configPlugin = new ConfigPlugin(envConfig);
460
413
  const manager = new ServiceManager();
461
414
  manager.attachPlugin(configPlugin);
462
415
 
463
416
  // Register services with config labels (not config objects)
464
- manager.register('database', DatabaseService, 'database'); // Uses envConfig.database
465
- manager.register('cache', RedisService, 'redis'); // Uses envConfig.redis
417
+ manager.register('database', DatabaseService, 'database');
418
+ manager.register('cache', RedisService, 'redis');
466
419
 
467
420
  await manager.startAll();
421
+
422
+ // Hot-reload configuration
423
+ await configPlugin.replaceConfig('database', newDatabaseConfig);
468
424
  ```
469
425
 
470
- #### Configuration
426
+ **See [PLUGINS.md](./PLUGINS.md) for complete plugin documentation and
427
+ live configuration updates.**
471
428
 
472
- The plugin constructor accepts an object with configuration data, which can come from any source. E.g. the environment or a configuration file.
429
+ ## Quick Reference
473
430
 
474
- ```javascript
475
- // Combine multiple config sources
476
- const config = {
477
- ...getPrivateEnv(), // Environment variables
478
- ...(await loadConfigFile()), // Config file
479
- database: {
480
- // Override specific settings
481
- ...envConfig.database,
482
- connectionTimeout: 5000
483
- }
484
- };
431
+ ### Service Lifecycle
485
432
 
486
- const plugin = new ConfigPlugin(config);
433
+ ```
434
+ created → configuring → configured → starting → running
435
+
436
+ stopping → stopped → destroying → destroyed
437
+
438
+ error ← → recovering
487
439
  ```
488
440
 
489
- ### Methods
441
+ ### Common Operations
490
442
 
491
443
  ```javascript
492
- // Replace all configurations and clean up unused ones
493
- await configPlugin.replaceAllConfigs(newConfig);
494
-
495
- // Replace configuration for a specific label
496
- await configPlugin.replaceConfig('database', newDatabaseConfig);
497
-
498
- // Clean up configurations not used by any service
499
- await configPlugin.cleanupConfigs();
500
- ```
444
+ // Create and start service
445
+ const service = new MyService('my-service');
446
+ await service.configure(config);
447
+ await service.start();
501
448
 
502
- ### Live Configuration Updates
449
+ // Check health
450
+ const health = await service.healthCheck();
503
451
 
504
- The ConfigPlugin supports pushing configuration updates to running services:
452
+ // Reconfigure
453
+ await service.configure(newConfig);
505
454
 
506
- ```javascript
507
- // Replace config for a specific label and notify all affected services
508
- const updatedServices = await configPlugin.replaceConfig('database', {
509
- host: 'new-host.example.com',
510
- port: 5433,
511
- maxConnections: 50
512
- });
455
+ // Stop service
456
+ await service.stop();
513
457
 
514
- // Returns array of service names that were updated: ['user-service', 'order-service']
515
- console.log(`Updated ${updatedServices.length} services`);
458
+ // Destroy service
459
+ await service.destroy();
516
460
  ```
517
461
 
518
- #### Service Requirements for Live Updates
519
-
520
- For services to support live configuration updates, they must:
521
-
522
- 1. **Implement intelligent `_configure()` logic** that can handle both initial setup and reconfiguration
523
- 2. **Check for meaningful changes** between old and new config
524
- 3. **Apply changes without full restart** when possible
462
+ ### ServiceManager Operations
525
463
 
526
464
  ```javascript
527
- class DatabaseService extends ServiceBase {
528
- // eslint-disable-next-line no-unused-vars
529
- async _configure(newConfig, oldConfig = null) {
530
- if (!oldConfig) {
531
- // Initial configuration
532
- this.connectionString = newConfig.connectionString;
533
- this.maxConnections = newConfig.maxConnections || 10;
534
- return;
535
- }
465
+ // Register services
466
+ manager.register('name', ServiceClass, config, options);
536
467
 
537
- // Live reconfiguration - handle changes intelligently
538
- if (oldConfig.connectionString !== newConfig.connectionString) {
539
- // Connection changed - need to reconnect
540
- await this.connection?.close();
541
- this.connectionString = newConfig.connectionString;
468
+ // Lifecycle
469
+ await manager.startAll();
470
+ await manager.stopAll();
471
+ await manager.destroyAll();
542
472
 
543
- if (this.state === 'running') {
544
- this.connection = await createConnection(this.connectionString);
545
- }
546
- }
473
+ // Access
474
+ const service = manager.get('name');
475
+ const service = manager.getService('name'); // Throws if missing
547
476
 
548
- if (oldConfig.maxConnections !== newConfig.maxConnections) {
549
- // Pool size changed - update without reconnect
550
- this.maxConnections = newConfig.maxConnections;
551
- await this.connection?.setMaxConnections(this.maxConnections);
552
- }
553
- }
554
- }
477
+ // Health
478
+ const health = await manager.checkHealth();
479
+ const serviceHealth = await manager.getServiceHealth('name');
480
+
481
+ // Recovery
482
+ await manager.recoverService('name');
555
483
  ```
556
484
 
557
- ## Best Practices
485
+ ## Next Steps
558
486
 
559
- 1. **Always extend ServiceBase** for consistent lifecycle management
560
- 2. **Keep configuration lightweight** - heavy work should be in `_start()`
561
- 3. **Implement proper cleanup** in `_stop()` to prevent resource leaks
562
- 4. **Use health checks** for monitoring critical service functionality
563
- 5. **Declare dependencies explicitly** when registering with ServiceManager
564
- 6. **Handle errors gracefully** and implement recovery where appropriate
565
- 7. **Use descriptive service names** for better logging and debugging
487
+ - **Patterns**: See [PATTERNS.md](./PATTERNS.md) for service access
488
+ patterns, configuration patterns, and best practices
489
+ - **Plugins**: See [PLUGINS.md](./PLUGINS.md) for ConfigPlugin and
490
+ custom plugins
491
+ - **Architecture**: See [docs/setup/services-logging.md](../../docs/setup/services-logging.md)
492
+ for integration with logging and SvelteKit hooks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.5.92",
3
+ "version": "0.5.93",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"