digitaltwin-core 0.14.3 → 1.0.1

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 (159) hide show
  1. package/README.md +218 -1
  2. package/dist/auth/apisix_parser.d.ts +56 -56
  3. package/dist/auth/apisix_parser.d.ts.map +1 -1
  4. package/dist/auth/apisix_parser.js +72 -86
  5. package/dist/auth/apisix_parser.js.map +1 -1
  6. package/dist/auth/auth_provider.d.ts +118 -0
  7. package/dist/auth/auth_provider.d.ts.map +1 -0
  8. package/dist/auth/auth_provider.js +8 -0
  9. package/dist/auth/auth_provider.js.map +1 -0
  10. package/dist/auth/auth_provider_factory.d.ts +91 -0
  11. package/dist/auth/auth_provider_factory.d.ts.map +1 -0
  12. package/dist/auth/auth_provider_factory.js +146 -0
  13. package/dist/auth/auth_provider_factory.js.map +1 -0
  14. package/dist/auth/index.d.ts +4 -1
  15. package/dist/auth/index.d.ts.map +1 -1
  16. package/dist/auth/index.js +3 -0
  17. package/dist/auth/index.js.map +1 -1
  18. package/dist/auth/providers/gateway_auth_provider.d.ts +78 -0
  19. package/dist/auth/providers/gateway_auth_provider.d.ts.map +1 -0
  20. package/dist/auth/providers/gateway_auth_provider.js +109 -0
  21. package/dist/auth/providers/gateway_auth_provider.js.map +1 -0
  22. package/dist/auth/providers/index.d.ts +4 -0
  23. package/dist/auth/providers/index.d.ts.map +1 -0
  24. package/dist/auth/providers/index.js +4 -0
  25. package/dist/auth/providers/index.js.map +1 -0
  26. package/dist/auth/providers/jwt_auth_provider.d.ts +91 -0
  27. package/dist/auth/providers/jwt_auth_provider.d.ts.map +1 -0
  28. package/dist/auth/providers/jwt_auth_provider.js +204 -0
  29. package/dist/auth/providers/jwt_auth_provider.js.map +1 -0
  30. package/dist/auth/providers/no_auth_provider.d.ts +61 -0
  31. package/dist/auth/providers/no_auth_provider.d.ts.map +1 -0
  32. package/dist/auth/providers/no_auth_provider.js +76 -0
  33. package/dist/auth/providers/no_auth_provider.js.map +1 -0
  34. package/dist/auth/types.d.ts +5 -3
  35. package/dist/auth/types.d.ts.map +1 -1
  36. package/dist/components/assets_manager.d.ts +1 -1
  37. package/dist/components/assets_manager.d.ts.map +1 -1
  38. package/dist/components/assets_manager.js +54 -48
  39. package/dist/components/assets_manager.js.map +1 -1
  40. package/dist/components/collector.d.ts.map +1 -1
  41. package/dist/components/collector.js +30 -18
  42. package/dist/components/collector.js.map +1 -1
  43. package/dist/components/custom_table_manager.d.ts.map +1 -1
  44. package/dist/components/custom_table_manager.js +36 -65
  45. package/dist/components/custom_table_manager.js.map +1 -1
  46. package/dist/components/global_assets_handler.d.ts +4 -2
  47. package/dist/components/global_assets_handler.d.ts.map +1 -1
  48. package/dist/components/global_assets_handler.js.map +1 -1
  49. package/dist/components/harvester.d.ts.map +1 -1
  50. package/dist/components/harvester.js +46 -33
  51. package/dist/components/harvester.js.map +1 -1
  52. package/dist/components/interfaces.d.ts +3 -2
  53. package/dist/components/interfaces.d.ts.map +1 -1
  54. package/dist/components/map_manager.d.ts.map +1 -1
  55. package/dist/components/map_manager.js.map +1 -1
  56. package/dist/components/tileset_manager.d.ts +2 -1
  57. package/dist/components/tileset_manager.d.ts.map +1 -1
  58. package/dist/components/tileset_manager.js +20 -15
  59. package/dist/components/tileset_manager.js.map +1 -1
  60. package/dist/database/adapters/knex_database_adapter.d.ts +6 -1
  61. package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -1
  62. package/dist/database/adapters/knex_database_adapter.js +118 -36
  63. package/dist/database/adapters/knex_database_adapter.js.map +1 -1
  64. package/dist/database/database_adapter.d.ts +13 -1
  65. package/dist/database/database_adapter.d.ts.map +1 -1
  66. package/dist/database/database_adapter.js.map +1 -1
  67. package/dist/engine/component_types.d.ts +95 -0
  68. package/dist/engine/component_types.d.ts.map +1 -0
  69. package/dist/engine/component_types.js +93 -0
  70. package/dist/engine/component_types.js.map +1 -0
  71. package/dist/engine/digital_twin_engine.d.ts +121 -6
  72. package/dist/engine/digital_twin_engine.d.ts.map +1 -1
  73. package/dist/engine/digital_twin_engine.js +402 -74
  74. package/dist/engine/digital_twin_engine.js.map +1 -1
  75. package/dist/engine/endpoints.d.ts.map +1 -1
  76. package/dist/engine/endpoints.js +35 -3
  77. package/dist/engine/endpoints.js.map +1 -1
  78. package/dist/engine/error_handler.d.ts +20 -0
  79. package/dist/engine/error_handler.d.ts.map +1 -0
  80. package/dist/engine/error_handler.js +69 -0
  81. package/dist/engine/error_handler.js.map +1 -0
  82. package/dist/engine/events.d.ts +1 -1
  83. package/dist/engine/events.d.ts.map +1 -1
  84. package/dist/engine/events.js.map +1 -1
  85. package/dist/engine/health.d.ts +112 -0
  86. package/dist/engine/health.d.ts.map +1 -0
  87. package/dist/engine/health.js +190 -0
  88. package/dist/engine/health.js.map +1 -0
  89. package/dist/engine/initializer.d.ts.map +1 -1
  90. package/dist/engine/initializer.js +6 -4
  91. package/dist/engine/initializer.js.map +1 -1
  92. package/dist/engine/scheduler.d.ts.map +1 -1
  93. package/dist/engine/scheduler.js +17 -9
  94. package/dist/engine/scheduler.js.map +1 -1
  95. package/dist/engine/upload_processor.d.ts.map +1 -1
  96. package/dist/engine/upload_processor.js +24 -12
  97. package/dist/engine/upload_processor.js.map +1 -1
  98. package/dist/errors/index.d.ts +94 -0
  99. package/dist/errors/index.d.ts.map +1 -0
  100. package/dist/errors/index.js +149 -0
  101. package/dist/errors/index.js.map +1 -0
  102. package/dist/index.d.ts +9 -0
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +13 -0
  105. package/dist/index.js.map +1 -1
  106. package/dist/loader/component_loader.d.ts +128 -0
  107. package/dist/loader/component_loader.d.ts.map +1 -0
  108. package/dist/loader/component_loader.js +330 -0
  109. package/dist/loader/component_loader.js.map +1 -0
  110. package/dist/loader/index.d.ts +19 -0
  111. package/dist/loader/index.d.ts.map +1 -0
  112. package/dist/loader/index.js +19 -0
  113. package/dist/loader/index.js.map +1 -0
  114. package/dist/storage/adapters/local_storage_service.d.ts +6 -0
  115. package/dist/storage/adapters/local_storage_service.d.ts.map +1 -1
  116. package/dist/storage/adapters/local_storage_service.js +26 -4
  117. package/dist/storage/adapters/local_storage_service.js.map +1 -1
  118. package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -1
  119. package/dist/storage/adapters/ovh_storage_service.js +5 -6
  120. package/dist/storage/adapters/ovh_storage_service.js.map +1 -1
  121. package/dist/storage/storage_factory.d.ts.map +1 -1
  122. package/dist/storage/storage_factory.js +4 -1
  123. package/dist/storage/storage_factory.js.map +1 -1
  124. package/dist/storage/storage_service.d.ts.map +1 -1
  125. package/dist/storage/storage_service.js +6 -2
  126. package/dist/storage/storage_service.js.map +1 -1
  127. package/dist/types/http.d.ts +156 -0
  128. package/dist/types/http.d.ts.map +1 -0
  129. package/dist/types/http.js +8 -0
  130. package/dist/types/http.js.map +1 -0
  131. package/dist/utils/graceful_shutdown.d.ts +44 -0
  132. package/dist/utils/graceful_shutdown.d.ts.map +1 -0
  133. package/dist/utils/graceful_shutdown.js +79 -0
  134. package/dist/utils/graceful_shutdown.js.map +1 -0
  135. package/dist/utils/http_responses.d.ts +20 -0
  136. package/dist/utils/http_responses.d.ts.map +1 -1
  137. package/dist/utils/http_responses.js +28 -2
  138. package/dist/utils/http_responses.js.map +1 -1
  139. package/dist/utils/logger.d.ts +8 -8
  140. package/dist/utils/logger.d.ts.map +1 -1
  141. package/dist/utils/logger.js +8 -8
  142. package/dist/utils/logger.js.map +1 -1
  143. package/dist/utils/safe_async.d.ts +50 -0
  144. package/dist/utils/safe_async.d.ts.map +1 -0
  145. package/dist/utils/safe_async.js +90 -0
  146. package/dist/utils/safe_async.js.map +1 -0
  147. package/dist/validation/index.d.ts +3 -0
  148. package/dist/validation/index.d.ts.map +1 -0
  149. package/dist/validation/index.js +7 -0
  150. package/dist/validation/index.js.map +1 -0
  151. package/dist/validation/schemas.d.ts +273 -0
  152. package/dist/validation/schemas.d.ts.map +1 -0
  153. package/dist/validation/schemas.js +82 -0
  154. package/dist/validation/schemas.js.map +1 -0
  155. package/dist/validation/validate.d.ts +49 -0
  156. package/dist/validation/validate.d.ts.map +1 -0
  157. package/dist/validation/validate.js +110 -0
  158. package/dist/validation/validate.js.map +1 -0
  159. package/package.json +14 -8
