@zintrust/workers 0.1.28 → 0.1.30

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 (111) hide show
  1. package/README.md +16 -1
  2. package/dist/AnomalyDetection.d.ts +4 -0
  3. package/dist/AnomalyDetection.js +8 -0
  4. package/dist/BroadcastWorker.d.ts +2 -0
  5. package/dist/CanaryController.js +49 -5
  6. package/dist/ChaosEngineering.js +13 -0
  7. package/dist/ClusterLock.js +21 -10
  8. package/dist/DeadLetterQueue.js +12 -8
  9. package/dist/MultiQueueWorker.d.ts +1 -1
  10. package/dist/MultiQueueWorker.js +12 -7
  11. package/dist/NotificationWorker.d.ts +2 -0
  12. package/dist/PriorityQueue.d.ts +2 -2
  13. package/dist/PriorityQueue.js +20 -21
  14. package/dist/ResourceMonitor.js +65 -38
  15. package/dist/WorkerFactory.d.ts +23 -3
  16. package/dist/WorkerFactory.js +420 -40
  17. package/dist/WorkerInit.js +8 -3
  18. package/dist/WorkerMetrics.d.ts +2 -1
  19. package/dist/WorkerMetrics.js +152 -93
  20. package/dist/WorkerRegistry.d.ts +6 -0
  21. package/dist/WorkerRegistry.js +70 -1
  22. package/dist/WorkerShutdown.d.ts +21 -0
  23. package/dist/WorkerShutdown.js +82 -9
  24. package/dist/WorkerShutdownDurableObject.d.ts +12 -0
  25. package/dist/WorkerShutdownDurableObject.js +41 -0
  26. package/dist/build-manifest.json +171 -99
  27. package/dist/createQueueWorker.d.ts +2 -0
  28. package/dist/createQueueWorker.js +42 -27
  29. package/dist/dashboard/types.d.ts +5 -0
  30. package/dist/dashboard/workers-api.js +136 -43
  31. package/dist/http/WorkerApiController.js +1 -0
  32. package/dist/http/WorkerController.js +133 -85
  33. package/dist/http/WorkerMonitoringService.d.ts +11 -0
  34. package/dist/http/WorkerMonitoringService.js +62 -0
  35. package/dist/http/middleware/CustomValidation.js +1 -1
  36. package/dist/http/middleware/EditWorkerValidation.d.ts +1 -1
  37. package/dist/http/middleware/EditWorkerValidation.js +7 -6
  38. package/dist/http/middleware/ProcessorPathSanitizer.js +101 -35
  39. package/dist/http/middleware/WorkerValidationChain.js +1 -0
  40. package/dist/index.d.ts +2 -1
  41. package/dist/index.js +1 -0
  42. package/dist/routes/workers.js +48 -6
  43. package/dist/storage/WorkerStore.d.ts +4 -1
  44. package/dist/storage/WorkerStore.js +55 -7
  45. package/dist/telemetry/api/TelemetryAPI.d.ts +46 -0
  46. package/dist/telemetry/api/TelemetryAPI.js +219 -0
  47. package/dist/telemetry/api/TelemetryMonitoringService.d.ts +17 -0
  48. package/dist/telemetry/api/TelemetryMonitoringService.js +113 -0
  49. package/dist/telemetry/components/AlertPanel.d.ts +1 -0
  50. package/dist/telemetry/components/AlertPanel.js +13 -0
  51. package/dist/telemetry/components/CostTracking.d.ts +1 -0
  52. package/dist/telemetry/components/CostTracking.js +14 -0
  53. package/dist/telemetry/components/ResourceUsageChart.d.ts +1 -0
  54. package/dist/telemetry/components/ResourceUsageChart.js +11 -0
  55. package/dist/telemetry/components/WorkerHealthChart.d.ts +1 -0
  56. package/dist/telemetry/components/WorkerHealthChart.js +11 -0
  57. package/dist/telemetry/index.d.ts +15 -0
  58. package/dist/telemetry/index.js +60 -0
  59. package/dist/telemetry/routes/dashboard.d.ts +6 -0
  60. package/dist/telemetry/routes/dashboard.js +608 -0
  61. package/dist/ui/router/EmbeddedAssets.d.ts +4 -0
  62. package/dist/ui/router/EmbeddedAssets.js +13 -0
  63. package/dist/ui/router/ui.js +100 -4
  64. package/package.json +9 -5
  65. package/src/AnomalyDetection.ts +9 -0
  66. package/src/CanaryController.ts +41 -5
  67. package/src/ChaosEngineering.ts +14 -0
  68. package/src/ClusterLock.ts +22 -9
  69. package/src/DeadLetterQueue.ts +13 -8
  70. package/src/MultiQueueWorker.ts +15 -8
  71. package/src/PriorityQueue.ts +21 -22
  72. package/src/ResourceMonitor.ts +72 -40
  73. package/src/WorkerFactory.ts +545 -49
  74. package/src/WorkerInit.ts +8 -3
  75. package/src/WorkerMetrics.ts +183 -105
  76. package/src/WorkerRegistry.ts +80 -1
  77. package/src/WorkerShutdown.ts +115 -9
  78. package/src/WorkerShutdownDurableObject.ts +64 -0
  79. package/src/createQueueWorker.ts +73 -30
  80. package/src/dashboard/types.ts +5 -0
  81. package/src/dashboard/workers-api.ts +165 -52
  82. package/src/http/WorkerApiController.ts +1 -0
  83. package/src/http/WorkerController.ts +167 -90
  84. package/src/http/WorkerMonitoringService.ts +77 -0
  85. package/src/http/middleware/CustomValidation.ts +1 -1
  86. package/src/http/middleware/EditWorkerValidation.ts +7 -6
  87. package/src/http/middleware/ProcessorPathSanitizer.ts +123 -36
  88. package/src/http/middleware/WorkerValidationChain.ts +1 -0
  89. package/src/index.ts +6 -1
  90. package/src/routes/workers.ts +66 -9
  91. package/src/storage/WorkerStore.ts +59 -9
  92. package/src/telemetry/api/TelemetryAPI.ts +292 -0
  93. package/src/telemetry/api/TelemetryMonitoringService.ts +149 -0
  94. package/src/telemetry/components/AlertPanel.ts +13 -0
  95. package/src/telemetry/components/CostTracking.ts +14 -0
  96. package/src/telemetry/components/ResourceUsageChart.ts +11 -0
  97. package/src/telemetry/components/WorkerHealthChart.ts +11 -0
  98. package/src/telemetry/index.ts +121 -0
  99. package/src/telemetry/public/assets/zintrust-logo.svg +15 -0
  100. package/src/telemetry/routes/dashboard.ts +638 -0
  101. package/src/telemetry/styles/tailwind.css +1 -0
  102. package/src/telemetry/styles/zintrust-theme.css +8 -0
  103. package/src/ui/router/EmbeddedAssets.ts +13 -0
  104. package/src/ui/router/ui.ts +112 -5
  105. package/src/ui/workers/index.html +2 -2
  106. package/src/ui/workers/main.js +232 -61
  107. package/src/ui/workers/zintrust.svg +30 -0
  108. package/dist/dashboard/workers-dashboard-ui.d.ts +0 -3
  109. package/dist/dashboard/workers-dashboard-ui.js +0 -1026
  110. package/dist/dashboard/workers-dashboard.d.ts +0 -4
  111. package/dist/dashboard/workers-dashboard.js +0 -904
