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.
- package/README.md +218 -1
- package/dist/auth/apisix_parser.d.ts +56 -56
- package/dist/auth/apisix_parser.d.ts.map +1 -1
- package/dist/auth/apisix_parser.js +72 -86
- package/dist/auth/apisix_parser.js.map +1 -1
- package/dist/auth/auth_provider.d.ts +118 -0
- package/dist/auth/auth_provider.d.ts.map +1 -0
- package/dist/auth/auth_provider.js +8 -0
- package/dist/auth/auth_provider.js.map +1 -0
- package/dist/auth/auth_provider_factory.d.ts +91 -0
- package/dist/auth/auth_provider_factory.d.ts.map +1 -0
- package/dist/auth/auth_provider_factory.js +146 -0
- package/dist/auth/auth_provider_factory.js.map +1 -0
- package/dist/auth/index.d.ts +4 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/providers/gateway_auth_provider.d.ts +78 -0
- package/dist/auth/providers/gateway_auth_provider.d.ts.map +1 -0
- package/dist/auth/providers/gateway_auth_provider.js +109 -0
- package/dist/auth/providers/gateway_auth_provider.js.map +1 -0
- package/dist/auth/providers/index.d.ts +4 -0
- package/dist/auth/providers/index.d.ts.map +1 -0
- package/dist/auth/providers/index.js +4 -0
- package/dist/auth/providers/index.js.map +1 -0
- package/dist/auth/providers/jwt_auth_provider.d.ts +91 -0
- package/dist/auth/providers/jwt_auth_provider.d.ts.map +1 -0
- package/dist/auth/providers/jwt_auth_provider.js +204 -0
- package/dist/auth/providers/jwt_auth_provider.js.map +1 -0
- package/dist/auth/providers/no_auth_provider.d.ts +61 -0
- package/dist/auth/providers/no_auth_provider.d.ts.map +1 -0
- package/dist/auth/providers/no_auth_provider.js +76 -0
- package/dist/auth/providers/no_auth_provider.js.map +1 -0
- package/dist/auth/types.d.ts +5 -3
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/components/assets_manager.d.ts +1 -1
- package/dist/components/assets_manager.d.ts.map +1 -1
- package/dist/components/assets_manager.js +54 -48
- package/dist/components/assets_manager.js.map +1 -1
- package/dist/components/collector.d.ts.map +1 -1
- package/dist/components/collector.js +30 -18
- package/dist/components/collector.js.map +1 -1
- package/dist/components/custom_table_manager.d.ts.map +1 -1
- package/dist/components/custom_table_manager.js +36 -65
- package/dist/components/custom_table_manager.js.map +1 -1
- package/dist/components/global_assets_handler.d.ts +4 -2
- package/dist/components/global_assets_handler.d.ts.map +1 -1
- package/dist/components/global_assets_handler.js.map +1 -1
- package/dist/components/harvester.d.ts.map +1 -1
- package/dist/components/harvester.js +46 -33
- package/dist/components/harvester.js.map +1 -1
- package/dist/components/interfaces.d.ts +3 -2
- package/dist/components/interfaces.d.ts.map +1 -1
- package/dist/components/map_manager.d.ts.map +1 -1
- package/dist/components/map_manager.js.map +1 -1
- package/dist/components/tileset_manager.d.ts +2 -1
- package/dist/components/tileset_manager.d.ts.map +1 -1
- package/dist/components/tileset_manager.js +20 -15
- package/dist/components/tileset_manager.js.map +1 -1
- package/dist/database/adapters/knex_database_adapter.d.ts +6 -1
- package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -1
- package/dist/database/adapters/knex_database_adapter.js +118 -36
- package/dist/database/adapters/knex_database_adapter.js.map +1 -1
- package/dist/database/database_adapter.d.ts +13 -1
- package/dist/database/database_adapter.d.ts.map +1 -1
- package/dist/database/database_adapter.js.map +1 -1
- package/dist/engine/component_types.d.ts +95 -0
- package/dist/engine/component_types.d.ts.map +1 -0
- package/dist/engine/component_types.js +93 -0
- package/dist/engine/component_types.js.map +1 -0
- package/dist/engine/digital_twin_engine.d.ts +121 -6
- package/dist/engine/digital_twin_engine.d.ts.map +1 -1
- package/dist/engine/digital_twin_engine.js +402 -74
- package/dist/engine/digital_twin_engine.js.map +1 -1
- package/dist/engine/endpoints.d.ts.map +1 -1
- package/dist/engine/endpoints.js +35 -3
- package/dist/engine/endpoints.js.map +1 -1
- package/dist/engine/error_handler.d.ts +20 -0
- package/dist/engine/error_handler.d.ts.map +1 -0
- package/dist/engine/error_handler.js +69 -0
- package/dist/engine/error_handler.js.map +1 -0
- package/dist/engine/events.d.ts +1 -1
- package/dist/engine/events.d.ts.map +1 -1
- package/dist/engine/events.js.map +1 -1
- package/dist/engine/health.d.ts +112 -0
- package/dist/engine/health.d.ts.map +1 -0
- package/dist/engine/health.js +190 -0
- package/dist/engine/health.js.map +1 -0
- package/dist/engine/initializer.d.ts.map +1 -1
- package/dist/engine/initializer.js +6 -4
- package/dist/engine/initializer.js.map +1 -1
- package/dist/engine/scheduler.d.ts.map +1 -1
- package/dist/engine/scheduler.js +17 -9
- package/dist/engine/scheduler.js.map +1 -1
- package/dist/engine/upload_processor.d.ts.map +1 -1
- package/dist/engine/upload_processor.js +24 -12
- package/dist/engine/upload_processor.js.map +1 -1
- package/dist/errors/index.d.ts +94 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +149 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/loader/component_loader.d.ts +128 -0
- package/dist/loader/component_loader.d.ts.map +1 -0
- package/dist/loader/component_loader.js +330 -0
- package/dist/loader/component_loader.js.map +1 -0
- package/dist/loader/index.d.ts +19 -0
- package/dist/loader/index.d.ts.map +1 -0
- package/dist/loader/index.js +19 -0
- package/dist/loader/index.js.map +1 -0
- package/dist/storage/adapters/local_storage_service.d.ts +6 -0
- package/dist/storage/adapters/local_storage_service.d.ts.map +1 -1
- package/dist/storage/adapters/local_storage_service.js +26 -4
- package/dist/storage/adapters/local_storage_service.js.map +1 -1
- package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -1
- package/dist/storage/adapters/ovh_storage_service.js +5 -6
- package/dist/storage/adapters/ovh_storage_service.js.map +1 -1
- package/dist/storage/storage_factory.d.ts.map +1 -1
- package/dist/storage/storage_factory.js +4 -1
- package/dist/storage/storage_factory.js.map +1 -1
- package/dist/storage/storage_service.d.ts.map +1 -1
- package/dist/storage/storage_service.js +6 -2
- package/dist/storage/storage_service.js.map +1 -1
- package/dist/types/http.d.ts +156 -0
- package/dist/types/http.d.ts.map +1 -0
- package/dist/types/http.js +8 -0
- package/dist/types/http.js.map +1 -0
- package/dist/utils/graceful_shutdown.d.ts +44 -0
- package/dist/utils/graceful_shutdown.d.ts.map +1 -0
- package/dist/utils/graceful_shutdown.js +79 -0
- package/dist/utils/graceful_shutdown.js.map +1 -0
- package/dist/utils/http_responses.d.ts +20 -0
- package/dist/utils/http_responses.d.ts.map +1 -1
- package/dist/utils/http_responses.js +28 -2
- package/dist/utils/http_responses.js.map +1 -1
- package/dist/utils/logger.d.ts +8 -8
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +8 -8
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/safe_async.d.ts +50 -0
- package/dist/utils/safe_async.d.ts.map +1 -0
- package/dist/utils/safe_async.js +90 -0
- package/dist/utils/safe_async.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +7 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/schemas.d.ts +273 -0
- package/dist/validation/schemas.d.ts.map +1 -0
- package/dist/validation/schemas.js +82 -0
- package/dist/validation/schemas.js.map +1 -0
- package/dist/validation/validate.d.ts +49 -0
- package/dist/validation/validate.d.ts.map +1 -0
- package/dist/validation/validate.js +110 -0
- package/dist/validation/validate.js.map +1 -0
- 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.#
|
|
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.#
|
|
63
|
-
...this.#
|
|
64
|
-
...this.#
|
|
65
|
-
...this.#
|
|
66
|
-
...this.#
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
368
|
-
* 2.
|
|
369
|
-
* 3. Closes
|
|
370
|
-
* 4.
|
|
371
|
-
* 5.
|
|
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.
|
|
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(
|
|
672
|
+
errors.push(this.#wrapError('Server close', error));
|
|
391
673
|
}
|
|
392
674
|
this.#server = undefined;
|
|
393
675
|
}
|
|
394
|
-
//
|
|
395
|
-
if (this.#
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
//
|
|
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(
|
|
693
|
+
errors.push(this.#wrapError('Upload processor', error));
|
|
421
694
|
}
|
|
422
695
|
}
|
|
423
|
-
//
|
|
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(
|
|
702
|
+
errors.push(this.#wrapError('Queue manager', error));
|
|
430
703
|
}
|
|
431
704
|
}
|
|
432
|
-
//
|
|
705
|
+
// 7. Close database connections
|
|
433
706
|
try {
|
|
434
707
|
await this.#database.close();
|
|
435
708
|
}
|
|
436
709
|
catch (error) {
|
|
437
|
-
errors.push(
|
|
710
|
+
errors.push(this.#wrapError('Database', error));
|
|
438
711
|
}
|
|
439
|
-
|
|
440
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
|
479
|
-
for (const customTableManager of this.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
}
|