@@ -2,14 +2,18 @@ import express from 'ultimate-express';
2
2
  import multer from 'multer';
3
3
  import fs from 'fs/promises';
4
4
  import cors from 'cors';
5
+ import compression from 'compression';
5
6
  import { initializeComponents, initializeAssetsManagers } from './initializer.js';
7
+ import { detectComponentType, isCollector, isHarvester, isHandler, isAssetsManager, isCustomTableManager } from './component_types.js';
6
8
  import { UserService } from '../auth/user_service.js';
7
9
  import { exposeEndpoints } from './endpoints.js';
8
10
  import { scheduleComponents } from './scheduler.js';
9
11
  import { LogLevel } from '../utils/logger.js';
10
12
  import { QueueManager } from './queue_manager.js';
11
13
  import { UploadProcessor } from './upload_processor.js';
14
+ import { engineEventBus } from './events.js';
12
15
  import { isAsyncUploadable } from '../components/async_upload.js';
16
+ import { HealthChecker, createDatabaseCheck, createRedisCheck, createStorageCheck, livenessCheck } from './health.js';
13
17
  /**
14
18
  * Digital Twin Engine - Core orchestrator for collectors, harvesters, and handlers
15
19
  *
@@ -52,18 +56,47 @@ export class DigitalTwinEngine {
52
56
  /** uWebSockets.js TemplatedApp - has close() method to shut down all connections */