@@ -4,10 +4,9 @@
4
4
  * HTTP handlers for worker management API
5
5
  */
6
6
 
7
- import { Logger, getValidatedBody, type IRequest, type IResponse } from '@zintrust/core';
7
+ import { Env, Logger, getValidatedBody, type IRequest, type IResponse } from '@zintrust/core';
8
8
  import type { Job } from 'bullmq';
9
9
  import { CanaryController } from '../CanaryController';
10
- import { getWorkers } from '../dashboard/workers-api';
11
10
  import { HealthMonitor } from '../HealthMonitor';
12
11
  import { getParam } from '../helper';
13
12
  import { SLAMonitor } from '../index';
@@ -19,6 +18,7 @@ import { WorkerRegistry } from '../WorkerRegistry';
19
18
  import { WorkerShutdown } from '../WorkerShutdown';
20
19
  import { WorkerVersioning } from '../WorkerVersioning';
21
20
  import type { InfrastructureConfig } from './middleware/InfrastructureValidator';
21
+ import { WorkerMonitoringService } from './WorkerMonitoringService';
22
22
 
23
23
  /**
24
24
  * Helper to get request body
@@ -62,13 +62,13 @@ async function create(req: IRequest, res: IResponse): Promise<void> {
62
62
 
63
63
  const rawProcessor = body.processor;
64
64
  let processor: (job: Job) => Promise<unknown>;
65
- let processorPath: string | undefined;
65
+ let processorSpec: string | undefined;
66
66
 
67
67
  if (typeof rawProcessor === 'string') {
68
- processorPath = rawProcessor;
69
- const resolved = await WorkerFactory.resolveProcessorPath(rawProcessor);
68
+ processorSpec = rawProcessor;
69
+ const resolved = await WorkerFactory.resolveProcessorSpec(rawProcessor);
70
70
  if (!resolved) {
71
- res.setStatus(400).json({ error: 'Processor path could not be resolved' });
71
+ res.setStatus(400).json({ error: 'Processor spec could not be resolved' });
72
72
  return;
73
73
  }
74
74
  processor = resolved;
@@ -84,7 +84,7 @@ async function create(req: IRequest, res: IResponse): Promise<void> {
84
84
  const config = {
85
85
  ...(body as WorkerFactoryConfig),
86
86
  processor,
87
- processorPath,
87
+ processorSpec,
88
88
  };
89
89
 
90
90
  await WorkerFactory.create(config);
@@ -114,6 +114,8 @@ async function start(req: IRequest, res: IResponse): Promise<void> {
114
114
  return;
115
115
  }
116
116
  const persistenceOverride = resolvePersistenceOverride(req);
117
+ const isActive = await ensureActiveWorker(name, persistenceOverride, res);
118
+ if (!isActive) return;
117
119
  const registered = WorkerRegistry.list().includes(name);
118
120
 
119
121
  if (!registered) {
@@ -138,6 +140,8 @@ async function stop(req: IRequest, res: IResponse): Promise<void> {
138
140
  try {
139
141
  const name = getParam(req, 'name');
140
142
  const persistenceOverride = resolvePersistenceOverride(req);
143
+ const isActive = await ensureActiveWorker(name, persistenceOverride, res);
144
+ if (!isActive) return;
141
145
  await WorkerFactory.stop(name, persistenceOverride);
142
146
  res.json({ ok: true, message: `Worker ${name} stopped` });
143
147
  } catch (error) {
@@ -155,6 +159,8 @@ async function restart(req: IRequest, res: IResponse): Promise<void> {
155
159
  try {
156
160
  const name = getParam(req, 'name');
157
161
  const persistenceOverride = resolvePersistenceOverride(req);
162
+ const isActive = await ensureActiveWorker(name, persistenceOverride, res);
163
+ if (!isActive) return;
158
164
  await WorkerFactory.restart(name, persistenceOverride);
159
165
  res.json({ ok: true, message: `Worker ${name} restarted` });
160
166
  } catch (error) {
@@ -190,6 +196,8 @@ async function setAutoStart(req: IRequest, res: IResponse): Promise<void> {
190
196
  }
191
197
 
192
198
  const persistenceOverride = resolvePersistenceOverride(req);
199
+ const isActive = await ensureActiveWorker(name, persistenceOverride, res);
200
+ if (!isActive) return;
193
201
 
194
202
  await WorkerFactory.setAutoStart(name, enabled, persistenceOverride);
195
203
 
@@ -209,6 +217,8 @@ async function pause(req: IRequest, res: IResponse): Promise<void> {
209
217
  try {
210
218
  const name = getParam(req, 'name');
211
219
  const persistenceOverride = resolvePersistenceOverride(req);
220
+ const isActive = await ensureActiveWorker(name, persistenceOverride, res);
221
+ if (!isActive) return;
212
222
  await WorkerFactory.pause(name, persistenceOverride);
213
223
  res.json({ ok: true, message: `Worker ${name} paused` });
214
224
  } catch (error) {
@@ -226,6 +236,8 @@ async function resume(req: IRequest, res: IResponse): Promise<void> {
226
236
  try {
227
237
  const name = getParam(req, 'name');
228
238
  const persistenceOverride = resolvePersistenceOverride(req);
239
+ const isActive = await ensureActiveWorker(name, persistenceOverride, res);
240
+ if (!isActive) return;
229
241
  await WorkerFactory.resume(name, persistenceOverride);
230
242
  res.json({ ok: true, message: `Worker ${name} resumed` });
231
243
  } catch (error) {
@@ -305,6 +317,32 @@ const resolvePersistenceOverride = (
305
317
  return undefined;
306
318
  };
307
319
 
320
+ const ensureActiveWorker = async (
321
+ name: string | undefined,
322
+ persistenceOverride:
323
+ | { driver: 'memory' }
324
+ | { driver: 'redis'; redis: { env: true }; keyPrefix?: string }
325
+ | { driver: 'database'; connection?: string; table?: string }
326
+ | undefined,
327
+ res: IResponse
328
+ ): Promise<boolean> => {
329
+ if (!name) return false;
330
+
331
+ const instance = WorkerFactory.get(name);
332
+ if (instance?.config?.activeStatus === false) {
333
+ res.setStatus(410).json({ error: 'Worker is inactive', code: 'WORKER_INACTIVE' });
334
+ return false;
335
+ }
336
+
337
+ const persisted = await WorkerFactory.getPersisted(name, persistenceOverride);
338
+ if (persisted?.activeStatus === false) {
339
+ res.setStatus(410).json({ error: 'Worker is inactive', code: 'WORKER_INACTIVE' });
340
+ return false;
341
+ }
342
+
343
+ return true;
344
+ };
345
+
308
346
  /**
309
347
  * Get a specific worker instance
310
348
  * @param req.params.name - Worker name
@@ -354,9 +392,15 @@ async function update(req: IRequest, res: IResponse): Promise<void> {
354
392
  return;
355
393
  }
356
394
 
357
- // Validate and merge updates (excluding immutable fields)
395
+ // Remove immutable fields and prepare updates
358
396
  const { name: _name, driver: _driver, ...updateData } = reqData; // Remove immutable fields
359
397
 
398
+ const processorValid = await validateProcessorSpecIfNeeded(updateData);
399
+ if (!processorValid) {
400
+ res.setStatus(400).json({ error: 'Processor spec could not be resolved' });
401
+ return;
402
+ }
403
+
360
404
  // Note: driver is determined by persistence configuration, not stored in worker record
361
405
  const updatedRecord = {
362
406
  ...currentRecord,
@@ -367,39 +411,16 @@ async function update(req: IRequest, res: IResponse): Promise<void> {
367
411
 
368
412
  (updatedRecord.infrastructure as unknown as InfrastructureConfig).persistence.driver = driver;
369
413
 
370
- // Update persistence store with the complete updated record
371
- try {
372
- // Persist merged record via WorkerFactory API
373
- await WorkerFactory.update(
374
- name,
375
- updatedRecord as unknown as WorkerRecord,
376
- persistenceOverride
377
- );
378
- Logger.info(`Worker ${name} persistence updated with fields:`, Object.keys(updateData));
379
- } catch (persistError) {
380
- Logger.warn(`Failed to persist some updates for ${name}`, persistError as Error);
381
- // Continue with restart even if persistence update partially fails
382
- }
414
+ await persistUpdatedRecord(name, updatedRecord, persistenceOverride, updateData);
383
415
 
384
- // If worker is currently running, restart it to apply new configuration changes
385
- // This ensures new concurrency, queue settings, and other config take effect
386
416
  const currentInstance = WorkerFactory.get(name);
387
- let restartError: string | undefined;
388
-
389
- if (currentInstance && currentInstance.status === 'running') {
390
- try {
391
- Logger.info(`Restarting worker ${name} to apply configuration changes`);
392
- await WorkerFactory.restart(name, persistenceOverride);
393
- } catch (error) {
394
- restartError = (error as Error).message;
395
- Logger.warn(`Failed to restart worker ${name} after update`, error as Error);
396
- // Don't fail the update, but warn about restart failure
397
- }
398
- } else {
399
- Logger.info(
400
- `Worker ${name} is not running (status: ${currentInstance?.status || 'not found'}), skipping restart`
401
- );
402
- }
417
+ const restartError = await restartIfNeeded(
418
+ name,
419
+ currentInstance,
420
+ updatedRecord,
421
+ currentRecord,
422
+ persistenceOverride
423
+ );
403
424
 
404
425
  // Worker configuration updated in persistence and memory
405
426
  Logger.info(`Worker configuration updated: ${name}`, {
@@ -420,6 +441,64 @@ async function update(req: IRequest, res: IResponse): Promise<void> {
420
441
  }
421
442
  }
422
443
 
444
+ // Helpers extracted from update() to reduce complexity
445
+ async function validateProcessorSpecIfNeeded(
446
+ updateData: Record<string, unknown>
447
+ ): Promise<boolean> {
448
+ if (typeof updateData['processorSpec'] === 'string') {
449
+ const resolved = await WorkerFactory.resolveProcessorSpec(
450
+ updateData['processorSpec'] as string
451
+ );
452
+ return Boolean(resolved);
453
+ }
454
+ return true;
455
+ }
456
+
457
+ async function persistUpdatedRecord(
458
+ name: string,
459
+ updatedRecord: WorkerRecord | unknown,
460
+ persistenceOverride: ReturnType<typeof resolvePersistenceOverride> | undefined,
461
+ updateData: Record<string, unknown>
462
+ ): Promise<void> {
463
+ try {
464
+ await WorkerFactory.update(name, updatedRecord as unknown as WorkerRecord, persistenceOverride);
465
+ Logger.info(`Worker ${name} persistence updated with fields:`, Object.keys(updateData));
466
+ } catch (persistError) {
467
+ Logger.warn(`Failed to persist some updates for ${name}`, persistError as Error);
468
+ // Continue execution even if persistence update partially fails
469
+ }
470
+ }
471
+
472
+ async function restartIfNeeded(
473
+ name: string,
474
+ currentInstance: ReturnType<typeof WorkerFactory.get> | undefined,
475
+ updatedRecord: WorkerRecord,
476
+ currentRecord: WorkerRecord,
477
+ persistenceOverride: ReturnType<typeof resolvePersistenceOverride> | undefined
478
+ ): Promise<string | undefined> {
479
+ if (
480
+ !currentInstance ||
481
+ currentInstance.status !== 'running' ||
482
+ updatedRecord.activeStatus === false ||
483
+ currentRecord.activeStatus === false
484
+ ) {
485
+ Logger.info(
486
+ `Worker ${name} is not running (status: ${currentInstance?.status || 'not found'}), skipping restart`
487
+ );
488
+ return undefined;
489
+ }
490
+
491
+ try {
492
+ Logger.info(`Restarting worker ${name} to apply configuration changes`);
493
+ await WorkerFactory.restart(name, persistenceOverride);
494
+ return undefined;
495
+ } catch (err) {
496
+ const restartError = (err as Error).message;
497
+ Logger.warn(`Failed to restart worker ${name} after update`, err as Error);
498
+ return restartError;
499
+ }
500
+ }
501
+
423
502
  /**
424
503
  * Get worker status
425
504
  * @param req.params.name - Worker name
@@ -590,7 +669,7 @@ async function getSlaStatus(req: IRequest, res: IResponse): Promise<void> {
590
669
  } catch (error) {
591
670
  Logger.error('WorkerController.getSlaStatus failed', error);
592
671
  if ((error as Error).message.includes('SLA config not found')) {
593
- res.setStatus(404).json({ error: 'SLA config not found for worker' });
672
+ res.setStatus(400).json({ error: 'SLA config not found for worker' });
594
673
  } else {
595
674
  res.setStatus(500).json({ error: (error as Error).message });
596
675
  }
@@ -1257,65 +1336,63 @@ async function monitoringSummary(_req: IRequest, res: IResponse): Promise<void>
1257
1336
  }
1258
1337
  }
1259
1338
 
1339
+ const SSE_HEARTBEAT_INTERVAL = Env.SSE_HEARTBEAT_INTERVAL;
1340
+
1260
1341
  /**
1261
1342
  * SSE endpoint: stream worker and monitoring events
1262
1343
  * GET /api/workers/events
1263
1344
  */
