@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 CHANGED
@@ -3,7 +3,28 @@
3
3
  Core library that we use to power up our SvelteKit projects
4
4
 
5
5
  This is a library for [SvelteKit](https://svelte.dev/) projects.
6
- It contains common code, base components and documentation that help you with setting up a new project.
6
+ It contains common code, base components and documentation that help
7
+ you with setting up a new project.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Using the library](#using-the-library)
12
+ - [Install](#install)
13
+ - [Peer Dependencies](#peer-dependencies)
14
+ - [Design System & Configuration](#design-system--configuration)
15
+ - [Logging System](#logging-system)
16
+ - [Documentation](#documentation)
17
+ - [Update](#update)
18
+ - [Available scripts](#available-scripts)
19
+ - [Import Validation](#import-validation)
20
+ - [Import Patterns and Export Structure](#import-patterns-and-export-structure)
21
+ - [CSS Architecture](#css-architecture-appcss)
22
+ - [Critical: data-theme Attribute](#critical-data-theme-attribute)
23
+ - [Building the library](#building-the-library)
24
+ - [Running the showcase app](#running-the-showcase-app)
25
+ - [Developing](#developing)
26
+ - [Publishing](#publishing)
27
+ - [Contribute](#contribute)
7
28
 
8
29
  ## Using the library
9
30
 
@@ -247,14 +268,47 @@ The library includes a comprehensive logging system that provides:
247
268
 
248
269
  ## Documentation
249
270
 
250
- For detailed setup guides and configuration:
251
- - **Project setup**: [docs/setup/new-project.md](./docs/setup/new-project.md) - SvelteKit project setup
252
- - **Library setup**: [docs/setup/new-lib.md](./docs/setup/new-lib.md) - SvelteKit library setup
253
- - **Services & logging**: [docs/setup/services-logging.md](./docs/setup/services-logging.md) - Service management architecture
254
- - **Configuration files**: [docs/config/root-config-files.md](./docs/config/root-config-files.md) - Config file reference
255
- - **Design system**: [src/lib/design/README.md](./src/lib/design/README.md) - Design tokens and theming
256
- - **Vite configuration**: [src/lib/config/README.md](./src/lib/config/README.md) - Build configuration
257
- - **Logging system**: [src/lib/logging/README.md](./src/lib/logging/README.md) - Server and client logging
271
+ Comprehensive documentation organized by topic.
272
+
273
+ ### Getting Started
274
+
275
+ Start here if you're setting up a new project or library:
276
+
277
+ - **[New project setup](./docs/setup/new-project.md)** - Complete guide
278
+ for setting up a SvelteKit application with lib-core
279
+ - **[New library setup](./docs/setup/new-lib.md)** - Complete guide for
280
+ setting up a SvelteKit library with lib-core
281
+
282
+ ### Architecture Guides
283
+
284
+ Learn how the different systems work together:
285
+
286
+ - **[Services & logging architecture](./docs/setup/services-logging.md)**
287
+ - How service management and logging integrate with SvelteKit
288
+ - **[Service patterns](./src/lib/services/PATTERNS.md)** - Best
289
+ practices and design patterns for implementing services
290
+ - **[Service plugins](./src/lib/services/PLUGINS.md)** - ConfigPlugin
291
+ and custom plugin development
292
+
293
+ ### API Reference
294
+
295
+ Detailed API documentation for each module:
296
+
297
+ - **[Logging](./src/lib/logging/README.md)** - Server and client
298
+ logging API, log levels, and formatters
299
+ - **[Services](./src/lib/services/README.md)** - ServiceBase and
300
+ ServiceManager API reference
301
+ - **[Design system](./src/lib/design/README.md)** - Design tokens,
302
+ theming, and UI utilities
303
+ - **[Vite configuration](./src/lib/config/README.md)** - Build
304
+ configuration and optimization
305
+
306
+ ### Configuration
307
+
308
+ Reference documentation for configuration files:
309
+
310
+ - **[Root config files](./docs/config/root-config-files.md)** - Guide
311
+ to vite.config.js, tailwind.config.js, and other root configs
258
312
 
259
313
  ### Update
260
314
 
@@ -131,13 +131,8 @@ export function getIsPhone() {
131
131
  * @returns {boolean} true if mobile device
132
132
  */
133
133
  export function getIsMobile() {
134
- // @ts-ignore
135
- if (navigator?.userAgentData?.mobile !== undefined) {
136
- // Modern API - most reliable
137
- // @ts-ignore
138
- return navigator.userAgentData.mobile;
139
- }
140
-
134
+ // Check Apple devices first - userAgentData.mobile is unreliable for
135
+ // iPads with M-series chips (reports false despite being mobile)
141
136
  if (getIsAppleMobile()) {
142
137
  return true;
143
138
  }
@@ -146,6 +141,13 @@ export function getIsMobile() {
146
141
  return true;
147
142
  }
148
143
 
144
+ // @ts-ignore
145
+ if (navigator?.userAgentData?.mobile !== undefined) {
146
+ // Modern API - use as fallback
147
+ // @ts-ignore
148
+ return navigator.userAgentData.mobile;
149
+ }
150
+
149
151
  return false;
150
152
  }
151
153
 
@@ -3,6 +3,14 @@
3
3
  Universal logging utilities for SvelteKit applications with
4
4
  server/client/universal logger factories.
5
5
 
6
+ **See also:**
7
+ - **Architecture**: [docs/setup/services-logging.md](../../docs/setup/services-logging.md)
8
+ - How logging and services work together
9
+ - **Services**: [src/lib/services/README.md](../services/README.md) -
10
+ Service management system
11
+ - **Main README**: [README.md](../../README.md) - Library overview and
12
+ setup
13
+
6
14
  ## Installation
7
15
 
8
16
  ```bash
@@ -182,61 +190,15 @@ export function handleError({ error, event }) {
182
190
  }
183
191
  ```
184
192
 
185
- ### Client Service Integration
186
-
187
- When integrating with a service management system, you can set up global
188
- error handling and forward service logs to the main logger:
189
-
190
- ```javascript
191
- import { ServiceManager } from '@hkdigital/lib-core/services/index.js';
192
- import { initClientLogger } from '$lib/logging/client.js';
193
-
194
- /** @type {ServiceManager} */
195
- let manager;
196
-
197
- export async function initClientServices() {
198
- if (!manager) {
199
- const logger = initClientLogger();
200
-
201
- // Catch errors and unhandled promise rejections
202
-
203
- // Log unhandled errors
204
- window.addEventListener('error', (event) => {
205
- logger.error(event, { url: window.location.pathname });
206
- event.preventDefault();
207
- });
208
-
209
- // Log unhandled promise rejections
210
- window.addEventListener('unhandledrejection', (event) => {
211
- logger.error(event, { url: window.location.pathname });
212
- // Ignored by Firefox
213
- event.preventDefault();
214
- });
215
-
216
- manager = new ServiceManager({ debug: true });
217
-
218
- // Listen to all log events and forward them to the logger
219
- manager.onLogEvent((logEvent) => {
220
- logger.logFromEvent(logEvent);
221
- });
222
-
223
- // Register services
224
- manager.register(SERVICE_AUDIO, AudioService);
225
- manager.register(SERVICE_EVENT_LOG, EventLogService);
226
- manager.register(SERVICE_PLAYER_DATA, PlayerDataService);
227
- }
193
+ ### Service Integration
228
194
 
229
- await manager.startAll();
230
- return manager;
231
- }
195
+ For integrating logging with the service management system, including
196
+ how to forward service logs to the main logger, see:
232
197
 
233
- export function getManager() {
234
- if (!manager) {
235
- throw new Error('Client services should be initialised first');
236
- }
237
- return manager;
238
- }
239
- ```
198
+ - [Services README](../services/README.md) - ServiceManager log event
199
+ forwarding
200
+ - [Services & Logging Architecture](../../docs/setup/services-logging.md)
201
+ - Complete integration examples
240
202
 
241
203
  ## Development
242
204
 
@@ -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)