53
57
  #server;
54
58
  #workers = [];
59
+ #isShuttingDown = false;
60
+ #shutdownTimeout = 30000;
61
+ #healthChecker = new HealthChecker();
62
+ // Mutable arrays for dynamically registered components
63
+ #dynamicCollectors = [];
64
+ #dynamicHarvesters = [];
65
+ #dynamicHandlers = [];
66
+ #dynamicAssetsManagers = [];
67
+ #dynamicCustomTableManagers = [];
68
+ /** Get all collectors (from constructor + register()) */
69
+ get #allCollectors() {
70
+ return [...this.#collectors, ...this.#dynamicCollectors];
71
+ }
72
+ /** Get all harvesters (from constructor + register()) */
73
+ get #allHarvesters() {
74
+ return [...this.#harvesters, ...this.#dynamicHarvesters];
75
+ }
76
+ /** Get all handlers (from constructor + register()) */
77
+ get #allHandlers() {
78
+ return [...this.#handlers, ...this.#dynamicHandlers];
79
+ }
80
+ /** Get all assets managers (from constructor + register()) */
81
+ get #allAssetsManagers() {
82
+ return [...this.#assetsManagers, ...this.#dynamicAssetsManagers];
83
+ }
84
+ /** Get all custom table managers (from constructor + register()) */
85
+ get #allCustomTableManagers() {
86
+ return [...this.#customTableManagers, ...this.#dynamicCustomTableManagers];
87
+ }
55
88
  /** Get all active components (collectors and harvesters) */
56
89
  get #activeComponents() {
