@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
@@ -1,8 +1,28 @@
1
- import { Logger, NodeSingletons, type IRequest, type IResponse } from '@zintrust/core';
1
+ import {
2
+ Logger,
3
+ NodeSingletons,
4
+ workersConfig,
5
+ type IRequest,
6
+ type IResponse,
7
+ } from '@zintrust/core';
2
8
 
3
9
  export type RouteHandler = (req: IRequest, res: IResponse) => Promise<void> | void;
4
10
 
5
- const PROCESSOR_PATH_PATTERN = /^[a-zA-Z0-9/_.-]+\.(ts|js)$/;
11
+ const PROCESSOR_PATH_PATTERN = /^[a-zA-Z0-9/_.-]+\.(ts|js|mjs|cjs)$/;
12
+
13
+ const isUrlSpec = (value: string): boolean => {
14
+ if (value.startsWith('url:')) return true;
15
+ return value.includes('://');
16
+ };
17
+
18
+ const normalizeUrlSpec = (value: string): string => {
19
+ return value.startsWith('url:') ? value.slice(4) : value;
20
+ };
21
+
22
+ const isAllowedRemoteHost = (host: string): boolean => {
23
+ const allowlist = workersConfig.processorSpec.remoteAllowlist;
24
+ return allowlist.map((value) => value.toLowerCase()).includes(host.toLowerCase());
25
+ };
6
26
 
