digitaltwin-core 0.14.2 → 1.0.0

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 (227) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +494 -359
  3. package/dist/auth/apisix_parser.d.ts +141 -0
  4. package/dist/auth/apisix_parser.d.ts.map +1 -0
  5. package/dist/auth/apisix_parser.js +161 -0
  6. package/dist/auth/apisix_parser.js.map +1 -0
  7. package/dist/auth/auth_config.d.ts +126 -0
  8. package/dist/auth/auth_config.d.ts.map +1 -0
  9. package/dist/auth/auth_config.js +169 -0
  10. package/dist/auth/auth_config.js.map +1 -0
  11. package/dist/auth/auth_provider.d.ts +118 -0
  12. package/dist/auth/auth_provider.d.ts.map +1 -0
  13. package/dist/auth/auth_provider.js +8 -0
  14. package/dist/auth/auth_provider.js.map +1 -0
  15. package/dist/auth/auth_provider_factory.d.ts +91 -0
  16. package/dist/auth/auth_provider_factory.d.ts.map +1 -0
  17. package/dist/auth/auth_provider_factory.js +146 -0
  18. package/dist/auth/auth_provider_factory.js.map +1 -0
  19. package/dist/auth/index.d.ts +8 -0
  20. package/dist/auth/index.d.ts.map +1 -0
  21. package/dist/auth/index.js +7 -0
  22. package/dist/auth/index.js.map +1 -0
  23. package/dist/auth/providers/gateway_auth_provider.d.ts +78 -0
  24. package/dist/auth/providers/gateway_auth_provider.d.ts.map +1 -0
  25. package/dist/auth/providers/gateway_auth_provider.js +109 -0
  26. package/dist/auth/providers/gateway_auth_provider.js.map +1 -0
  27. package/dist/auth/providers/index.d.ts +4 -0
  28. package/dist/auth/providers/index.d.ts.map +1 -0
  29. package/dist/auth/providers/index.js +4 -0
  30. package/dist/auth/providers/index.js.map +1 -0
  31. package/dist/auth/providers/jwt_auth_provider.d.ts +91 -0
  32. package/dist/auth/providers/jwt_auth_provider.d.ts.map +1 -0
  33. package/dist/auth/providers/jwt_auth_provider.js +204 -0
  34. package/dist/auth/providers/jwt_auth_provider.js.map +1 -0
  35. package/dist/auth/providers/no_auth_provider.d.ts +61 -0
  36. package/dist/auth/providers/no_auth_provider.d.ts.map +1 -0
  37. package/dist/auth/providers/no_auth_provider.js +76 -0
  38. package/dist/auth/providers/no_auth_provider.js.map +1 -0
  39. package/dist/auth/types.d.ts +100 -0
  40. package/dist/auth/types.d.ts.map +1 -0
  41. package/dist/auth/types.js +2 -0
  42. package/dist/auth/types.js.map +1 -0
  43. package/dist/auth/user_service.d.ts +86 -0
  44. package/dist/auth/user_service.d.ts.map +1 -0
  45. package/dist/auth/user_service.js +237 -0
  46. package/dist/auth/user_service.js.map +1 -0
  47. package/dist/components/assets_manager.d.ts +662 -0
  48. package/dist/components/assets_manager.d.ts.map +1 -0
  49. package/dist/components/assets_manager.js +1537 -0
  50. package/dist/components/assets_manager.js.map +1 -0
  51. package/dist/components/async_upload.d.ts +20 -0
  52. package/dist/components/async_upload.d.ts.map +1 -0
  53. package/dist/components/async_upload.js +10 -0
  54. package/dist/components/async_upload.js.map +1 -0
  55. package/dist/components/collector.d.ts +203 -0
  56. package/dist/components/collector.d.ts.map +1 -0
  57. package/dist/components/collector.js +214 -0
  58. package/dist/components/collector.js.map +1 -0
  59. package/dist/components/custom_table_manager.d.ts +503 -0
  60. package/dist/components/custom_table_manager.d.ts.map +1 -0
  61. package/dist/components/custom_table_manager.js +1023 -0
  62. package/dist/components/custom_table_manager.js.map +1 -0
  63. package/dist/components/global_assets_handler.d.ts +63 -0
  64. package/dist/components/global_assets_handler.d.ts.map +1 -0
  65. package/dist/components/global_assets_handler.js +127 -0
  66. package/dist/components/global_assets_handler.js.map +1 -0
  67. package/dist/components/handler.d.ts +104 -0
  68. package/dist/components/handler.d.ts.map +1 -0
  69. package/dist/components/handler.js +110 -0
  70. package/dist/components/handler.js.map +1 -0
  71. package/dist/components/harvester.d.ts +182 -0
  72. package/dist/components/harvester.d.ts.map +1 -0
  73. package/dist/components/harvester.js +406 -0
  74. package/dist/components/harvester.js.map +1 -0
  75. package/dist/components/index.d.ts +11 -0
  76. package/dist/components/index.d.ts.map +1 -0
  77. package/dist/components/index.js +9 -0
  78. package/dist/components/index.js.map +1 -0
  79. package/dist/components/interfaces.d.ts +126 -0
  80. package/dist/components/interfaces.d.ts.map +1 -0
  81. package/dist/components/interfaces.js +8 -0
  82. package/dist/components/interfaces.js.map +1 -0
  83. package/dist/components/map_manager.d.ts +61 -0
  84. package/dist/components/map_manager.d.ts.map +1 -0
  85. package/dist/components/map_manager.js +242 -0
  86. package/dist/components/map_manager.js.map +1 -0
  87. package/dist/components/tileset_manager.d.ts +125 -0
  88. package/dist/components/tileset_manager.d.ts.map +1 -0
  89. package/dist/components/tileset_manager.js +623 -0
  90. package/dist/components/tileset_manager.js.map +1 -0
  91. package/dist/components/types.d.ts +226 -0
  92. package/dist/components/types.d.ts.map +1 -0
  93. package/dist/components/types.js +8 -0
  94. package/dist/components/types.js.map +1 -0
  95. package/dist/database/adapters/knex_database_adapter.d.ts +97 -0
  96. package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -0
  97. package/dist/database/adapters/knex_database_adapter.js +729 -0
  98. package/dist/database/adapters/knex_database_adapter.js.map +1 -0
  99. package/dist/database/database_adapter.d.ts +262 -0
  100. package/dist/database/database_adapter.d.ts.map +1 -0
  101. package/dist/database/database_adapter.js +46 -0
  102. package/dist/database/database_adapter.js.map +1 -0
  103. package/dist/engine/digital_twin_engine.d.ts +295 -0
  104. package/dist/engine/digital_twin_engine.d.ts.map +1 -0
  105. package/dist/engine/digital_twin_engine.js +907 -0
  106. package/dist/engine/digital_twin_engine.js.map +1 -0
  107. package/dist/engine/endpoints.d.ts +47 -0
  108. package/dist/engine/endpoints.d.ts.map +1 -0
  109. package/dist/engine/endpoints.js +88 -0
  110. package/dist/engine/endpoints.js.map +1 -0
  111. package/dist/engine/error_handler.d.ts +20 -0
  112. package/dist/engine/error_handler.d.ts.map +1 -0
  113. package/dist/engine/error_handler.js +69 -0
  114. package/dist/engine/error_handler.js.map +1 -0
  115. package/dist/engine/events.d.ts +93 -0
  116. package/dist/engine/events.d.ts.map +1 -0
  117. package/dist/engine/events.js +71 -0
  118. package/dist/engine/events.js.map +1 -0
  119. package/dist/engine/health.d.ts +112 -0
  120. package/dist/engine/health.d.ts.map +1 -0
  121. package/dist/engine/health.js +190 -0
  122. package/dist/engine/health.js.map +1 -0
  123. package/dist/engine/initializer.d.ts +62 -0
  124. package/dist/engine/initializer.d.ts.map +1 -0
  125. package/dist/engine/initializer.js +108 -0
  126. package/dist/engine/initializer.js.map +1 -0
  127. package/dist/engine/queue_manager.d.ts +87 -0
  128. package/dist/engine/queue_manager.d.ts.map +1 -0
  129. package/dist/engine/queue_manager.js +196 -0
  130. package/dist/engine/queue_manager.js.map +1 -0
  131. package/dist/engine/scheduler.d.ts +30 -0
  132. package/dist/engine/scheduler.d.ts.map +1 -0
  133. package/dist/engine/scheduler.js +378 -0
  134. package/dist/engine/scheduler.js.map +1 -0
  135. package/dist/engine/upload_processor.d.ts +36 -0
  136. package/dist/engine/upload_processor.d.ts.map +1 -0
  137. package/dist/engine/upload_processor.js +113 -0
  138. package/dist/engine/upload_processor.js.map +1 -0
  139. package/dist/env/env.d.ts +134 -0
  140. package/dist/env/env.d.ts.map +1 -0
  141. package/dist/env/env.js +177 -0
  142. package/dist/env/env.js.map +1 -0
  143. package/dist/errors/index.d.ts +94 -0
  144. package/dist/errors/index.d.ts.map +1 -0
  145. package/dist/errors/index.js +149 -0
  146. package/dist/errors/index.js.map +1 -0
  147. package/dist/index.d.ts +55 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +65 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/openapi/generator.d.ts +93 -0
  152. package/dist/openapi/generator.d.ts.map +1 -0
  153. package/dist/openapi/generator.js +293 -0
  154. package/dist/openapi/generator.js.map +1 -0
  155. package/dist/openapi/index.d.ts +9 -0
  156. package/dist/openapi/index.d.ts.map +1 -0
  157. package/dist/openapi/index.js +9 -0
  158. package/dist/openapi/index.js.map +1 -0
  159. package/dist/openapi/types.d.ts +182 -0
  160. package/dist/openapi/types.d.ts.map +1 -0
  161. package/dist/openapi/types.js +16 -0
  162. package/dist/openapi/types.js.map +1 -0
  163. package/dist/storage/adapters/local_storage_service.d.ts +57 -0
  164. package/dist/storage/adapters/local_storage_service.d.ts.map +1 -0
  165. package/dist/storage/adapters/local_storage_service.js +132 -0
  166. package/dist/storage/adapters/local_storage_service.js.map +1 -0
  167. package/dist/storage/adapters/ovh_storage_service.d.ts +72 -0
  168. package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -0
  169. package/dist/storage/adapters/ovh_storage_service.js +205 -0
  170. package/dist/storage/adapters/ovh_storage_service.js.map +1 -0
  171. package/dist/storage/storage_factory.d.ts +14 -0
  172. package/dist/storage/storage_factory.d.ts.map +1 -0
  173. package/dist/storage/storage_factory.js +43 -0
  174. package/dist/storage/storage_factory.js.map +1 -0
  175. package/dist/storage/storage_service.d.ts +163 -0
  176. package/dist/storage/storage_service.d.ts.map +1 -0
  177. package/dist/storage/storage_service.js +58 -0
  178. package/dist/storage/storage_service.js.map +1 -0
  179. package/dist/types/data_record.d.ts +123 -0
  180. package/dist/types/data_record.d.ts.map +1 -0
  181. package/dist/types/data_record.js +8 -0
  182. package/dist/types/data_record.js.map +1 -0
  183. package/dist/utils/graceful_shutdown.d.ts +44 -0
  184. package/dist/utils/graceful_shutdown.d.ts.map +1 -0
  185. package/dist/utils/graceful_shutdown.js +79 -0
  186. package/dist/utils/graceful_shutdown.js.map +1 -0
  187. package/dist/utils/http_responses.d.ts +175 -0
  188. package/dist/utils/http_responses.d.ts.map +1 -0
  189. package/dist/utils/http_responses.js +216 -0
  190. package/dist/utils/http_responses.js.map +1 -0
  191. package/dist/utils/index.d.ts +8 -0
  192. package/dist/utils/index.d.ts.map +1 -0
  193. package/dist/utils/index.js +6 -0
  194. package/dist/utils/index.js.map +1 -0
  195. package/dist/utils/logger.d.ts +74 -0
  196. package/dist/utils/logger.d.ts.map +1 -0
  197. package/dist/utils/logger.js +92 -0
  198. package/dist/utils/logger.js.map +1 -0
  199. package/dist/utils/map_to_data_record.d.ts +10 -0
  200. package/dist/utils/map_to_data_record.d.ts.map +1 -0
  201. package/dist/utils/map_to_data_record.js +36 -0
  202. package/dist/utils/map_to_data_record.js.map +1 -0
  203. package/dist/utils/safe_async.d.ts +50 -0
  204. package/dist/utils/safe_async.d.ts.map +1 -0
  205. package/dist/utils/safe_async.js +90 -0
  206. package/dist/utils/safe_async.js.map +1 -0
  207. package/dist/utils/servable_endpoint.d.ts +63 -0
  208. package/dist/utils/servable_endpoint.d.ts.map +1 -0
  209. package/dist/utils/servable_endpoint.js +67 -0
  210. package/dist/utils/servable_endpoint.js.map +1 -0
  211. package/dist/utils/zip_utils.d.ts +66 -0
  212. package/dist/utils/zip_utils.d.ts.map +1 -0
  213. package/dist/utils/zip_utils.js +169 -0
  214. package/dist/utils/zip_utils.js.map +1 -0
  215. package/dist/validation/index.d.ts +3 -0
  216. package/dist/validation/index.d.ts.map +1 -0
  217. package/dist/validation/index.js +7 -0
  218. package/dist/validation/index.js.map +1 -0
  219. package/dist/validation/schemas.d.ts +273 -0
  220. package/dist/validation/schemas.d.ts.map +1 -0
  221. package/dist/validation/schemas.js +82 -0
  222. package/dist/validation/schemas.js.map +1 -0
  223. package/dist/validation/validate.d.ts +49 -0
  224. package/dist/validation/validate.d.ts.map +1 -0
  225. package/dist/validation/validate.js +110 -0
  226. package/dist/validation/validate.js.map +1 -0
  227. package/package.json +23 -13