57
- return [...this.#collectors, ...this.#harvesters];
90
+ return [...this.#allCollectors, ...this.#allHarvesters];
58
91
  }
59
92
  /** Get all components (collectors + harvesters + handlers + assetsManagers + customTableManagers) */
60
93
  get #allComponents() {
61
94
  return [
62
- ...this.#collectors,
63
- ...this.#harvesters,
64
- ...this.#handlers,
65
- ...this.#assetsManagers,
66
- ...this.#customTableManagers
95
+ ...this.#allCollectors,
96
+ ...this.#allHarvesters,
97
+ ...this.#allHandlers,
98
+ ...this.#allAssetsManagers,
99
+ ...this.#allCustomTableManagers
67
100
  ];
68
101
  }
69
102
  /** Check if multi-queue mode is enabled */
@@ -139,6 +172,8 @@ export class DigitalTwinEngine {
139
172
  }
140
173
  #createQueueManager() {
141
174
  // Create queue manager if we have collectors, harvesters, OR assets managers that may need async uploads
175
+ // Note: At construction time, only constructor-provided components are available
176
+ // Dynamic components registered via register() will be handled at start() time
142
177
  const hasActiveComponents = this.#collectors.length > 0 || this.#harvesters.length > 0;
143
178
  const hasAssetsManagers = this.#assetsManagers.length > 0;
144
179
  if (!hasActiveComponents && !hasAssetsManagers) {
@@ -181,20 +216,32 @@ export class DigitalTwinEngine {
181
216
  * @private
182
217
  */
183
218
  #setupMonitoringEndpoints() {
184
- // Health check endpoint
219
+ // Register default health checks
220
+ this.#healthChecker.registerCheck('database', createDatabaseCheck(this.#database));
221
+ if (this.#queueManager) {
222
+ this.#healthChecker.registerCheck('redis', createRedisCheck(this.#queueManager));
223
+ }
224
+ this.#healthChecker.registerCheck('storage', createStorageCheck(this.#storage));
225
+ // Set component counts (includes both constructor and dynamically registered components)
226
+ this.#healthChecker.setComponentCounts({
227
+ collectors: this.#allCollectors.length,
228
+ harvesters: this.#allHarvesters.length,
229
+ handlers: this.#allHandlers.length,
230
+ assetsManagers: this.#allAssetsManagers.length
231
+ });
232
+ // Liveness probe - shallow check, always returns ok if process is running
233
+ this.#router.get('/api/health/live', (req, res) => {
234
+ res.status(200).json(livenessCheck());
235
+ });
236
+ // Readiness probe - deep check with database and redis verification
237
+ this.#router.get('/api/health/ready', async (req, res) => {
238
+ const health = await this.#healthChecker.performCheck();
239
+ const statusCode = health.status === 'unhealthy' ? 503 : 200;
240
+ res.status(statusCode).json(health);
241
+ });
242
+ // Full health check endpoint (detailed)
185
243
  this.#router.get('/api/health', async (req, res) => {
186
- const health = {
187
- status: 'ok',
188
- timestamp: new Date().toISOString(),
189
- uptime: process.uptime(),
190
- components: {
191
- collectors: this.#collectors.length,
192
- harvesters: this.#harvesters.length,
193
- handlers: this.#handlers.length,
194
- assetsManagers: this.#assetsManagers.length,
195
- customTableManagers: this.#customTableManagers.length
196
- }
197
- };
244
+ const health = await this.#healthChecker.performCheck();
198
245
  res.json(health);
199
246
  });
200
247
  // Queue statistics endpoint
@@ -263,8 +310,7 @@ export class DigitalTwinEngine {
263
310
  }
264
311
  // Inject upload queue to components that support async uploads