1264
1345
  const eventsStream = async (_req: IRequest, res: IResponse): Promise<void> => {
1265
- const raw = res.getRaw();
1346
+ try {
1347
+ const raw = res.getRaw();
1348
+ raw.writeHead(200, {
1349
+ 'Content-Type': 'text/event-stream',
1350
+ 'Cache-Control': 'no-cache, no-transform',
1351
+ Connection: 'keep-alive',
1352
+ 'X-Accel-Buffering': 'no',
1353
+ });
1266
1354
 
1267
- raw.writeHead(200, {
1268
- 'Content-Type': 'text/event-stream',
1269
- 'Cache-Control': 'no-cache, no-transform',
1270
- Connection: 'keep-alive',
1271
- 'X-Accel-Buffering': 'no',
1272
- });
1355
+ let closed = false;
1273
1356
 
1274
- let closed = false;
1357
+ const send = (payload: unknown) => {
1358
+ if (closed) return;
1359
+ try {
1360
+ const data = JSON.stringify(payload);
1361
+ raw.write(`data: ${data}\n\n`);
1362
+ } catch (err) {
1363
+ Logger.error('WorkerController.eventsStream serialization failed', err);
1364
+ }
1365
+ };
1275
1366
 
1276
- const send = async (payload: unknown) => {
1277
- try {
1278
- const data = JSON.stringify(payload);
1279
- raw.write(`data: ${data}\n\n`);
1280
- } catch (err) {
1281
- Logger.error('WorkerController.eventsStream failed', err);
1282
- // ignore serialization errors
1283
- }
1284
- };
1285
-
1286
- // Send initial hello
1287
- await send({ type: 'hello', ts: new Date().toISOString() });
1288
-
1289
- // Periodic snapshot sender
1290
- const intervalMs = 5000;
1291
- const interval = setInterval(async () => {
1292
- try {
1293
- const monitoring = await HealthMonitor.getSummary();
1294
- // include full workers listing with metrics/pagination to allow clients to patch the UI
1295
- const workersPayload = await getWorkers({ page: 1, limit: 200 });
1296
- await send({
1297
- type: 'snapshot',
1298
- ts: new Date().toISOString(),
1299
- monitoring,
1300
- workers: workersPayload,
1301
- });
1302
- } catch (err) {
1303
- // send error event
1304
- await send({ type: 'error', ts: new Date().toISOString(), message: (err as Error).message });
1367
+ // Send hello immediately
1368
+ send({ type: 'hello', ts: new Date().toISOString() });
1369
+
1370
+ // Defined subscription callback
1371
+ const onSnapshot = (data: unknown) => {
1372
+ send(data);
1373
+ };
1374
+
1375
+ // Subscribe to centralized service
1376
+ WorkerMonitoringService.subscribe(onSnapshot);
1377
+
1378
+ // Heartbeat to keep connection alive
1379
+ const hb = setInterval(() => {
1380
+ if (!closed) raw.write(': ping\n\n');
1381
+ }, SSE_HEARTBEAT_INTERVAL);
1382
+
1383
+ // Clean up when client disconnects
1384
+ raw.on('close', () => {
1385
+ closed = true;
1386
+ clearInterval(hb);
1387
+ WorkerMonitoringService.unsubscribe(onSnapshot);
1388
+ });
1389
+ } catch (error) {
1390
+ Logger.error('WorkerController.eventsStream failed', error);
1391
+ const raw = res.getRaw && typeof res.getRaw === 'function' ? res.getRaw() : null;
1392
+ if (!raw?.headersSent) {
1393
+ res.setStatus(500).json({ error: (error as Error).message });
1305
1394
  }
1306
- }, intervalMs);
1307
-
1308
- // Heartbeat to keep connection alive
1309
- const hb = setInterval(() => {
1310
- if (!closed) raw.write(': ping\n\n');
1311
- }, 15000);
1312
-
1313
- // Clean up when client disconnects
1314
- raw.on('close', () => {
1315
- closed = true;
1316
- clearInterval(interval);
1317
- clearInterval(hb);
1318
- });
1395
+ }
1319
1396
  };
1320
1397
 
1321
1398
  /**
@@ -0,0 +1,77 @@
1
+ import { Logger, NodeSingletons, workersConfig } from '@zintrust/core';
2
+ import { HealthMonitor } from '../HealthMonitor';
3
+ import { getWorkers } from '../dashboard/workers-api';
4
+
5
+ type SnapshotData = {
6
+ type: string;
7
+ ts: string;
8
+ monitoring: unknown;
9
+ workers: unknown;
10
+ };
11
+
12
+ // Internal state
13
+ const emitter = new NodeSingletons.EventEmitter();
14
+ emitter.setMaxListeners(Infinity);
15
+ let interval: NodeJS.Timeout | null = null;
16
+ let subscribers = 0;
17
+ const INTERVAL_MS = workersConfig?.intervalMs || 5000;
18
+
19
+ const broadcastSnapshot = async (): Promise<void> => {
20
+ try {
21
+ if (subscribers <= 0) return;
22
+
23
+ const monitoring = await HealthMonitor.getSummary();
24
+ // Fetch full workers listing optimized for dashboard
25
+ const workersPayload = await getWorkers({ page: 1, limit: 200 });
26
+
27
+ const payload: SnapshotData = {
28
+ type: 'snapshot',
29
+ ts: new Date().toISOString(),
30
+ monitoring,
31
+ workers: workersPayload,
32
+ };
33
+
34
+ emitter.emit('snapshot', payload);
35
+ } catch (err) {
36
+ Logger.error('WorkerMonitoringService.broadcastSnapshot failed', err);
37
+ emitter.emit('error', err);
38
+ }
39
+ };
40
+
41
+ const startPolling = (): void => {
42
+ if (interval) return;
43
+
44
+ Logger.debug('Starting WorkerMonitoringService polling');
45
+ // Initial fetch
46
+ void broadcastSnapshot();
47
+
48
+ interval = setInterval(() => {
49
+ void broadcastSnapshot();
50
+ }, INTERVAL_MS);
51
+ };
52
+
53
+ const stopPolling = (): void => {
54
+ if (interval) {
55
+ Logger.debug('Stopping WorkerMonitoringService polling');
56
+ clearInterval(interval);
57
+ interval = null;
58
+ }
59
+ };
60
+
61
+ export const WorkerMonitoringService = Object.freeze({
62
+ subscribe(callback: (data: SnapshotData) => void): void {
63
+ emitter.on('snapshot', callback);
64
+ subscribers++;
65
+ if (subscribers === 1) {
66
+ startPolling();
67
+ }
68
+ },
69
+
70
+ unsubscribe(callback: (data: SnapshotData) => void): void {
71
+ emitter.off('snapshot', callback);
72
+ subscribers--;
73
+ if (subscribers <= 0) {
74
+ stopPolling();
75
+ }
76
+ },
77
+ });
@@ -348,7 +348,7 @@ export const ValidationSchemas = {
348
348
  },
349
349
  driver: {
350
350
  type: 'string' as const,
351
- allowedValues: ['db', 'redis', 'memory', ''],
351
+ allowedValues: ['db', 'database', 'redis', 'memory', ''],
352
352
  optional: true,
353
353
  },
354
354
  search: {
@@ -14,7 +14,7 @@ import { withWorkerNameValidation } from './WorkerNameSanitizer';
14
14
 
15
15
  /**
16
16
  * Composite middleware for worker edit validation
17
- * Maps processorPath to processor for validation and validates all editable fields
17
+ * Maps processorSpec to processor for validation and validates all editable fields
18
18
  */
19
19
  export const withEditWorkerValidation = (handler: RouteHandler): RouteHandler => {
20
20
  return async (req: IRequest, res: IResponse): Promise<void> => {
@@ -22,12 +22,12 @@ export const withEditWorkerValidation = (handler: RouteHandler): RouteHandler =>
22
22
  const data = req.data();
23
23
  const currentBody = req.getBody() as Record<string, unknown>;
24
24
 
25
- // Map processorPath to processor for validation if processorPath exists
25
+ // Map processorSpec/processorSpec to processor for validation if provided
26
26
  let mappedBody = { ...currentBody };
27
- if (data['processorPath'] && !data['processor']) {
27
+ if (data['processorSpec'] && !data['processor']) {
28
28
  mappedBody = {
29
29
  ...mappedBody,
30
- processor: data['processorPath'], // Map for validation
30
+ processor: data['processorSpec'],
31
31
  };
32
32
  }
33
33
 
@@ -39,8 +39,8 @@ export const withEditWorkerValidation = (handler: RouteHandler): RouteHandler =>
39
39
  [
40
40
  'name',
41
41
  'queueName',
42
- 'processor', // Validated field (mapped from processorPath)
43
- 'processorPath', // Original field
42
+ 'processor', // Validated field (mapped from processorSpec)
43
+ 'processorSpec',
44
44
  'version',
45
45
  'options', // Skip strict validation for editing
46
46
  'infrastructure',
@@ -49,6 +49,7 @@ export const withEditWorkerValidation = (handler: RouteHandler): RouteHandler =>
49
49
  'concurrency', // Original field
50
50
  'region',
51
51
  'autoStart',
52
+ 'activeStatus',
52
53
  'status',
53
54
  ],
54
55
  withProcessorPathValidation(