@@ -0,0 +1,907 @@
1
+ import express from 'ultimate-express';
2
+ import multer from 'multer';
3
+ import fs from 'fs/promises';
4
+ import cors from 'cors';
5
+ import { initializeComponents, initializeAssetsManagers } from './initializer.js';
6
+ import { UserService } from '../auth/user_service.js';
7
+ import { exposeEndpoints } from './endpoints.js';
8
+ import { scheduleComponents } from './scheduler.js';
9
+ import { LogLevel } from '../utils/logger.js';
10
+ import { QueueManager } from './queue_manager.js';
11
+ import { UploadProcessor } from './upload_processor.js';
12
+ import { engineEventBus } from './events.js';
13
+ import { isAsyncUploadable } from '../components/async_upload.js';
14
+ import { HealthChecker, createDatabaseCheck, createRedisCheck, createStorageCheck, livenessCheck } from './health.js';
15
+ /**
16
+ * Digital Twin Engine - Core orchestrator for collectors, harvesters, and handlers
17
+ *
18
+ * The engine manages the lifecycle of all components, sets up queues for processing,
19
+ * exposes HTTP endpoints, and handles the overall coordination of the digital twin system.
20
+ *
21
+ * @class DigitalTwinEngine
22
+ * @example
23
+ * ```TypeScript
24
+ * import { DigitalTwinEngine } from './digital_twin_engine.js'
25
+ * import { StorageServiceFactory } from '../storage/storage_factory.js'
26
+ * import { KnexDatabaseAdapter } from '../database/adapters/knex_database_adapter.js'
27
+ *
28
+ * const storage = StorageServiceFactory.create()
29
+ * const database = new KnexDatabaseAdapter({ client: 'sqlite3', connection: ':memory:' }, storage)
30
+ *
31
+ * const engine = new DigitalTwinEngine({
32
+ * storage,
33
+ * database,
34
+ * collectors: [myCollector],
35
+ * server: { port: 3000 }
36
+ * })
37
+ *
38
+ * await engine.start()
39
+ * ```
40
+ */
41
+ export class DigitalTwinEngine {
42
+ #collectors;
43
+ #harvesters;
44
+ #handlers;
45
+ #assetsManagers;
46
+ #customTableManagers;
47
+ #storage;
48
+ #database;
49
+ #app;
50
+ #router;
51
+ #options;
52
+ #queueManager;
53
+ #uploadProcessor;
54
+ /** uWebSockets.js TemplatedApp - has close() method to shut down all connections */
55
+ #server;
56
+ #workers = [];
57
+ #isShuttingDown = false;
58
+ #shutdownTimeout = 30000;
59
+ #healthChecker = new HealthChecker();
60
+ /** Get all active components (collectors and harvesters) */
61
+ get #activeComponents() {
62
+ return [...this.#collectors, ...this.#harvesters];
63
+ }
64
+ /** Get all components (collectors + harvesters + handlers + assetsManagers + customTableManagers) */
65
+ get #allComponents() {
66
+ return [
67
+ ...this.#collectors,
68
+ ...this.#harvesters,
69
+ ...this.#handlers,
70
+ ...this.#assetsManagers,
71
+ ...this.#customTableManagers
72
+ ];
73
+ }
74
+ /** Check if multi-queue mode is enabled */
75
+ get #isMultiQueueEnabled() {
76
+ return this.#options.queues?.multiQueue ?? true;
77
+ }
78
+ /**
79
+ * Creates a new Digital Twin Engine instance
80
+ *
81
+ * @param {EngineOptions} options - Configuration options for the engine
82
+ * @throws {Error} If required options (storage, database) are missing
83
+ *
84
+ * @example
85
+ * ```TypeScript
86
+ * const engine = new DigitalTwinEngine({
87
+ * storage: myStorageService,
88
+ * database: myDatabaseAdapter,
89
+ * collectors: [collector1, collector2],
90
+ * server: { port: 4000, host: 'localhost' }
91
+ * })
92
+ * ```
93
+ */
94
+ constructor(options) {
95
+ this.#options = this.#applyDefaults(options);
96
+ this.#collectors = this.#options.collectors ?? [];
97
+ this.#harvesters = this.#options.harvesters ?? [];
98
+ this.#handlers = this.#options.handlers ?? [];
99
+ this.#assetsManagers = this.#options.assetsManagers ?? [];
100
+ this.#customTableManagers = this.#options.customTableManagers ?? [];
101
+ this.#storage = this.#options.storage;
102
+ this.#database = this.#options.database;
103
+ this.#app = express();
104
+ this.#router = express.Router();
105
+ this.#queueManager = this.#createQueueManager();
106
+ this.#uploadProcessor = this.#createUploadProcessor();
107
+ }
108
+ #createUploadProcessor() {
109
+ // Only create upload processor if we have a queue manager (which means Redis is available)
110
+ if (!this.#queueManager) {
111
+ return null;
112
+ }
113
+ return new UploadProcessor(this.#storage, this.#database);
114
+ }
115
+ #applyDefaults(options) {
116
+ return {
117
+ collectors: [],
118
+ harvesters: [],
119
+ handlers: [],
120
+ assetsManagers: [],
121
+ customTableManagers: [],
122
+ server: {
123
+ port: 3000,
124
+ host: '0.0.0.0',
125
+ ...options.server
126
+ },
127
+ queues: {
128
+ multiQueue: true,
129
+ workers: {
130
+ collectors: 1,
131
+ harvesters: 1,
132
+ ...options.queues?.workers
133
+ },
134
+ ...options.queues
135
+ },
136
+ logging: {
137
+ level: LogLevel.INFO,
138
+ format: 'text',
139
+ ...options.logging
140
+ },
141
+ dryRun: false,
142
+ ...options
143
+ };
144
+ }
145
+ #createQueueManager() {
146
+ // Create queue manager if we have collectors, harvesters, OR assets managers that may need async uploads
147
+ const hasActiveComponents = this.#collectors.length > 0 || this.#harvesters.length > 0;
148
+ const hasAssetsManagers = this.#assetsManagers.length > 0;
149
+ if (!hasActiveComponents && !hasAssetsManagers) {
150
+ return null;
151
+ }
152
+ return new QueueManager({
153
+ redis: this.#options.redis,
154
+ collectorWorkers: this.#options.queues?.workers?.collectors,
155
+ harvesterWorkers: this.#options.queues?.workers?.harvesters,
156
+ queueOptions: this.#options.queues?.options
157
+ });
158
+ }
159
+ /**
160
+ * Initialize store managers and create their database tables
161
+ * @private
162
+ */
163
+ async #initializeCustomTableManagers() {
164
+ for (const customTableManager of this.#customTableManagers) {
165
+ // Inject dependencies
166
+ customTableManager.setDependencies(this.#database);
167
+ // Initialize the table with custom columns
168
+ await customTableManager.initializeTable();
169
+ }
170
+ }
171
+ /**
172
+ * Ensure temporary upload directory exists
173
+ * @private
174
+ */
175
+ async #ensureTempUploadDir() {
176
+ const tempDir = process.env.TEMP_UPLOAD_DIR || '/tmp/digitaltwin-uploads';
177
+ try {
178
+ await fs.mkdir(tempDir, { recursive: true });
179
+ }
180
+ catch (error) {
181
+ throw new Error(`Failed to create temp upload directory ${tempDir}: ${error}`);
182
+ }
183
+ }
184
+ /**
185
+ * Setup monitoring endpoints for queue statistics and health checks
186
+ * @private
187
+ */
188
+ #setupMonitoringEndpoints() {
189
+ // Register default health checks
190
+ this.#healthChecker.registerCheck('database', createDatabaseCheck(this.#database));
191
+ if (this.#queueManager) {
192
+ this.#healthChecker.registerCheck('redis', createRedisCheck(this.#queueManager));
193
+ }
194
+ this.#healthChecker.registerCheck('storage', createStorageCheck(this.#storage));
195
+ // Set component counts
196
+ this.#healthChecker.setComponentCounts({
197
+ collectors: this.#collectors.length,
198
+ harvesters: this.#harvesters.length,
199
+ handlers: this.#handlers.length,
200
+ assetsManagers: this.#assetsManagers.length
201
+ });
202
+ // Liveness probe - shallow check, always returns ok if process is running
203
+ this.#router.get('/api/health/live', (req, res) => {
204
+ res.status(200).json(livenessCheck());
205
+ });
206
+ // Readiness probe - deep check with database and redis verification
207
+ this.#router.get('/api/health/ready', async (req, res) => {
208
+ const health = await this.#healthChecker.performCheck();
209
+ const statusCode = health.status === 'unhealthy' ? 503 : 200;
210
+ res.status(statusCode).json(health);
211
+ });
212
+ // Full health check endpoint (detailed)
213
+ this.#router.get('/api/health', async (req, res) => {
214
+ const health = await this.#healthChecker.performCheck();
215
+ res.json(health);
216
+ });
217
+ // Queue statistics endpoint
218
+ this.#router.get('/api/queues/stats', async (req, res) => {
219
+ if (this.#queueManager) {
220
+ const stats = await this.#queueManager.getQueueStats();
221
+ res.json(stats);
222
+ }
223
+ else {
224
+ res.json({
225
+ collectors: { status: 'No collectors configured' },
226
+ harvesters: { status: 'No harvesters configured' }
227
+ });
228
+ }
229
+ });
230
+ }
231
+ /**
232
+ * Starts the Digital Twin Engine
233
+ *
234
+ * This method:
235
+ * 1. Initializes all registered components (collectors, harvesters, handlers)
236
+ * 2. Set up HTTP endpoints for component access
237
+ * 3. Configures and starts background job queues
238
+ * 4. Starts the HTTP server
239
+ * 5. Exposes queue monitoring endpoints
240
+ *
241
+ * @async
242
+ * @returns {Promise<void>}
243
+ *
244
+ * @example
245
+ * ```TypeScript
246
+ * await engine.start()
247
+ * console.log('Engine is running!')
248
+ * ```
249
+ */
250
+ async start() {
251
+ const isDryRun = this.#options.dryRun ?? false;
252
+ if (isDryRun) {
253
+ // In dry run, just validate everything without creating tables
254
+ const validationResult = await this.validateConfiguration();
255
+ if (!validationResult.valid) {
256
+ throw new Error(`Validation failed:\n${validationResult.engineErrors.join('\n')}`);
257
+ }
258
+ return;
259
+ }
260
+ // Normal startup - initialize user management tables first
261
+ const userService = new UserService(this.#database);
262
+ await userService.initializeTables();
263
+ // Get autoMigration setting (default: true)
264
+ const autoMigration = this.#options.autoMigration ?? true;
265
+ // Initialize components and create tables if needed
266
+ await initializeComponents(this.#activeComponents, this.#database, this.#storage, autoMigration);
267
+ // Initialize assets managers and create their tables if needed
268
+ await initializeAssetsManagers(this.#assetsManagers, this.#database, this.#storage, autoMigration);
269
+ // Initialize store managers and create their tables if needed
270
+ await this.#initializeCustomTableManagers();
271
+ // Initialize handlers (inject dependencies if needed)
272
+ for (const handler of this.#handlers) {
273
+ if ('setDependencies' in handler && typeof handler.setDependencies === 'function') {
274
+ handler.setDependencies(this.#database, this.#storage);
275
+ }
276
+ // If it's a GlobalAssetsHandler, inject the AssetsManager instances
277
+ if ('setAssetsManagers' in handler && typeof handler.setAssetsManagers === 'function') {
278
+ handler.setAssetsManagers(this.#assetsManagers);
279
+ }
280
+ }
281
+ // Inject upload queue to components that support async uploads
282
+ if (this.#queueManager) {
283
+ const allManagers = [...this.#assetsManagers];
284
+ for (const manager of allManagers) {
285
+ if (isAsyncUploadable(manager)) {
286
+ manager.setUploadQueue(this.#queueManager.uploadQueue);
287
+ }
288
+ }
289
+ }
290
+ // Start upload processor worker (for async file processing)
291
+ // Uses same Redis config as QueueManager (defaults to localhost:6379 if not specified)
292
+ if (this.#uploadProcessor) {
293
+ const redisConfig = this.#options.redis || {
294
+ host: 'localhost',
295
+ port: 6379,
296
+ maxRetriesPerRequest: null
297
+ };
298
+ this.#uploadProcessor.start(redisConfig);
299
+ }
300
+ await exposeEndpoints(this.#router, this.#allComponents);
301
+ // Setup component scheduling with queue manager (only if we have active components)
302
+ if (this.#activeComponents.length > 0 && this.#queueManager) {
303
+ this.#workers = await scheduleComponents(this.#activeComponents, this.#queueManager, this.#isMultiQueueEnabled);
304
+ }
305
+ this.#setupMonitoringEndpoints();
306
+ // Ensure temporary upload directory exists
307
+ await this.#ensureTempUploadDir();
308
+ // Enable CORS for cross-origin requests from frontend applications
309
+ this.#app.use(cors({
310
+ origin: process.env.CORS_ORIGIN || true, // Allow all origins by default, configure in production
311
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
312
+ allowedHeaders: ['Content-Type', 'Authorization'],
313
+ credentials: true // Allow cookies/credentials
314
+ }));
315
+ // Configure Express middlewares for body parsing - no limits for large files
316
+ this.#app.use(express.json({ limit: '10gb' }));
317
+ this.#app.use(express.urlencoded({ extended: true, limit: '10gb' }));
318
+ // Add multipart/form-data support for file uploads with disk storage for large files
319
+ const upload = multer({
320
+ storage: multer.diskStorage({
321
+ destination: (req, file, cb) => {
322
+ // Use temporary directory, will be cleaned up after processing
323
+ const tempDir = process.env.TEMP_UPLOAD_DIR || '/tmp/digitaltwin-uploads';
324
+ cb(null, tempDir);
325
+ },
326
+ filename: (req, file, cb) => {
327
+ // Generate unique filename to avoid conflicts
328
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
329
+ cb(null, file.fieldname + '-' + uniqueSuffix + '-' + file.originalname);
330
+ }
331
+ }),
332
+ limits: {
333
+ // Remove file size limit to allow large files (10GB+)
334
+ files: 1, // Only one file per request for safety
335
+ parts: 10, // Limit form parts
336
+ headerPairs: 2000 // Limit header pairs
337
+ }
338
+ });
339
+ this.#app.use(upload.single('file'));
340
+ this.#app.use(this.#router);
341
+ const { port, host = '0.0.0.0' } = this.#options.server ?? {
342
+ port: 3000,
343
+ host: '0.0.0.0'
344
+ };
345
+ // Wait for server to be ready
346
+ // Note: ultimate-express types say (host, port, callback) but implementation accepts (port, host, callback)
347
+ // The implementation internally reorders to (host, port) for uWebSockets.js
348
+ // Using type assertion to work around this types bug in ultimate-express
349
+ await new Promise(resolve => {
350
+ const app = this.#app;
351
+ this.#server = app.listen(port, host, () => {
352
+ resolve();
353
+ });
354
+ });
355
+ // Note: uWebSockets.js (used by ultimate-express) handles timeouts differently than Node.js http.Server:
356
+ // - HTTP idle timeout is 10 seconds (only when connection is inactive)
357
+ // - During active data transfer (file uploads), the connection stays open
358
+ // - Properties like timeout, keepAliveTimeout, headersTimeout don't exist on TemplatedApp
359
+ // For large file uploads, this should work fine as the connection remains active during transfer.
360
+ }
361
+ /**
362
+ * Get the server port
363
+ *
364
+ * @returns {number | undefined} The server port or undefined if not started
365
+ *
366
+ * @example
367
+ * ```TypeScript
368
+ * const port = engine.getPort()
369
+ * console.log(`Server running on port ${port}`)
370
+ * ```
371
+ */
372
+ getPort() {
373
+ if (!this.#server)
374
+ return undefined;
375
+ // ultimate-express stores port on the app instance
376
+ // Use type assertion to access the port property
377
+ const app = this.#app;
378
+ return app.port ?? this.#options.server?.port;
379
+ }
380
+ /**
381
+ * Configure the shutdown timeout (in ms)
382
+ * @param timeout Timeout in milliseconds (default: 30000)
383
+ */
384
+ setShutdownTimeout(timeout) {
385
+ this.#shutdownTimeout = timeout;
386
+ }
387
+ /**
388
+ * Check if the engine is currently shutting down
389
+ * @returns true if shutdown is in progress
390
+ */
391
+ isShuttingDown() {
392
+ return this.#isShuttingDown;
393
+ }
394
+ /**
395
+ * Register a custom health check
396
+ * @param name Unique name for the check
397
+ * @param checkFn Function that performs the check
398
+ *
399
+ * @example
400
+ * ```typescript
401
+ * engine.registerHealthCheck('external-api', async () => {
402
+ * try {
403
+ * const res = await fetch('https://api.example.com/health')
404
+ * return { status: res.ok ? 'up' : 'down' }
405
+ * } catch (error) {
406
+ * return { status: 'down', error: error.message }
407
+ * }
408
+ * })
409
+ * ```
410
+ */
411
+ registerHealthCheck(name, checkFn) {
412
+ this.#healthChecker.registerCheck(name, checkFn);
413
+ }
414
+ /**
415
+ * Remove a custom health check
416
+ * @param name Name of the check to remove
417
+ * @returns true if the check was removed, false if it didn't exist
418
+ */
419
+ removeHealthCheck(name) {
420
+ return this.#healthChecker.removeCheck(name);
421
+ }
422
+ /**
423
+ * Get list of registered health check names
424
+ */
425
+ getHealthCheckNames() {
426
+ return this.#healthChecker.getCheckNames();
427
+ }
428
+ /**
429
+ * Stops the Digital Twin Engine with graceful shutdown
430
+ *
431
+ * This method:
432
+ * 1. Prevents new work from being accepted
433
+ * 2. Removes all event listeners
434
+ * 3. Closes HTTP server
435
+ * 4. Drains queues and waits for active jobs
436
+ * 5. Closes all queue workers
437
+ * 6. Stops upload processor
438
+ * 7. Closes queue manager
439
+ * 8. Closes database connections
440
+ *
441
+ * @async
442
+ * @returns {Promise<void>}
443
+ *
444
+ * @example
445
+ * ```TypeScript
446
+ * await engine.stop()
447
+ * console.log('Engine stopped gracefully')
448
+ * ```
449
+ */
450
+ async stop() {
451
+ if (this.#isShuttingDown) {
452
+ if (process.env.NODE_ENV !== 'test') {
453
+ console.warn('[DigitalTwin] Shutdown already in progress');
454
+ }
455
+ return;
456
+ }
457
+ this.#isShuttingDown = true;
458
+ const startTime = Date.now();
459
+ if (process.env.NODE_ENV !== 'test') {
460
+ console.log('[DigitalTwin] Graceful shutdown initiated...');
461
+ }
462
+ const errors = [];
463
+ // 1. Remove all event listeners to prevent new work
464
+ this.#cleanupEventListeners();
465
+ // 2. Close HTTP server (uWebSockets.js TemplatedApp.close() is synchronous)
466
+ if (this.#server) {
467
+ try {
468
+ this.#server.close();
469
+ }
470
+ catch (error) {
471
+ errors.push(this.#wrapError('Server close', error));
472
+ }
473
+ this.#server = undefined;
474
+ }
475
+ // 3. Drain queues - wait for active jobs with timeout
476
+ if (this.#queueManager) {
477
+ try {
478
+ await this.#drainQueues();
479
+ }
480
+ catch (error) {
481
+ errors.push(this.#wrapError('Queue drain', error));
482
+ }
483
+ }
484
+ // 4. Close all workers with extended timeout and force close
485
+ await this.#closeWorkers(errors);
486
+ // 5. Stop upload processor worker
487
+ if (this.#uploadProcessor) {
488
+ try {
489
+ await this.#uploadProcessor.stop();
490
+ }
491
+ catch (error) {
492
+ errors.push(this.#wrapError('Upload processor', error));
493
+ }
494
+ }
495
+ // 6. Close queue connections (only if we have a queue manager)
496
+ if (this.#queueManager) {
497
+ try {
498
+ await this.#queueManager.close();
499
+ }
500
+ catch (error) {
501
+ errors.push(this.#wrapError('Queue manager', error));
502
+ }
503
+ }
504
+ // 7. Close database connections
505
+ try {
506
+ await this.#database.close();
507
+ }
508
+ catch (error) {
509
+ errors.push(this.#wrapError('Database', error));
510
+ }
511
+ const duration = Date.now() - startTime;
512
+ if (process.env.NODE_ENV !== 'test') {
513
+ if (errors.length > 0) {
514
+ console.error(`[DigitalTwin] Shutdown completed with ${errors.length} errors in ${duration}ms:`, errors.map(e => e.message).join(', '));
515
+ }
516
+ else {
517
+ console.log(`[DigitalTwin] Shutdown completed successfully in ${duration}ms`);
518
+ }
519
+ }
520
+ }
521
+ #cleanupEventListeners() {
522
+ engineEventBus.removeAllListeners();
523
+ }
524
+ async #drainQueues() {
525
+ if (!this.#queueManager)
526
+ return;
527
+ const timeout = Math.min(this.#shutdownTimeout / 2, 15000);
528
+ const startTime = Date.now();
529
+ while (Date.now() - startTime < timeout) {
530
+ try {
531
+ const stats = await this.#queueManager.getQueueStats();
532
+ const totalActive = Object.values(stats).reduce((sum, q) => sum + (q.active || 0), 0);
533
+ if (totalActive === 0)
534
+ break;
535
+ if (process.env.NODE_ENV !== 'test') {
536
+ console.log(`[DigitalTwin] Waiting for ${totalActive} active jobs...`);
537
+ }
538
+ await new Promise(resolve => setTimeout(resolve, 1000));
539
+ }
540
+ catch {
541
+ await new Promise(resolve => setTimeout(resolve, 1000));
542
+ break;
543
+ }
544
+ }
545
+ }
546
+ async #closeWorkers(errors) {
547
+ const workerTimeout = Math.min(this.#shutdownTimeout / 3, 10000);
548
+ await Promise.all(this.#workers.map(async (worker) => {
549
+ try {
550
+ await Promise.race([
551
+ worker.close(),
552
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Worker close timeout')), workerTimeout))
553
+ ]);
554
+ }
555
+ catch {
556
+ try {
557
+ await worker.disconnect();
558
+ }
559
+ catch (disconnectError) {
560
+ errors.push(this.#wrapError('Worker disconnect', disconnectError));
561
+ }
562
+ }
563
+ }));
564
+ this.#workers = [];
565
+ }
566
+ #wrapError(context, error) {
567
+ const message = error instanceof Error ? error.message : String(error);
568
+ return new Error(`${context}: ${message}`);
569
+ }
570
+ /**
571
+ * Validate the engine configuration and all components
572
+ *
573
+ * This method checks that all components are properly configured and can be initialized
574
+ * without actually creating tables or starting the server.
575
+ *
576
+ * @returns {Promise<ValidationResult>} Comprehensive validation results
577
+ *
578
+ * @example
579
+ * ```typescript
580
+ * const result = await engine.validateConfiguration()
581
+ * if (!result.valid) {
582
+ * console.error('Validation errors:', result.engineErrors)
583
+ * }
584
+ * ```
585
+ */
586
+ async validateConfiguration() {
587
+ const componentResults = [];
588
+ const engineErrors = [];
589
+ // Validate collectors
590
+ for (const collector of this.#collectors) {
591
+ componentResults.push(await this.#validateComponent(collector, 'collector'));
592
+ }
593
+ // Validate harvesters
594
+ for (const harvester of this.#harvesters) {
595
+ componentResults.push(await this.#validateComponent(harvester, 'harvester'));
596
+ }
597
+ // Validate handlers
598
+ for (const handler of this.#handlers) {
599
+ componentResults.push(await this.#validateComponent(handler, 'handler'));
600
+ }
601
+ // Validate assets managers
602
+ for (const assetsManager of this.#assetsManagers) {
603
+ componentResults.push(await this.#validateComponent(assetsManager, 'assets_manager'));
604
+ }
605
+ // Validate store managers
606
+ for (const customTableManager of this.#customTableManagers) {
607
+ componentResults.push(await this.#validateComponent(customTableManager, 'custom_table_manager'));
608
+ }
609
+ // Validate engine-level configuration
610
+ try {
611
+ if (!this.#storage) {
612
+ engineErrors.push('Storage service is required');
613
+ }
614
+ if (!this.#database) {
615
+ engineErrors.push('Database adapter is required');
616
+ }
617
+ // Test storage connection
618
+ if (this.#storage && typeof this.#storage.save === 'function') {
619
+ // Storage validation passed
620
+ }
621
+ else {
622
+ engineErrors.push('Storage service does not implement required methods');
623
+ }
624
+ // Test database connection
625
+ if (this.#database && typeof this.#database.save === 'function') {
626
+ // Database validation passed
627
+ }
628
+ else {
629
+ engineErrors.push('Database adapter does not implement required methods');
630
+ }
631
+ }
632
+ catch (error) {
633
+ engineErrors.push(`Engine configuration error: ${error instanceof Error ? error.message : String(error)}`);
634
+ }
635
+ // Calculate summary
636
+ const validComponents = componentResults.filter(c => c.valid).length;
637
+ const totalWarnings = componentResults.reduce((acc, c) => acc + c.warnings.length, 0);
638
+ const result = {
639
+ valid: componentResults.every(c => c.valid) && engineErrors.length === 0,
640
+ components: componentResults,
641
+ engineErrors,
642
+ summary: {
643
+ total: componentResults.length,
644
+ valid: validComponents,
645
+ invalid: componentResults.length - validComponents,
646
+ warnings: totalWarnings
647
+ }
648
+ };
649
+ return result;
650
+ }
651
+ /**
652
+ * Test all components by running their core methods without persistence
653
+ *
654
+ * @returns {Promise<ComponentValidationResult[]>} Test results for each component
655
+ *
656
+ * @example
657
+ * ```typescript
658
+ * const results = await engine.testComponents()
659
+ * results.forEach(result => {
660
+ * console.log(`${result.name}: ${result.valid ? '✅' : '❌'}`)
661
+ * })
662
+ * ```
663
+ */
664
+ async testComponents() {
665
+ const results = [];
666
+ // Test collectors
667
+ for (const collector of this.#collectors) {
668
+ const result = await this.#testCollector(collector);
669
+ results.push(result);
670
+ }
671
+ // Test harvesters
672
+ for (const harvester of this.#harvesters) {
673
+ const result = await this.#testHarvester(harvester);
674
+ results.push(result);
675
+ }
676
+ // Test handlers
677
+ for (const handler of this.#handlers) {
678
+ const result = await this.#testHandler(handler);
679
+ results.push(result);
680
+ }
681
+ // Test assets managers
682
+ for (const assetsManager of this.#assetsManagers) {
683
+ const result = await this.#testAssetsManager(assetsManager);
684
+ results.push(result);
685
+ }
686
+ return results;
687
+ }
688
+ /**
689
+ * Validate a single component
690
+ */
691
+ async #validateComponent(component, type) {
692
+ const errors = [];
693
+ const warnings = [];
694
+ try {
695
+ // Check if component has required methods
696
+ if (typeof component.getConfiguration !== 'function') {
697
+ errors.push('Component must implement getConfiguration() method');
698
+ }
699
+ const config = component.getConfiguration();
700
+ // Validate configuration
701
+ if (!config.name) {
702
+ errors.push('Component configuration must have a name');
703
+ }
704
+ if (!config.description) {
705
+ warnings.push('Component configuration should have a description');
706
+ }
707
+ // Type-specific validation
708
+ if (type === 'collector' || type === 'harvester') {
709
+ const activeComponent = component;
710
+ if (typeof activeComponent.setDependencies !== 'function') {
711
+ errors.push('Active components must implement setDependencies() method');
712
+ }
713
+ }
714
+ if (type === 'collector') {
715
+ const collector = component;
716
+ if (typeof collector.collect !== 'function') {
717
+ errors.push('Collector must implement collect() method');
718
+ }
719
+ if (typeof collector.getSchedule !== 'function') {
720
+ errors.push('Collector must implement getSchedule() method');
721
+ }
722
+ }
723
+ if (type === 'harvester') {
724
+ const harvester = component;
725
+ if (typeof harvester.harvest !== 'function') {
726
+ errors.push('Harvester must implement harvest() method');
727
+ }
728
+ }
729
+ if (type === 'assets_manager') {
730
+ const assetsManager = component;
731
+ if (typeof assetsManager.uploadAsset !== 'function') {
732
+ errors.push('AssetsManager must implement uploadAsset() method');
733
+ }
734
+ if (typeof assetsManager.getAllAssets !== 'function') {
735
+ errors.push('AssetsManager must implement getAllAssets() method');
736
+ }
737
+ }
738
+ if (type === 'custom_table_manager') {
739
+ const customTableManager = component;
740
+ if (typeof customTableManager.setDependencies !== 'function') {
741
+ errors.push('CustomTableManager must implement setDependencies() method');
742
+ }
743
+ if (typeof customTableManager.initializeTable !== 'function') {
744
+ errors.push('CustomTableManager must implement initializeTable() method');
745
+ }
746
+ // Validate store configuration
747
+ const config = customTableManager.getConfiguration();
748
+ if (typeof config !== 'object' || config === null) {
749
+ errors.push('CustomTableManager must return a valid configuration object');
750
+ }
751
+ else {
752
+ if (!config.columns || typeof config.columns !== 'object') {
753
+ errors.push('CustomTableManager configuration must define columns');
754
+ }
755
+ else {
756
+ // Validate columns definition
757
+ const columnCount = Object.keys(config.columns).length;
758
+ if (columnCount === 0) {
759
+ warnings.push('CustomTableManager has no custom columns defined');
760
+ }
761
+ // Validate column names and types
762
+ for (const [columnName, columnType] of Object.entries(config.columns)) {
763
+ if (!columnName || typeof columnName !== 'string') {
764
+ errors.push('Column names must be non-empty strings');
765
+ }
766
+ if (!columnType || typeof columnType !== 'string') {
767
+ errors.push(`Column '${columnName}' must have a valid SQL type`);
768
+ }
769
+ }
770
+ }
771
+ }
772
+ }
773
+ }
774
+ catch (error) {
775
+ errors.push(`Validation error: ${error instanceof Error ? error.message : String(error)}`);
776
+ }
777
+ return {
778
+ name: component.getConfiguration?.()?.name || 'unknown',
779
+ type,
780
+ valid: errors.length === 0,
781
+ errors,
782
+ warnings
783
+ };
784
+ }
785
+ /**
786
+ * Test a collector by running its collect method
787
+ */
788
+ async #testCollector(collector) {
789
+ const errors = [];
790
+ const warnings = [];
791
+ const config = collector.getConfiguration();
792
+ try {
793
+ // Test the collect method
794
+ const result = await collector.collect();
795
+ if (!Buffer.isBuffer(result)) {
796
+ errors.push('collect() method must return a Buffer');
797
+ }
798
+ if (result.length === 0) {
799
+ warnings.push('collect() method returned empty buffer');
800
+ }
801
+ }
802
+ catch (error) {
803
+ errors.push(`collect() method failed: ${error instanceof Error ? error.message : String(error)}`);
804
+ }
805
+ return {
806
+ name: config.name,
807
+ type: 'collector',
808
+ valid: errors.length === 0,
809
+ errors,
810
+ warnings
811
+ };
812
+ }
813
+ /**
814
+ * Test a harvester (more complex as it needs mock data)
815
+ */
816
+ async #testHarvester(harvester) {
817
+ const errors = [];
818
+ const warnings = [];
819
+ const config = harvester.getConfiguration();
820
+ try {
821
+ // Create mock data for testing
822
+ const mockData = {
823
+ id: 1,
824
+ name: 'test',
825
+ date: new Date(),
826
+ contentType: 'application/json',
827
+ url: 'test://url',
828
+ data: async () => Buffer.from('{"test": true}')
829
+ };
830
+ // Test the harvest method
831
+ const result = await harvester.harvest(mockData, {});
832
+ if (!Buffer.isBuffer(result)) {
833
+ errors.push('harvest() method must return a Buffer');
834
+ }
835
+ }
836
+ catch (error) {
837
+ errors.push(`harvest() method failed: ${error instanceof Error ? error.message : String(error)}`);
838
+ }
839
+ return {
840
+ name: config.name,
841
+ type: 'harvester',
842
+ valid: errors.length === 0,
843
+ errors,
844
+ warnings
845
+ };
846
+ }
847
+ /**
848
+ * Test a handler
849
+ */
850
+ async #testHandler(handler) {
851
+ const errors = [];
852
+ const warnings = [];
853
+ const config = handler.getConfiguration();
854
+ try {
855
+ // Handlers are mostly validated through their endpoint configuration
856
+ if (typeof handler.getEndpoints === 'function') {
857
+ const endpoints = handler.getEndpoints();
858
+ if (!Array.isArray(endpoints)) {
859
+ errors.push('getEndpoints() must return an array');
860
+ }
861
+ }
862
+ }
863
+ catch (error) {
864
+ errors.push(`Handler test failed: ${error instanceof Error ? error.message : String(error)}`);
865
+ }
866
+ return {
867
+ name: config.name,
868
+ type: 'handler',
869
+ valid: errors.length === 0,
870
+ errors,
871
+ warnings
872
+ };
873
+ }
874
+ /**
875
+ * Test an assets manager
876
+ */
877
+ async #testAssetsManager(assetsManager) {
878
+ const errors = [];
879
+ const warnings = [];
880
+ const config = assetsManager.getConfiguration();
881
+ try {
882
+ // Test configuration
883
+ if (!config.contentType) {
884
+ errors.push('AssetsManager configuration must have a contentType');
885
+ }
886
+ // In dry run mode, we can't test actual upload/download without dependencies
887
+ // Just validate that the methods exist and are callable
888
+ if (typeof assetsManager.getEndpoints === 'function') {
889
+ const endpoints = assetsManager.getEndpoints();
890
+ if (!Array.isArray(endpoints)) {
891
+ errors.push('getEndpoints() must return an array');
892
+ }
893
+ }
894
+ }
895
+ catch (error) {
896
+ errors.push(`AssetsManager test failed: ${error instanceof Error ? error.message : String(error)}`);
897
+ }
898
+ return {
899
+ name: config.name,
900
+ type: 'assets_manager',
901
+ valid: errors.length === 0,
902
+ errors,
903
+ warnings
904
+ };
905
+ }
906
+ }
907
+ //# sourceMappingURL=digital_twin_engine.js.map