265
312
  if (this.#queueManager) {
266
- const allManagers = [...this.#assetsManagers];
267
- for (const manager of allManagers) {
313
+ for (const manager of this.#allAssetsManagers) {
268
314
  if (isAsyncUploadable(manager)) {
269
315
  manager.setUploadQueue(this.#queueManager.uploadQueue);
270
316
  }
@@ -288,6 +334,24 @@ export class DigitalTwinEngine {
288
334
  this.#setupMonitoringEndpoints();
289
335
  // Ensure temporary upload directory exists
290
336
  await this.#ensureTempUploadDir();
337
+ // HTTP compression - disabled by default as API gateways (APISIX, Kong, etc.) typically handle this
338
+ // Enable with DIGITALTWIN_ENABLE_COMPRESSION=true for standalone deployments without a gateway
339
+ const enableCompression = process.env.DIGITALTWIN_ENABLE_COMPRESSION === 'true';
340
+ if (enableCompression) {
341
+ const compressionMiddleware = compression({
342
+ filter: (req, res) => {
343
+ // Don't compress binary streams
344
+ if (req.headers['accept']?.includes('application/octet-stream')) {
345
+ return false;
346
+ }
347
+ // Use default filter for other content types
348
+ return compression.filter(req, res);
349
+ },
350
+ level: 6, // Balance between speed and compression ratio
351
+ threshold: 1024 // Only compress responses larger than 1KB
352
+ });
353
+ this.#app.use(compressionMiddleware);
354
+ }
291
355
  // Enable CORS for cross-origin requests from frontend applications
292
356
  this.#app.use(cors({
293
357
  origin: process.env.CORS_ORIGIN || true, // Allow all origins by default, configure in production
@@ -361,14 +425,219 @@ export class DigitalTwinEngine {
361
425
  return app.port ?? this.#options.server?.port;
362
426
  }
363
427
  /**
364
- * Stops the Digital Twin Engine gracefully
428
+ * Registers a single component with automatic type detection.
429
+ *
430
+ * The engine automatically detects the component type based on its class
431
+ * and adds it to the appropriate internal collection.
432
+ *
433
+ * @param component - Component instance to register
434
+ * @returns The engine instance for method chaining
435
+ * @throws Error if component type cannot be determined or is already registered
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * const engine = new DigitalTwinEngine({ storage, database })
440
+ *
441
+ * engine
442
+ * .register(new WeatherCollector())
443
+ * .register(new TrafficAnalysisHarvester())
444
+ * .register(new ApiHandler())
445
+ * .register(new GLTFAssetsManager())
446
+ *
447
+ * await engine.start()
448
+ * ```
449
+ */
450
+ register(component) {
451
+ const type = detectComponentType(component);
452
+ const config = component.getConfiguration();
453
+ // Check for duplicate registration
454
+ if (this.#isComponentRegistered(config.name, type)) {
455
+ throw new Error(`Component "${config.name}" of type "${type}" is already registered. ` +
456
+ 'Each component must have a unique name within its type.');
457
+ }
458
+ switch (type) {
459
+ case 'collector':
460
+ if (isCollector(component)) {
461
+ this.#dynamicCollectors.push(component);
462
+ }
463
+ break;
464
+ case 'harvester':
465
+ if (isHarvester(component)) {
466
+ this.#dynamicHarvesters.push(component);
467
+ }
468
+ break;
469
+ case 'handler':
470
+ if (isHandler(component)) {
471
+ this.#dynamicHandlers.push(component);
472
+ }
473
+ break;
474
+ case 'assets_manager':
475
+ if (isAssetsManager(component)) {
476
+ this.#dynamicAssetsManagers.push(component);
477
+ }
478
+ break;
479
+ case 'custom_table_manager':
480
+ if (isCustomTableManager(component)) {
481
+ this.#dynamicCustomTableManagers.push(component);
482
+ }
483
+ break;
484
+ }
485
+ return this;
486
+ }
487
+ /**
488
+ * Registers multiple components at once with automatic type detection.
489
+ *
490
+ * Useful for registering all components from a module or when loading
491
+ * components dynamically.
492
+ *
493
+ * @param components - Array of component instances to register
494
+ * @returns The engine instance for method chaining
495
+ * @throws Error if any component type cannot be determined or is duplicate
496
+ *
497
+ * @example
498
+ * ```typescript
499
+ * const engine = new DigitalTwinEngine({ storage, database })
500
+ *
501
+ * engine.registerAll([
502
+ * new WeatherCollector(),
503
+ * new TrafficCollector(),
504
+ * new AnalysisHarvester(),
505
+ * new ApiHandler()
506
+ * ])
507
+ *
508
+ * await engine.start()
509
+ * ```
510
+ */
511
+ registerAll(components) {
512
+ for (const component of components) {
513
+ this.register(component);
514
+ }
515
+ return this;
516
+ }
517
+ /**
518
+ * Registers components with explicit type specification.
519
+ *
520
+ * Provides full type safety at compile time. Use this method when you
521
+ * have pre-sorted components from auto-discovery or want explicit control.
522
+ *
523
+ * @param components - Object with typed component arrays
524
+ * @returns The engine instance for method chaining
525
+ *
526
+ * @example
527
+ * ```typescript
528
+ * const loaded = await loadComponents('./src/components')
529
+ *
530
+ * engine.registerComponents({
531
+ * collectors: loaded.collectors,
532
+ * harvesters: loaded.harvesters,
533
+ * handlers: loaded.handlers,
534
+ * assetsManagers: loaded.assetsManagers,
535
+ * customTableManagers: loaded.customTableManagers
536
+ * })
537
+ * ```
538
+ */
539
+ registerComponents(components) {
540
+ if (components.collectors) {
541
+ this.#dynamicCollectors.push(...components.collectors);
542
+ }
543
+ if (components.harvesters) {
544
+ this.#dynamicHarvesters.push(...components.harvesters);
545
+ }
546
+ if (components.handlers) {
547
+ this.#dynamicHandlers.push(...components.handlers);
548
+ }
549
+ if (components.assetsManagers) {
550
+ this.#dynamicAssetsManagers.push(...components.assetsManagers);
551
+ }
552
+ if (components.customTableManagers) {
553
+ this.#dynamicCustomTableManagers.push(...components.customTableManagers);
554
+ }
555
+ return this;
556
+ }
557
+ /**
558
+ * Checks if a component with the given name is already registered.
559
+ */
560
+ #isComponentRegistered(name, type) {
561
+ const allComponents = this.#getAllComponentsOfType(type);
562
+ return allComponents.some(c => c.getConfiguration().name === name);
563
+ }
564
+ /**
565
+ * Gets all components of a specific type (both from constructor and register()).
566
+ */
567
+ #getAllComponentsOfType(type) {
568
+ switch (type) {
569
+ case 'collector':
570
+ return this.#allCollectors;
571
+ case 'harvester':
572
+ return this.#allHarvesters;
573
+ case 'handler':
574
+ return this.#allHandlers;
575
+ case 'assets_manager':
576
+ return this.#allAssetsManagers;
577
+ case 'custom_table_manager':
578
+ return this.#allCustomTableManagers;
579
+ }
580
+ }
581
+ /**
582
+ * Configure the shutdown timeout (in ms)
583
+ * @param timeout Timeout in milliseconds (default: 30000)
584
+ */
585
+ setShutdownTimeout(timeout) {
586
+ this.#shutdownTimeout = timeout;
587
+ }
588
+ /**
589
+ * Check if the engine is currently shutting down
590
+ * @returns true if shutdown is in progress
591
+ */
592
+ isShuttingDown() {
593
+ return this.#isShuttingDown;
594
+ }
595
+ /**
596
+ * Register a custom health check
597
+ * @param name Unique name for the check
598
+ * @param checkFn Function that performs the check
599
+ *
600
+ * @example
601
+ * ```typescript
602
+ * engine.registerHealthCheck('external-api', async () => {
603
+ * try {
604
+ * const res = await fetch('https://api.example.com/health')
605
+ * return { status: res.ok ? 'up' : 'down' }
606
+ * } catch (error) {
607
+ * return { status: 'down', error: error.message }
608
+ * }
609
+ * })
610
+ * ```
611
+ */
612
+ registerHealthCheck(name, checkFn) {
613
+ this.#healthChecker.registerCheck(name, checkFn);
614
+ }
615
+ /**
616
+ * Remove a custom health check
617
+ * @param name Name of the check to remove
618
+ * @returns true if the check was removed, false if it didn't exist
619
+ */
620
+ removeHealthCheck(name) {
621
+ return this.#healthChecker.removeCheck(name);
622
+ }
623
+ /**
624
+ * Get list of registered health check names
625
+ */
626
+ getHealthCheckNames() {
627
+ return this.#healthChecker.getCheckNames();
628
+ }
629
+ /**
630
+ * Stops the Digital Twin Engine with graceful shutdown
365
631
  *
366
632
  * This method:
367
- * 1. Closes HTTP server
368
- * 2. Stops background workers
369
- * 3. Closes all queue connections
370
- * 4. Closes database connections
371
- * 5. Clean up resources
633
+ * 1. Prevents new work from being accepted
634
+ * 2. Removes all event listeners
635
+ * 3. Closes HTTP server
636
+ * 4. Drains queues and waits for active jobs
637
+ * 5. Closes all queue workers
638
+ * 6. Stops upload processor
639
+ * 7. Closes queue manager
640
+ * 8. Closes database connections
372
641
  *
373
642
  * @async
374
643
  * @returns {Promise<void>}
@@ -380,66 +649,125 @@ export class DigitalTwinEngine {
380
649
  * ```
381
650
  */
382
651
  async stop() {
652
+ if (this.#isShuttingDown) {
653
+ if (process.env.NODE_ENV !== 'test') {
654
+ console.warn('[DigitalTwin] Shutdown already in progress');
655
+ }
656
+ return;
657
+ }
658
+ this.#isShuttingDown = true;
659
+ const startTime = Date.now();
660
+ if (process.env.NODE_ENV !== 'test') {
661
+ console.log('[DigitalTwin] Graceful shutdown initiated...');
662
+ }
383
663
  const errors = [];
384
- // 1. Close HTTP server first (uWebSockets.js TemplatedApp.close() is synchronous)
664
+ // 1. Remove all event listeners to prevent new work
665
+ this.#cleanupEventListeners();
666
+ // 2. Close HTTP server (uWebSockets.js TemplatedApp.close() is synchronous)
385
667
  if (this.#server) {
386
668
  try {
387
669
  this.#server.close();
388
670
  }
389
671
  catch (error) {
390
- errors.push(new Error(`Server close error: ${error instanceof Error ? error.message : String(error)}`));
672
+ errors.push(this.#wrapError('Server close', error));
391
673
  }
392
674
  this.#server = undefined;
393
675
  }
394
- // 2. Close all workers with extended timeout and force close
395
- if (this.#workers.length > 0) {
396
- await Promise.all(this.#workers.map(async (worker) => {
397
- try {
398
- await Promise.race([
399
- worker.close(),
400
- new Promise((_, reject) => setTimeout(() => reject(new Error('Worker close timeout')), 5000))
401
- ]);
402
- }
403
- catch {
404
- // Force close if timeout or error
405
- try {
406
- await worker.disconnect();
407
- }
408
- catch (disconnectError) {
409
- errors.push(new Error(`Worker force close error: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`));
410
- }
411
- }
412
- }));
676
+ // 3. Drain queues - wait for active jobs with timeout
677
+ if (this.#queueManager) {
678
+ try {
679
+ await this.#drainQueues();
680
+ }
681
+ catch (error) {
682
+ errors.push(this.#wrapError('Queue drain', error));
683
+ }
413
684
  }
414
- // 3. Stop upload processor worker
685
+ // 4. Close all workers with extended timeout and force close
686
+ await this.#closeWorkers(errors);
687
+ // 5. Stop upload processor worker
415
688
  if (this.#uploadProcessor) {
416
689
  try {
417
690
  await this.#uploadProcessor.stop();
418
691
  }
419
692
  catch (error) {
420
- errors.push(new Error(`Upload processor close error: ${error instanceof Error ? error.message : String(error)}`));
693
+ errors.push(this.#wrapError('Upload processor', error));
421
694
  }
422
695
  }
423
- // 4. Close queue connections (only if we have a queue manager)
696
+ // 6. Close queue connections (only if we have a queue manager)
424
697
  if (this.#queueManager) {
425
698
  try {
426
699
  await this.#queueManager.close();
427
700
  }
428
701
  catch (error) {
429
- errors.push(new Error(`Queue manager close error: ${error instanceof Error ? error.message : String(error)}`));
702
+ errors.push(this.#wrapError('Queue manager', error));
430
703
  }
431
704
  }
432
- // 5. Close database connections
705
+ // 7. Close database connections
433
706
  try {
434
707
  await this.#database.close();
435
708
  }
436
709
  catch (error) {
437
- errors.push(new Error(`Database close error: ${error instanceof Error ? error.message : String(error)}`));
710
+ errors.push(this.#wrapError('Database', error));
438
711
  }
439
- if (errors.length > 0 && process.env.NODE_ENV !== 'test') {
440
- console.warn('[DigitalTwin] Stopped with warnings:', errors.map(e => e.message).join(', '));
712
+ const duration = Date.now() - startTime;
713
+ if (process.env.NODE_ENV !== 'test') {
714
+ if (errors.length > 0) {
715
+ console.error(`[DigitalTwin] Shutdown completed with ${errors.length} errors in ${duration}ms:`, errors.map(e => e.message).join(', '));
716
+ }
717
+ else {
718
+ console.log(`[DigitalTwin] Shutdown completed successfully in ${duration}ms`);
719
+ }
720
+ }
721
+ }
722
+ #cleanupEventListeners() {
723
+ engineEventBus.removeAllListeners();
724
+ }
725
+ async #drainQueues() {
726
+ if (!this.#queueManager)
727
+ return;
728
+ const timeout = Math.min(this.#shutdownTimeout / 2, 15000);
729
+ const startTime = Date.now();
730
+ while (Date.now() - startTime < timeout) {
731
+ try {
732
+ const stats = await this.#queueManager.getQueueStats();
733
+ const totalActive = Object.values(stats).reduce((sum, q) => sum + (q.active || 0), 0);
734
+ if (totalActive === 0)
735
+ break;
736
+ if (process.env.NODE_ENV !== 'test') {
737
+ console.log(`[DigitalTwin] Waiting for ${totalActive} active jobs...`);
738
+ }
739
+ await new Promise(resolve => setTimeout(resolve, 1000));
740
+ }
741
+ catch {
742
+ await new Promise(resolve => setTimeout(resolve, 1000));
743
+ break;
744
+ }
441
745
  }
442
746
  }
747
+ async #closeWorkers(errors) {
748
+ const workerTimeout = Math.min(this.#shutdownTimeout / 3, 10000);
749
+ await Promise.all(this.#workers.map(async (worker) => {
750
+ try {
751
+ await Promise.race([
752
+ worker.close(),
753
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Worker close timeout')), workerTimeout))
754
+ ]);
755
+ }
756
+ catch {
757
+ try {
758
+ await worker.disconnect();
759
+ }
760
+ catch (disconnectError) {
761
+ errors.push(this.#wrapError('Worker disconnect', disconnectError));
762
+ }
763
+ }
764
+ }));
765
+ this.#workers = [];
766
+ }
767
+ #wrapError(context, error) {
768
+ const message = error instanceof Error ? error.message : String(error);
769
+ return new Error(`${context}: ${message}`);
770
+ }
443
771
  /**
444
772
  * Validate the engine configuration and all components
445
773
  *
@@ -459,24 +787,24 @@ export class DigitalTwinEngine {
459
787
  async validateConfiguration() {
460
788
  const componentResults = [];
461
789
  const engineErrors = [];
462
- // Validate collectors
463
- for (const collector of this.#collectors) {
790
+ // Validate collectors (includes dynamically registered)
791
+ for (const collector of this.#allCollectors) {
464
792
  componentResults.push(await this.#validateComponent(collector, 'collector'));
465
793
  }
466
- // Validate harvesters
467
- for (const harvester of this.#harvesters) {
794
+ // Validate harvesters (includes dynamically registered)
795
+ for (const harvester of this.#allHarvesters) {
468
796
  componentResults.push(await this.#validateComponent(harvester, 'harvester'));
469
797
  }
470
- // Validate handlers
471
- for (const handler of this.#handlers) {
798
+ // Validate handlers (includes dynamically registered)
799
+ for (const handler of this.#allHandlers) {
472
800
  componentResults.push(await this.#validateComponent(handler, 'handler'));
473
801
  }
474
- // Validate assets managers
475
- for (const assetsManager of this.#assetsManagers) {
802
+ // Validate assets managers (includes dynamically registered)
803
+ for (const assetsManager of this.#allAssetsManagers) {
476
804
  componentResults.push(await this.#validateComponent(assetsManager, 'assets_manager'));
477
805
  }
478
- // Validate store managers
479
- for (const customTableManager of this.#customTableManagers) {
806
+ // Validate custom table managers (includes dynamically registered)
807
+ for (const customTableManager of this.#allCustomTableManagers) {
480
808
  componentResults.push(await this.#validateComponent(customTableManager, 'custom_table_manager'));
481
809
  }
482
810
  // Validate engine-level configuration
@@ -536,23 +864,23 @@ export class DigitalTwinEngine {
536
864
  */
537
865
  async testComponents() {
538
866
  const results = [];
539
- // Test collectors
540
- for (const collector of this.#collectors) {
867
+ // Test collectors (includes dynamically registered)
868
+ for (const collector of this.#allCollectors) {
541
869
  const result = await this.#testCollector(collector);
542
870
  results.push(result);
543
871
  }
544
- // Test harvesters
545
- for (const harvester of this.#harvesters) {
872
+ // Test harvesters (includes dynamically registered)
873
+ for (const harvester of this.#allHarvesters) {
546
874
  const result = await this.#testHarvester(harvester);
547
875
  results.push(result);
548
876
  }
549
- // Test handlers
550
- for (const handler of this.#handlers) {
877
+ // Test handlers (includes dynamically registered)
878
+ for (const handler of this.#allHandlers) {
551
879
  const result = await this.#testHandler(handler);
552
880
  results.push(result);
553
881
  }
554
- // Test assets managers
555
- for (const assetsManager of this.#assetsManagers) {
882
+ // Test assets managers (includes dynamically registered)
883
+ for (const assetsManager of this.#allAssetsManagers) {
556
884
  const result = await this.#testAssetsManager(assetsManager);
557
885
  results.push(result);
558
886
  }