7
27
  const decodeProcessorPath = (processor: string): string => {
8
28
  return processor
@@ -16,6 +36,84 @@ const decodeProcessorPath = (processor: string): string => {
16
36
  .replaceAll('%2D', '-'); // URL encoding for -
17
37
  };
18
38
 
39
+ const validateUrlSpec = (
40
+ processor: string
41
+ ): { isValid: boolean; error?: { error: string; code: string } } => {
42
+ const normalized = normalizeUrlSpec(processor);
43
+ let parsed: URL;
44
+
45
+ try {
46
+ parsed = new URL(normalized);
47
+ } catch {
48
+ return {
49
+ isValid: false,
50
+ error: { error: 'Invalid processor url', code: 'INVALID_PROCESSOR_URL' },
51
+ };
52
+ }
53
+
54
+ if (parsed.protocol === 'file:') {
55
+ const path = NodeSingletons.path;
56
+ const baseDir = path.resolve(process.cwd());
57
+ const resolved = path.resolve(baseDir, decodeURIComponent(parsed.pathname));
58
+
59
+ if (!resolved.startsWith(baseDir)) {
60
+ return {
61
+ isValid: false,
62
+ error: { error: 'Invalid processor path', code: 'INVALID_PROCESSOR_PATH_TRAVERSAL' },
63
+ };
64
+ }
65
+ } else {
66
+ if (parsed.protocol !== 'https:') {
67
+ return {
68
+ isValid: false,
69
+ error: { error: 'Invalid processor url', code: 'INVALID_PROCESSOR_URL' },
70
+ };
71
+ }
72
+
73
+ if (!isAllowedRemoteHost(parsed.host)) {
74
+ return {
75
+ isValid: false,
76
+ error: { error: 'Invalid processor url host', code: 'INVALID_PROCESSOR_URL_HOST' },
77
+ };
78
+ }
79
+ }
80
+
81
+ return { isValid: true };
82
+ };
83
+
84
+ const validateRelativePath = (
85
+ processor: string
86
+ ): { isValid: boolean; error?: { error: string; code: string } } => {
87
+ if (processor.includes('..') || processor.startsWith('/')) {
88
+ return {
89
+ isValid: false,
90
+ error: { error: 'Invalid processor path', code: 'INVALID_PROCESSOR_PATH' },
91
+ };
92
+ }
93
+
94
+ if (!PROCESSOR_PATH_PATTERN.test(processor)) {
95
+ return {
96
+ isValid: false,
97
+ error: { error: 'Invalid processor path', code: 'INVALID_PROCESSOR_EXTENSION' },
98
+ };
99
+ }
100
+
101
+ return { isValid: true };
102
+ };
103
+
104
+ const sanitizeAndResolvePath = (processor: string): { isValid: boolean; sanitized: string } => {
105
+ const sanitizedProcessor = processor.replaceAll(/[^a-zA-Z0-9/_.-]/g, '');
106
+ const path = NodeSingletons.path;
107
+ const baseDir = path.resolve(process.cwd());
108
+ const resolved = path.resolve(baseDir, sanitizedProcessor);
109
+
110
+ if (!resolved.startsWith(baseDir)) {
111
+ return { isValid: false, sanitized: processor };
112
+ }
113
+
114
+ return { isValid: true, sanitized: sanitizedProcessor };
115
+ };
116
+
19
117
  export const withProcessorPathValidation = (handler: RouteHandler): RouteHandler => {
20
118
  return async (req: IRequest, res: IResponse): Promise<void> => {
21
119
  try {
@@ -24,8 +122,8 @@ export const withProcessorPathValidation = (handler: RouteHandler): RouteHandler
24
122
 
25
123
  if (!processor) {
26
124
  return res.setStatus(400).json({
27
- error: 'Processor path is required',
28
- code: 'MISSING_PROCESSOR_PATH',
125
+ error: 'Processor spec is required',
126
+ code: 'MISSING_PROCESSOR_SPEC',
29
127
  });
30
128
  }
31
129
 
@@ -35,44 +133,33 @@ export const withProcessorPathValidation = (handler: RouteHandler): RouteHandler
35
133
  // Trim whitespace
36
134
  processor = processor.trim();
37
135
 
38
- // Prevent path traversal
39
- if (processor.includes('..') || processor.startsWith('/')) {
40
- return res.setStatus(400).json({
41
- error: 'Invalid processor path',
42
- message: 'Processor path must be relative and cannot contain path traversal',
43
- code: 'INVALID_PROCESSOR_PATH',
44
- });
45
- }
136
+ const isUrl = isUrlSpec(processor);
137
+ let validation: { isValid: boolean; error?: { error: string; code: string } };
46
138
 
47
- if (!PROCESSOR_PATH_PATTERN.test(processor)) {
48
- Logger.error('Processor path validation failed', {
49
- processor,
50
- pattern: PROCESSOR_PATH_PATTERN.toString(),
51
- testResult: PROCESSOR_PATH_PATTERN.test(processor),
52
- });
53
- return res.setStatus(400).json({
54
- error: 'Invalid processor path',
55
- message: `Processor must be a TypeScript or JavaScript file. Got: "${processor}"`,
56
- code: 'INVALID_PROCESSOR_EXTENSION',
57
- });
139
+ if (isUrl) {
140
+ validation = validateUrlSpec(processor);
141
+ } else {
142
+ validation = validateRelativePath(processor);
143
+
144
+ if (validation.isValid) {
145
+ const pathValidation = sanitizeAndResolvePath(processor);
146
+ if (pathValidation.isValid) {
147
+ processor = pathValidation.sanitized;
148
+ } else {
149
+ validation = {
150
+ isValid: false,
151
+ error: { error: 'Invalid processor path', code: 'INVALID_PROCESSOR_PATH_TRAVERSAL' },
152
+ };
153
+ }
154
+ }
58
155
  }
59
156
 
60
- // Sanitize the processor path (remove any invalid characters just in case)
61
- const sanitizedProcessor = processor.replaceAll(/[^a-zA-Z0-9/_.-]/g, '');
62
- const path = NodeSingletons.path;
63
- // Ensure resolved path stays within repository/app scope
64
- const BASE_PROCESSOR_DIR = path.resolve(process.cwd());
65
- const resolved = path.resolve(BASE_PROCESSOR_DIR, sanitizedProcessor);
66
- if (!resolved.startsWith(BASE_PROCESSOR_DIR)) {
67
- return res.setStatus(400).json({
68
- error: 'Invalid processor path',
69
- message: 'Processor path resolves outside of allowed base directory',
70
- code: 'INVALID_PROCESSOR_PATH_TRAVERSAL',
71
- });
157
+ if (!validation.isValid) {
158
+ return res.setStatus(400).json(validation.error);
72
159
  }
73
160
 
74
161
  const currentBody = req.getBody() as Record<string, unknown>;
75
- req.setBody({ ...currentBody, processor: sanitizedProcessor });
162
+ req.setBody({ ...currentBody, processor });
76
163
 
77
164
  return handler(req, res);
78
165
  } catch (error) {
@@ -28,6 +28,7 @@ export const withCreateWorkerValidation = (handler: RouteHandler): RouteHandler
28
28
  'infrastructure',
29
29
  'features',
30
30
  'datacenter',
31
+ 'activeStatus',
31
32
  ],
32
33
  withProcessorPathValidation(
33
34
  withWorkerNameValidation(
package/src/index.ts CHANGED
@@ -39,9 +39,14 @@ export { WorkerVersioning } from './WorkerVersioning';
39
39
 
40
40
  // Factory & Lifecycle
41
41
  export { WorkerFactory } from './WorkerFactory';
42
- export type { WorkerPersistenceConfig } from './WorkerFactory';
42
+ export type {
43
+ ProcessorResolver,
44
+ WorkerFactoryConfig,
45
+ WorkerPersistenceConfig,
46
+ } from './WorkerFactory';
43
47
  export { WorkerInit } from './WorkerInit';
44
48
  export { WorkerShutdown } from './WorkerShutdown';
49
+ export { ZinTrustWorkerShutdownDurableObject } from './WorkerShutdownDurableObject';
45
50
 
46
51
  // HTTP Controllers & Routes
47
52
  export { WorkerController } from './http/WorkerController';
@@ -3,11 +3,10 @@
3
3
  * HTTP API for managing workers with dashboard functionality
4
4
  */
5
5
 
6
- import type { IRouter } from '@zintrust/core';
6
+ import type { IRequest, IResponse, IRouter } from '@zintrust/core';
7
7
  import { Logger, Router } from '@zintrust/core';
8
8
  import { type WorkersDashboardUiOptions } from '../dashboard';
9
- import { WorkerApiController } from '../http/WorkerApiController';
10
- import { WorkerController } from '../http/WorkerController';
9
+ import { HealthMonitor } from '../HealthMonitor';
11
10
  import { ValidationSchemas, withCustomValidation } from '../http/middleware/CustomValidation';
12
11
  import { withEditWorkerValidation } from '../http/middleware/EditWorkerValidation';
13
12
  import { withDriverValidation } from '../http/middleware/ValidateDriver';
@@ -15,8 +14,12 @@ import {
15
14
  withCreateWorkerValidation,
16
15
  withWorkerOperationValidation,
17
16
  } from '../http/middleware/WorkerValidationChain';
17
+ import { WorkerApiController } from '../http/WorkerApiController';
18
+ import { WorkerController } from '../http/WorkerController';
19
+ import { ResourceMonitor } from '../ResourceMonitor';
20
+ import { TelemetryDashboard } from '../telemetry';
18
21
  import { registerStaticAssets } from '../ui/router/ui';
19
-
22
+ import { WorkerFactory } from '../WorkerFactory';
20
23
  type WorkerUiOptions = WorkersDashboardUiOptions;
21
24
  type RouteOptions = { middleware?: ReadonlyArray<string> } | undefined;
22
25
 
@@ -28,7 +31,7 @@ function registerCoreWorkerRoutes(r: IRouter): void {
28
31
  Router.post(r, '/create', withCreateWorkerValidation(controller.create));
29
32
  Router.put(r, '/:name', withCreateWorkerValidation(controller.update));
30
33
 
31
- // Worker editing with custom validation that handles processorPath mapping
34
+ // Worker editing with custom validation that handles mapping
32
35
  Router.put(r, '/:name/edit', withEditWorkerValidation(controller.update));
33
36
  Router.post(
34
37
  r,
@@ -96,6 +99,9 @@ function registerWorkerQueryRoutes(r: IRouter): void {
96
99
  }
97
100
 
98
101
  function registerMonitoringRoutes(r: IRouter): void {
102
+ // SSE events stream for monitoring + workers snapshot
103
+ Router.get(r, '/events', controller.eventsStream);
104
+
99
105
  // Health monitoring
100
106
  Router.post(r, '/:name/monitoring/start', controller.startMonitoring);
101
107
  Router.post(r, '/:name/monitoring/stop', controller.stopMonitoring);
@@ -105,9 +111,6 @@ function registerMonitoringRoutes(r: IRouter): void {
105
111
 
106
112
  // SLA monitoring
107
113
  Router.get(r, '/:name/sla/status', controller.getSlaStatus);
108
-
109
- // SSE events stream for monitoring + workers snapshot
110
- Router.get(r, '/events', controller.eventsStream);
111
114
  }
112
115
 
113
116
  function registerVersioningRoutes(r: IRouter): void {
@@ -131,9 +134,9 @@ function registerWorkerLifecycleRoutes(router: IRouter, middleware?: ReadonlyArr
131
134
  (r: IRouter) => {
132
135
  Logger.info('Registering Worker Management Routes');
133
136
 
137
+ registerMonitoringRoutes(r); // ← Move FIRST - has /events
134
138
  registerCoreWorkerRoutes(r);
135
139
  registerWorkerQueryRoutes(r);
136
- registerMonitoringRoutes(r);
137
140
  registerVersioningRoutes(r);
138
141
  registerUtilityRoutes(r);
139
142
  },
@@ -141,6 +144,52 @@ function registerWorkerLifecycleRoutes(router: IRouter, middleware?: ReadonlyArr
141
144
  );
142
145
  }
143
146
 
147
+ function registerWorkerTelemetryRoutes(router: IRouter, middleware?: ReadonlyArray<string>): void {
148
+ const options = middleware ? { middleware } : undefined;
149
+
150
+ Router.group(
151
+ router,
152
+ '/api',
153
+ (r: IRouter) => {
154
+ Router.get(r, '/workers/system/summary', async (_req: IRequest, res: IResponse) => {
155
+ const workers = WorkerFactory.list();
156
+ const monitoringSummary = await HealthMonitor.getSummary();
157
+ const resourceUsage = ResourceMonitor.getCurrentUsage('system');
158
+
159
+ res.json({
160
+ ok: true,
161
+ summary: {
162
+ workers: workers.length,
163
+ monitoring: monitoringSummary,
164
+ resources: resourceUsage,
165
+ },
166
+ });
167
+ });
168
+
169
+ Router.get(
170
+ r,
171
+ '/workers/system/monitoring/summary',
172
+ async (_req: IRequest, res: IResponse) => {
173
+ const summary = await HealthMonitor.getSummary();
174
+ res.json({ ok: true, summary });
175
+ }
176
+ );
177
+
178
+ Router.get(r, '/resources/current', async (_req: IRequest, res: IResponse) => {
179
+ const usage = ResourceMonitor.getCurrentUsage('system');
180
+ res.json({ ok: true, usage });
181
+ });
182
+
183
+ Router.get(r, '/resources/trends', async (req: IRequest, res: IResponse) => {
184
+ const period = (req.getParam('period') ?? 'day') as 'hour' | 'day' | 'week';
185
+ const trends = ResourceMonitor.getAllTrends('system', period);
186
+ res.json({ ok: true, trends });
187
+ });
188
+ },
189
+ options
190
+ );
191
+ }
192
+
144
193
  export function registerWorkerRoutes(
145
194
  router: IRouter,
146
195
  _options?: WorkerUiOptions,
@@ -148,7 +197,15 @@ export function registerWorkerRoutes(
148
197
  ): void {
149
198
  registerStaticAssets(router, routeOptions?.middleware ?? []);
150
199
  registerWorkerLifecycleRoutes(router, routeOptions?.middleware);
200
+ registerWorkerTelemetryRoutes(router, routeOptions?.middleware);
201
+
202
+ // Register Telemetry Dashboard
203
+ const dashboard = TelemetryDashboard.create({
204
+ basePath: '/telemetry',
205
+ });
206
+ dashboard.registerRoutes(router);
151
207
  Logger.info('Worker routes registered at http://127.0.0.1:7777/workers');
208
+ Logger.info('Telemetry dashboard registered at http://127.0.0.1:7777/telemetry');
152
209
  }
153
210
 
154
211
  export default registerWorkerRoutes;
@@ -14,7 +14,8 @@ export type WorkerRecord = {
14
14
  autoStart: boolean;
15
15
  concurrency: number;
16
16
  region: string | null;
17
- processorPath?: string | null;
17
+ processorSpec?: string | null;
18
+ activeStatus?: boolean;
18
19
  features?: Record<string, unknown> | null;
19
20
  infrastructure?: Record<string, unknown> | null;
20
21
  datacenter?: Record<string, unknown> | null;
@@ -27,10 +28,16 @@ export type WorkerRecord = {
27
28
 
28
29
  export type WorkerStore = {
29
30
  init(): Promise<void>;
30
- list(options?: { offset?: number; limit?: number; search?: string }): Promise<WorkerRecord[]>;
31
+ list(options?: {
32
+ offset?: number;
33
+ limit?: number;
34
+ search?: string;
35
+ includeInactive?: boolean;
36
+ }): Promise<WorkerRecord[]>;
31
37
  get(name: string): Promise<WorkerRecord | null>;
32
38
  save(record: WorkerRecord): Promise<void>;
33
39
  update(name: string, patch: Partial<WorkerRecord>): Promise<void>;
40
+ updateMany?: (names: string[], patch: Partial<WorkerRecord>) => Promise<void>;
34
41
  remove(name: string): Promise<void>;
35
42
  };
36
43
 
@@ -42,6 +49,12 @@ const mergeRecord = (current: WorkerRecord, patch: Partial<WorkerRecord>): Worke
42
49
  updatedAt: patch.updatedAt ?? now(),
43
50
  });
44
51
 
52
+ const toSqlDateTime = (value: Date | undefined | null): string | null => {
53
+ if (!value) return null;
54
+ // Use UTC and drop timezone for SQL DATETIME compatibility
55
+ return value.toISOString().slice(0, 19).replace('T', ' ');
56
+ };
57
+
45
58
  const serializeDbWorker = (record: WorkerRecord): Record<string, unknown> => ({
46
59
  name: record.name,
47
60
  queue_name: record.queueName,
@@ -50,13 +63,14 @@ const serializeDbWorker = (record: WorkerRecord): Record<string, unknown> => ({
50
63
  auto_start: record.autoStart,
51
64
  concurrency: record.concurrency,
52
65
  region: record.region,
53
- processor_path: record.processorPath ?? null,
66
+ processor_spec: record.processorSpec ?? null,
67
+ active_status: record.activeStatus ?? true,
54
68
  features: record.features ? JSON.stringify(record.features) : null,
55
69
  infrastructure: record.infrastructure ? JSON.stringify(record.infrastructure) : null,
56
70
  datacenter: record.datacenter ? JSON.stringify(record.datacenter) : null,
57
- created_at: record.createdAt,
58
- updated_at: record.updatedAt,
59
- last_health_check: record.lastHealthCheck ?? null,
71
+ created_at: toSqlDateTime(record.createdAt),
72
+ updated_at: toSqlDateTime(record.updatedAt),
73
+ last_health_check: toSqlDateTime(record.lastHealthCheck ?? null),
60
74
  last_error: record.lastError ?? null,
61
75
  connection_state: record.connectionState ?? null,
62
76
  });
@@ -88,7 +102,8 @@ const deserializeDbWorker = (row: Record<string, unknown>): WorkerRecord => {
88
102
  autoStart: Boolean(row['auto_start'] ?? false),
89
103
  concurrency: Number(row['concurrency'] ?? 0),
90
104
  region: row['region'] ? String(row['region']) : null,
91
- processorPath: row['processor_path'] ? String(row['processor_path']) : null,
105
+ processorSpec: String(row['processor_spec']),
106
+ activeStatus: row['active_status'] === undefined ? true : Boolean(row['active_status']),
92
107
  features: parseJson(row['features']),
93
108
  infrastructure: parseJson(row['infrastructure']),
94
109
  datacenter: parseJson(row['datacenter']),
@@ -132,6 +147,13 @@ export const InMemoryWorkerStore = Object.freeze({
132
147
  if (!current) return;
133
148
  store.set(name, mergeRecord(current, patch));
134
149
  },
150
+ async updateMany(names: string[], patch: Partial<WorkerRecord>): Promise<void> {
151
+ for (const name of names) {
152
+ const current = store.get(name);
153
+ if (!current) continue;
154
+ store.set(name, mergeRecord(current, patch));
155
+ }
156
+ },
135
157
  async remove(name: string): Promise<void> {
136
158
  store.delete(name);
137
159
  },
@@ -168,7 +190,7 @@ export const RedisWorkerStore = Object.freeze({
168
190
  },
169
191
  async list(options?: { offset?: number; limit?: number }): Promise<WorkerRecord[]> {
170
192
  const all = await client.hgetall(key);
171
- let values = Object.values(all).map(deserialize);
193
+ let values = Object.values(all).map((element) => deserialize(element));
172
194
  values.sort((a, b) => a.name.localeCompare(b.name));
173
195
  if (options) {
174
196
  const start = options.offset || 0;
@@ -189,6 +211,19 @@ export const RedisWorkerStore = Object.freeze({
189
211
  if (!current) return;
190
212
  await client.hset(key, name, serialize(mergeRecord(current, patch)));
191
213
  },
214
+ async updateMany(names: string[], patch: Partial<WorkerRecord>): Promise<void> {
215
+ if (names.length === 0) return;
216
+ const entries = await client.hmget(key, ...names);
217
+ const updates: Array<string> = [];
218
+ entries.forEach((raw, index) => {
219
+ if (!raw) return;
220
+ const current = deserialize(raw);
221
+ const updated = mergeRecord(current, patch);
222
+ updates.push(names[index] as string, serialize(updated));
223
+ });
224
+ if (updates.length === 0) return;
225
+ await client.hset(key, ...updates);
226
+ },
192
227
  async remove(name: string): Promise<void> {
193
228
  await client.hdel(key, name);
194
229
  },
@@ -207,7 +242,7 @@ export const DbWorkerStore = Object.freeze({
207
242
  if (options?.limit) query.limit(options.limit);
208
243
  if (options?.offset) query.offset(options.offset);
209
244
  const rows = await query.get<Record<string, unknown>>();
210
- return rows.map(deserializeDbWorker);
245
+ return rows.map((element) => deserializeDbWorker(element));
211
246
  },
212
247
  async get(name: string): Promise<WorkerRecord | null> {
213
248
  const row = await db.table(table).where('name', '=', name).first<Record<string, unknown>>();
@@ -232,6 +267,21 @@ export const DbWorkerStore = Object.freeze({
232
267
  const updated = mergeRecord(current, patch);
233
268
  await db.table(table).where('name', '=', name).update(serializeDbWorker(updated));
234
269
  },
270
+ async updateMany(names: string[], patch: Partial<WorkerRecord>): Promise<void> {
271
+ if (names.length === 0) return;
272
+ const update: Record<string, unknown> = {
273
+ updated_at: toSqlDateTime(patch.updatedAt ?? now()),
274
+ };
275
+
276
+ if (patch.status !== undefined) update['status'] = patch.status;
277
+ if (patch.lastError !== undefined) update['last_error'] = patch.lastError ?? null;
278
+ if (patch.lastHealthCheck !== undefined)
279
+ update['last_health_check'] = toSqlDateTime(patch.lastHealthCheck ?? null);
280
+ if (patch.connectionState !== undefined)
281
+ update['connection_state'] = patch.connectionState ?? null;
282
+
283
+ await db.table(table).whereIn('name', names).update(update);
284
+ },
235
285
  async remove(name: string): Promise<void> {
236
286
  await db.table(table).where('name', '=', name).delete();
237
287
  },