@hkdigital/lib-core 0.5.92 → 0.5.94
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/config/generators/imagetools.d.ts +14 -0
- package/dist/config/generators/imagetools.js +55 -0
- package/dist/config/imagetools.d.ts +12 -0
- package/dist/logging/README.md +15 -53
- package/dist/meta/README.md +92 -0
- package/dist/meta/components/Favicons.svelte +30 -0
- package/dist/meta/components/Favicons.svelte.d.ts +103 -0
- package/dist/meta/components/PWA.svelte +51 -0
- package/dist/meta/components/PWA.svelte.d.ts +103 -0
- package/dist/meta/components/SEO.svelte +146 -0
- package/dist/meta/components/SEO.svelte.d.ts +108 -0
- package/dist/meta/components.d.ts +3 -0
- package/dist/meta/components.js +3 -0
- package/dist/meta/config.typedef.d.ts +98 -0
- package/dist/meta/config.typedef.js +44 -0
- package/dist/meta/typedef.d.ts +3 -0
- package/dist/meta/typedef.js +14 -0
- package/dist/meta/utils/lang.d.ts +29 -0
- package/dist/meta/utils/lang.js +84 -0
- package/dist/meta/utils/robots.d.ts +1 -0
- package/dist/meta/utils/robots.js +1 -0
- package/dist/meta/utils/sitemap.d.ts +1 -0
- package/dist/meta/utils/sitemap.js +1 -0
- package/dist/meta/utils.d.ts +3 -0
- package/dist/meta/utils.js +11 -0
- 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
- package/dist/meta/robots.d.ts +0 -1
- package/dist/meta/robots.js +0 -5
- package/dist/meta/sitemap.d.ts +0 -1
- package/dist/meta/sitemap.js +0 -5
- /package/dist/meta/{robots/index.d.ts → utils/robots/robots.d.ts} +0 -0
- /package/dist/meta/{robots/index.js → utils/robots/robots.js} +0 -0
- /package/dist/meta/{robots → utils/robots}/typedef.d.ts +0 -0
- /package/dist/meta/{robots → utils/robots}/typedef.js +0 -0
- /package/dist/meta/{sitemap/index.d.ts → utils/sitemap/sitemap.d.ts} +0 -0
- /package/dist/meta/{sitemap/index.js → utils/sitemap/sitemap.js} +0 -0
- /package/dist/meta/{sitemap → utils/sitemap}/typedef.d.ts +0 -0
- /package/dist/meta/{sitemap → utils/sitemap}/typedef.js +0 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# Service Patterns and Best Practices
|
|
2
|
+
|
|
3
|
+
Design patterns and best practices for implementing services with the
|
|
4
|
+
ServiceBase and ServiceManager system.
|
|
5
|
+
|
|
6
|
+
**See also:**
|
|
7
|
+
- **API Reference**: [README.md](./README.md) - ServiceBase and
|
|
8
|
+
ServiceManager API
|
|
9
|
+
- **Plugins**: [PLUGINS.md](./PLUGINS.md) - Plugin system and
|
|
10
|
+
ConfigPlugin
|
|
11
|
+
- **Architecture**: [docs/setup/services-logging.md](../../docs/setup/services-logging.md)
|
|
12
|
+
- Integration patterns
|
|
13
|
+
|
|
14
|
+
## Service Access Patterns
|
|
15
|
+
|
|
16
|
+
Services receive helpful utilities in their constructor options for
|
|
17
|
+
accessing other services and the manager.
|
|
18
|
+
|
|
19
|
+
### Basic Pattern
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
/**
|
|
23
|
+
* Example service that depends on other services
|
|
24
|
+
*/
|
|
25
|
+
class AuthService extends ServiceBase {
|
|
26
|
+
/** @type {(<T>(serviceName: string) => T)} */
|
|
27
|
+
#getService;
|
|
28
|
+
|
|
29
|
+
/** @type {() => import('@hkdigital/lib-core/services/index.js').ServiceManager} */
|
|
30
|
+
#getManager;
|
|
31
|
+
|
|
32
|
+
constructor(serviceName, options) {
|
|
33
|
+
super(serviceName, options);
|
|
34
|
+
|
|
35
|
+
// Store service access utilities as private methods
|
|
36
|
+
this.#getService = options.getService; // Bound getService function
|
|
37
|
+
this.#getManager = options.getManager; // Function to get manager (lazy)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async authenticateUser(credentials) {
|
|
41
|
+
// Access other services with full type safety and error checking
|
|
42
|
+
const database = this.#getService('database');
|
|
43
|
+
const user = await database.findUser(credentials.username);
|
|
44
|
+
|
|
45
|
+
// Access manager for advanced operations when needed
|
|
46
|
+
const manager = this.#getManager();
|
|
47
|
+
const health = await manager.checkHealth();
|
|
48
|
+
|
|
49
|
+
return user;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Recommended Pattern: Private Methods
|
|
55
|
+
|
|
56
|
+
The recommended approach is to store service access functions as
|
|
57
|
+
**private methods** using the hash prefix. This pattern provides:
|
|
58
|
+
|
|
59
|
+
**Benefits:**
|
|
60
|
+
- **Keeps the API clean** - No public getService/getManager methods
|
|
61
|
+
exposed
|
|
62
|
+
- **Prevents serialization issues** - Private fields don't serialize
|
|
63
|
+
to JSON
|
|
64
|
+
- **Enforces proper encapsulation** - Service dependencies stay
|
|
65
|
+
internal
|
|
66
|
+
- **Provides type safety** - Full generic support with
|
|
67
|
+
`this.#getService<DatabaseService>('database')`
|
|
68
|
+
|
|
69
|
+
**Example:**
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
/**
|
|
73
|
+
* Unified service for tracking complete player data including progress
|
|
74
|
+
* and profile matches
|
|
75
|
+
*/
|
|
76
|
+
export default class PlayerService extends ServiceBase {
|
|
77
|
+
|
|
78
|
+
/** @type {(<T>(serviceName: string) => T)} */
|
|
79
|
+
#getService;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} serviceName
|
|
83
|
+
* @param {import('@hkdigital/lib-core/services/typedef.js').ServiceOptions} [options]
|
|
84
|
+
*/
|
|
85
|
+
constructor(serviceName, options) {
|
|
86
|
+
super(serviceName, options);
|
|
87
|
+
|
|
88
|
+
this.#getService = options?.getService;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getPlayerProfile(playerId) {
|
|
92
|
+
// Access dependent services cleanly
|
|
93
|
+
const database = this.#getService('database');
|
|
94
|
+
const analytics = this.#getService('analytics');
|
|
95
|
+
|
|
96
|
+
const profile = await database.getPlayer(playerId);
|
|
97
|
+
const stats = await analytics.getPlayerStats(playerId);
|
|
98
|
+
|
|
99
|
+
return { ...profile, stats };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Service Access Methods
|
|
105
|
+
|
|
106
|
+
ServiceManager provides two access patterns:
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
// 1. Permissive - returns undefined if not found/created
|
|
110
|
+
const service = manager.get('optional-service');
|
|
111
|
+
if (service) {
|
|
112
|
+
// Use service safely
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Strict - throws error if not found/created
|
|
116
|
+
const service = manager.getService('required-service'); // Throws if missing
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**When to use each:**
|
|
120
|
+
- Use `get()` for optional services or when checking availability
|
|
121
|
+
- Use `getService()` for required dependencies (clearer error messages)
|
|
122
|
+
|
|
123
|
+
### Constructor Utilities Benefits
|
|
124
|
+
|
|
125
|
+
**Lightweight:**
|
|
126
|
+
Functions don't serialize, keeping services serialization-safe.
|
|
127
|
+
|
|
128
|
+
**Lazy access:**
|
|
129
|
+
Manager is only accessed when needed, avoiding circular dependencies
|
|
130
|
+
during initialization.
|
|
131
|
+
|
|
132
|
+
**Type safety:**
|
|
133
|
+
Full generic support with JSDoc annotations enables IDE autocomplete
|
|
134
|
+
and type checking.
|
|
135
|
+
|
|
136
|
+
**Error handling:**
|
|
137
|
+
Clear, consistent errors when services are missing or not yet
|
|
138
|
+
initialized.
|
|
139
|
+
|
|
140
|
+
## Configuration Patterns
|
|
141
|
+
|
|
142
|
+
### Initial Configuration vs Reconfiguration
|
|
143
|
+
|
|
144
|
+
Services should handle both initial setup and runtime reconfiguration
|
|
145
|
+
intelligently.
|
|
146
|
+
|
|
147
|
+
**Pattern:**
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
class DatabaseService extends ServiceBase {
|
|
151
|
+
// eslint-disable-next-line no-unused-vars
|
|
152
|
+
async _configure(newConfig, oldConfig = null) {
|
|
153
|
+
if (!oldConfig) {
|
|
154
|
+
// Initial configuration - store all settings
|
|
155
|
+
this.connectionString = newConfig.connectionString;
|
|
156
|
+
this.maxConnections = newConfig.maxConnections || 10;
|
|
157
|
+
this.timeout = newConfig.timeout || 5000;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Reconfiguration - handle changes intelligently
|
|
162
|
+
if (oldConfig.connectionString !== newConfig.connectionString) {
|
|
163
|
+
// Connection changed - need to reconnect
|
|
164
|
+
await this.connection?.close();
|
|
165
|
+
this.connectionString = newConfig.connectionString;
|
|
166
|
+
|
|
167
|
+
if (this.state === 'running') {
|
|
168
|
+
this.connection = await createConnection(this.connectionString);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (oldConfig.maxConnections !== newConfig.maxConnections) {
|
|
173
|
+
// Pool size changed - update without reconnect
|
|
174
|
+
this.maxConnections = newConfig.maxConnections;
|
|
175
|
+
await this.connection?.setMaxConnections(this.maxConnections);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (oldConfig.timeout !== newConfig.timeout) {
|
|
179
|
+
// Timeout changed - just update the setting
|
|
180
|
+
this.timeout = newConfig.timeout;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Key principles:**
|
|
187
|
+
- Check `!oldConfig` to detect initial configuration
|
|
188
|
+
- Compare old and new config to identify what changed
|
|
189
|
+
- Apply minimal changes (don't restart if not needed)
|
|
190
|
+
- Update running state when possible
|
|
191
|
+
|
|
192
|
+
### Configuration Defaults
|
|
193
|
+
|
|
194
|
+
Handle missing configuration gracefully with sensible defaults.
|
|
195
|
+
|
|
196
|
+
**Pattern:**
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
async _configure(newConfig, oldConfig = null) {
|
|
200
|
+
// Use defaults for missing values
|
|
201
|
+
this.host = newConfig.host || 'localhost';
|
|
202
|
+
this.port = newConfig.port || 5432;
|
|
203
|
+
this.maxConnections = newConfig.maxConnections || 10;
|
|
204
|
+
this.timeout = newConfig.timeout || 5000;
|
|
205
|
+
this.retryAttempts = newConfig.retryAttempts ?? 3; // Use ?? for zero values
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Lifecycle Patterns
|
|
210
|
+
|
|
211
|
+
### Proper Resource Management
|
|
212
|
+
|
|
213
|
+
Always clean up resources in the stop method.
|
|
214
|
+
|
|
215
|
+
**Pattern:**
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
class WebSocketService extends ServiceBase {
|
|
219
|
+
async _start() {
|
|
220
|
+
this.ws = new WebSocket(this.url);
|
|
221
|
+
this.intervalId = setInterval(() => this.#ping(), 30000);
|
|
222
|
+
|
|
223
|
+
await new Promise((resolve) => {
|
|
224
|
+
this.ws.onopen = resolve;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async _stop() {
|
|
229
|
+
// Clear timers
|
|
230
|
+
if (this.intervalId) {
|
|
231
|
+
clearInterval(this.intervalId);
|
|
232
|
+
this.intervalId = null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Close connections
|
|
236
|
+
if (this.ws) {
|
|
237
|
+
this.ws.close();
|
|
238
|
+
this.ws = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async _destroy() {
|
|
243
|
+
// Clean up any remaining resources
|
|
244
|
+
await this._stop();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Async Initialization
|
|
250
|
+
|
|
251
|
+
Keep `_configure()` lightweight, do heavy work in `_start()`.
|
|
252
|
+
|
|
253
|
+
**Good:**
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
async _configure(newConfig, oldConfig = null) {
|
|
257
|
+
// Just store config
|
|
258
|
+
this.apiKey = newConfig.apiKey;
|
|
259
|
+
this.endpoint = newConfig.endpoint;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async _start() {
|
|
263
|
+
// Heavy work here
|
|
264
|
+
this.client = await createApiClient(this.apiKey, this.endpoint);
|
|
265
|
+
await this.client.authenticate();
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Bad:**
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
async _configure(newConfig, oldConfig = null) {
|
|
273
|
+
// Don't do heavy work in configure
|
|
274
|
+
this.client = await createApiClient(newConfig.apiKey);
|
|
275
|
+
await this.client.authenticate(); // ❌ Heavy operation
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Health Check Patterns
|
|
280
|
+
|
|
281
|
+
### Return Useful Metrics
|
|
282
|
+
|
|
283
|
+
Health checks should return actionable information.
|
|
284
|
+
|
|
285
|
+
**Pattern:**
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
async _healthCheck() {
|
|
289
|
+
const start = Date.now();
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await this.connection.ping();
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
latency: Date.now() - start,
|
|
296
|
+
connections: this.connection.activeConnections,
|
|
297
|
+
queueSize: this.connection.queueSize
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
error: error.message,
|
|
302
|
+
lastSuccessful: this.lastSuccessfulPing
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Implement Recovery Logic
|
|
309
|
+
|
|
310
|
+
Provide custom recovery for services that can auto-heal.
|
|
311
|
+
|
|
312
|
+
**Pattern:**
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
async _recover() {
|
|
316
|
+
// Close broken connection
|
|
317
|
+
await this.connection?.close();
|
|
318
|
+
|
|
319
|
+
// Wait before reconnecting
|
|
320
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
321
|
+
|
|
322
|
+
// Recreate connection
|
|
323
|
+
this.connection = await createConnection(this.connectionString);
|
|
324
|
+
|
|
325
|
+
// Verify it works
|
|
326
|
+
await this.connection.ping();
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Error Handling Patterns
|
|
331
|
+
|
|
332
|
+
### Graceful Degradation
|
|
333
|
+
|
|
334
|
+
Handle errors without crashing the entire system.
|
|
335
|
+
|
|
336
|
+
**Pattern:**
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
class CacheService extends ServiceBase {
|
|
340
|
+
async get(key) {
|
|
341
|
+
try {
|
|
342
|
+
return await this.redis.get(key);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
this.logger.warn('Cache read failed, falling back', { key, error });
|
|
345
|
+
return null; // Graceful fallback
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async set(key, value) {
|
|
350
|
+
try {
|
|
351
|
+
await this.redis.set(key, value);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
this.logger.error('Cache write failed', { key, error });
|
|
354
|
+
// Don't throw - cache writes are not critical
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Timeout Handling
|
|
361
|
+
|
|
362
|
+
Set appropriate timeouts for long-running operations.
|
|
363
|
+
|
|
364
|
+
**Pattern:**
|
|
365
|
+
|
|
366
|
+
```javascript
|
|
367
|
+
async _start() {
|
|
368
|
+
const timeout = this.config.startTimeout || 10000;
|
|
369
|
+
|
|
370
|
+
const controller = new AbortController();
|
|
371
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
this.connection = await fetch(this.endpoint, {
|
|
375
|
+
signal: controller.signal
|
|
376
|
+
});
|
|
377
|
+
} finally {
|
|
378
|
+
clearTimeout(timeoutId);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Dependency Management Patterns
|
|
384
|
+
|
|
385
|
+
### Declare Dependencies Explicitly
|
|
386
|
+
|
|
387
|
+
Always declare service dependencies for proper startup ordering.
|
|
388
|
+
|
|
389
|
+
**Pattern:**
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
// Register services with explicit dependencies
|
|
393
|
+
manager.register('database', DatabaseService, dbConfig);
|
|
394
|
+
|
|
395
|
+
manager.register(
|
|
396
|
+
'cache',
|
|
397
|
+
CacheService,
|
|
398
|
+
cacheConfig,
|
|
399
|
+
{
|
|
400
|
+
dependencies: ['database'] // Cache needs database
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
manager.register(
|
|
405
|
+
'auth',
|
|
406
|
+
AuthService,
|
|
407
|
+
authConfig,
|
|
408
|
+
{
|
|
409
|
+
dependencies: ['database', 'cache'] // Auth needs both
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Startup Priority
|
|
415
|
+
|
|
416
|
+
Use priority for services that should start early.
|
|
417
|
+
|
|
418
|
+
**Pattern:**
|
|
419
|
+
|
|
420
|
+
```javascript
|
|
421
|
+
// Start logger first (highest priority)
|
|
422
|
+
manager.register(
|
|
423
|
+
'logger',
|
|
424
|
+
LoggerService,
|
|
425
|
+
logConfig,
|
|
426
|
+
{ startupPriority: 100 }
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Then database (high priority)
|
|
430
|
+
manager.register(
|
|
431
|
+
'database',
|
|
432
|
+
DatabaseService,
|
|
433
|
+
dbConfig,
|
|
434
|
+
{ startupPriority: 50 }
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Then other services (default priority: 0)
|
|
438
|
+
manager.register('auth', AuthService, authConfig);
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Best Practices
|
|
442
|
+
|
|
443
|
+
### Service Design
|
|
444
|
+
|
|
445
|
+
1. **Always extend ServiceBase** for consistent lifecycle management
|
|
446
|
+
2. **Keep configuration lightweight** - heavy work should be in
|
|
447
|
+
`_start()`
|
|
448
|
+
3. **Implement proper cleanup** in `_stop()` to prevent resource leaks
|
|
449
|
+
4. **Use health checks** for monitoring critical service functionality
|
|
450
|
+
5. **Handle errors gracefully** and implement recovery where
|
|
451
|
+
appropriate
|
|
452
|
+
|
|
453
|
+
### Service Registration
|
|
454
|
+
|
|
455
|
+
6. **Declare dependencies explicitly** when registering with
|
|
456
|
+
ServiceManager
|
|
457
|
+
7. **Use descriptive service names** for better logging and debugging
|
|
458
|
+
8. **Set appropriate priorities** for services with ordering
|
|
459
|
+
requirements
|
|
460
|
+
|
|
461
|
+
### Service Implementation
|
|
462
|
+
|
|
463
|
+
9. **Store service access as private methods** using hash prefix
|
|
464
|
+
10. **Return useful metrics** from health checks
|
|
465
|
+
11. **Implement intelligent reconfiguration** that applies minimal
|
|
466
|
+
changes
|
|
467
|
+
12. **Handle missing config gracefully** with sensible defaults
|
|
468
|
+
|
|
469
|
+
### Resource Management
|
|
470
|
+
|
|
471
|
+
13. **Clean up all resources** in `_stop()` (timers, connections,
|
|
472
|
+
listeners)
|
|
473
|
+
14. **Set appropriate timeouts** for long-running operations
|
|
474
|
+
15. **Use graceful degradation** instead of throwing errors when
|
|
475
|
+
possible
|
|
476
|
+
16. **Test service lifecycle** (start, stop, restart, reconfigure)
|