@zintrust/workers 0.1.27

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 (178) hide show
  1. package/README.md +861 -0
  2. package/dist/AnomalyDetection.d.ts +102 -0
  3. package/dist/AnomalyDetection.js +321 -0
  4. package/dist/AutoScaler.d.ts +127 -0
  5. package/dist/AutoScaler.js +425 -0
  6. package/dist/BroadcastWorker.d.ts +21 -0
  7. package/dist/BroadcastWorker.js +24 -0
  8. package/dist/CanaryController.d.ts +103 -0
  9. package/dist/CanaryController.js +380 -0
  10. package/dist/ChaosEngineering.d.ts +79 -0
  11. package/dist/ChaosEngineering.js +216 -0
  12. package/dist/CircuitBreaker.d.ts +106 -0
  13. package/dist/CircuitBreaker.js +374 -0
  14. package/dist/ClusterLock.d.ts +90 -0
  15. package/dist/ClusterLock.js +385 -0
  16. package/dist/ComplianceManager.d.ts +177 -0
  17. package/dist/ComplianceManager.js +556 -0
  18. package/dist/DatacenterOrchestrator.d.ts +133 -0
  19. package/dist/DatacenterOrchestrator.js +404 -0
  20. package/dist/DeadLetterQueue.d.ts +122 -0
  21. package/dist/DeadLetterQueue.js +539 -0
  22. package/dist/HealthMonitor.d.ts +42 -0
  23. package/dist/HealthMonitor.js +301 -0
  24. package/dist/MultiQueueWorker.d.ts +89 -0
  25. package/dist/MultiQueueWorker.js +277 -0
  26. package/dist/NotificationWorker.d.ts +21 -0
  27. package/dist/NotificationWorker.js +23 -0
  28. package/dist/Observability.d.ts +153 -0
  29. package/dist/Observability.js +530 -0
  30. package/dist/PluginManager.d.ts +123 -0
  31. package/dist/PluginManager.js +392 -0
  32. package/dist/PriorityQueue.d.ts +117 -0
  33. package/dist/PriorityQueue.js +244 -0
  34. package/dist/ResourceMonitor.d.ts +164 -0
  35. package/dist/ResourceMonitor.js +605 -0
  36. package/dist/SLAMonitor.d.ts +110 -0
  37. package/dist/SLAMonitor.js +274 -0
  38. package/dist/WorkerFactory.d.ts +193 -0
  39. package/dist/WorkerFactory.js +1507 -0
  40. package/dist/WorkerInit.d.ts +85 -0
  41. package/dist/WorkerInit.js +223 -0
  42. package/dist/WorkerMetrics.d.ts +114 -0
  43. package/dist/WorkerMetrics.js +509 -0
  44. package/dist/WorkerRegistry.d.ts +145 -0
  45. package/dist/WorkerRegistry.js +319 -0
  46. package/dist/WorkerShutdown.d.ts +61 -0
  47. package/dist/WorkerShutdown.js +159 -0
  48. package/dist/WorkerVersioning.d.ts +107 -0
  49. package/dist/WorkerVersioning.js +300 -0
  50. package/dist/build-manifest.json +462 -0
  51. package/dist/config/workerConfig.d.ts +3 -0
  52. package/dist/config/workerConfig.js +19 -0
  53. package/dist/createQueueWorker.d.ts +23 -0
  54. package/dist/createQueueWorker.js +113 -0
  55. package/dist/dashboard/index.d.ts +1 -0
  56. package/dist/dashboard/index.js +1 -0
  57. package/dist/dashboard/types.d.ts +117 -0
  58. package/dist/dashboard/types.js +1 -0
  59. package/dist/dashboard/workers-api.d.ts +4 -0
  60. package/dist/dashboard/workers-api.js +638 -0
  61. package/dist/dashboard/workers-dashboard-ui.d.ts +3 -0
  62. package/dist/dashboard/workers-dashboard-ui.js +1026 -0
  63. package/dist/dashboard/workers-dashboard.d.ts +4 -0
  64. package/dist/dashboard/workers-dashboard.js +904 -0
  65. package/dist/helper/index.d.ts +5 -0
  66. package/dist/helper/index.js +10 -0
  67. package/dist/http/WorkerApiController.d.ts +38 -0
  68. package/dist/http/WorkerApiController.js +312 -0
  69. package/dist/http/WorkerController.d.ts +374 -0
  70. package/dist/http/WorkerController.js +1351 -0
  71. package/dist/http/middleware/CustomValidation.d.ts +92 -0
  72. package/dist/http/middleware/CustomValidation.js +270 -0
  73. package/dist/http/middleware/DatacenterValidator.d.ts +3 -0
  74. package/dist/http/middleware/DatacenterValidator.js +94 -0
  75. package/dist/http/middleware/EditWorkerValidation.d.ts +7 -0
  76. package/dist/http/middleware/EditWorkerValidation.js +55 -0
  77. package/dist/http/middleware/FeaturesValidator.d.ts +3 -0
  78. package/dist/http/middleware/FeaturesValidator.js +60 -0
  79. package/dist/http/middleware/InfrastructureValidator.d.ts +31 -0
  80. package/dist/http/middleware/InfrastructureValidator.js +226 -0
  81. package/dist/http/middleware/OptionsValidator.d.ts +3 -0
  82. package/dist/http/middleware/OptionsValidator.js +112 -0
  83. package/dist/http/middleware/PayloadSanitizer.d.ts +7 -0
  84. package/dist/http/middleware/PayloadSanitizer.js +42 -0
  85. package/dist/http/middleware/ProcessorPathSanitizer.d.ts +3 -0
  86. package/dist/http/middleware/ProcessorPathSanitizer.js +74 -0
  87. package/dist/http/middleware/QueueNameSanitizer.d.ts +3 -0
  88. package/dist/http/middleware/QueueNameSanitizer.js +45 -0
  89. package/dist/http/middleware/ValidateDriver.d.ts +7 -0
  90. package/dist/http/middleware/ValidateDriver.js +20 -0
  91. package/dist/http/middleware/VersionSanitizer.d.ts +3 -0
  92. package/dist/http/middleware/VersionSanitizer.js +25 -0
  93. package/dist/http/middleware/WorkerNameSanitizer.d.ts +3 -0
  94. package/dist/http/middleware/WorkerNameSanitizer.js +46 -0
  95. package/dist/http/middleware/WorkerValidationChain.d.ts +27 -0
  96. package/dist/http/middleware/WorkerValidationChain.js +185 -0
  97. package/dist/index.d.ts +46 -0
  98. package/dist/index.js +48 -0
  99. package/dist/routes/workers.d.ts +12 -0
  100. package/dist/routes/workers.js +81 -0
  101. package/dist/storage/WorkerStore.d.ts +45 -0
  102. package/dist/storage/WorkerStore.js +195 -0
  103. package/dist/type.d.ts +76 -0
  104. package/dist/type.js +1 -0
  105. package/dist/ui/router/ui.d.ts +3 -0
  106. package/dist/ui/router/ui.js +83 -0
  107. package/dist/ui/types/worker-ui.d.ts +229 -0
  108. package/dist/ui/types/worker-ui.js +5 -0
  109. package/package.json +53 -0
  110. package/src/AnomalyDetection.ts +434 -0
  111. package/src/AutoScaler.ts +654 -0
  112. package/src/BroadcastWorker.ts +34 -0
  113. package/src/CanaryController.ts +531 -0
  114. package/src/ChaosEngineering.ts +301 -0
  115. package/src/CircuitBreaker.ts +495 -0
  116. package/src/ClusterLock.ts +499 -0
  117. package/src/ComplianceManager.ts +815 -0
  118. package/src/DatacenterOrchestrator.ts +561 -0
  119. package/src/DeadLetterQueue.ts +733 -0
  120. package/src/HealthMonitor.ts +390 -0
  121. package/src/MultiQueueWorker.ts +431 -0
  122. package/src/NotificationWorker.ts +33 -0
  123. package/src/Observability.ts +696 -0
  124. package/src/PluginManager.ts +551 -0
  125. package/src/PriorityQueue.ts +351 -0
  126. package/src/ResourceMonitor.ts +769 -0
  127. package/src/SLAMonitor.ts +408 -0
  128. package/src/WorkerFactory.ts +2108 -0
  129. package/src/WorkerInit.ts +313 -0
  130. package/src/WorkerMetrics.ts +709 -0
  131. package/src/WorkerRegistry.ts +443 -0
  132. package/src/WorkerShutdown.ts +210 -0
  133. package/src/WorkerVersioning.ts +422 -0
  134. package/src/config/workerConfig.ts +25 -0
  135. package/src/createQueueWorker.ts +174 -0
  136. package/src/dashboard/index.ts +6 -0
  137. package/src/dashboard/types.ts +141 -0
  138. package/src/dashboard/workers-api.ts +785 -0
  139. package/src/dashboard/zintrust.svg +30 -0
  140. package/src/helper/index.ts +11 -0
  141. package/src/http/WorkerApiController.ts +369 -0
  142. package/src/http/WorkerController.ts +1512 -0
  143. package/src/http/middleware/CustomValidation.ts +360 -0
  144. package/src/http/middleware/DatacenterValidator.ts +124 -0
  145. package/src/http/middleware/EditWorkerValidation.ts +74 -0
  146. package/src/http/middleware/FeaturesValidator.ts +82 -0
  147. package/src/http/middleware/InfrastructureValidator.ts +295 -0
  148. package/src/http/middleware/OptionsValidator.ts +144 -0
  149. package/src/http/middleware/PayloadSanitizer.ts +52 -0
  150. package/src/http/middleware/ProcessorPathSanitizer.ts +86 -0
  151. package/src/http/middleware/QueueNameSanitizer.ts +55 -0
  152. package/src/http/middleware/ValidateDriver.ts +29 -0
  153. package/src/http/middleware/VersionSanitizer.ts +30 -0
  154. package/src/http/middleware/WorkerNameSanitizer.ts +56 -0
  155. package/src/http/middleware/WorkerValidationChain.ts +230 -0
  156. package/src/index.ts +98 -0
  157. package/src/routes/workers.ts +154 -0
  158. package/src/storage/WorkerStore.ts +240 -0
  159. package/src/type.ts +89 -0
  160. package/src/types/queue-monitor.d.ts +38 -0
  161. package/src/types/queue-redis.d.ts +38 -0
  162. package/src/ui/README.md +13 -0
  163. package/src/ui/components/JsonEditor.js +670 -0
  164. package/src/ui/components/JsonViewer.js +387 -0
  165. package/src/ui/components/WorkerCard.js +178 -0
  166. package/src/ui/components/WorkerExpandPanel.js +257 -0
  167. package/src/ui/components/fetcher.js +42 -0
  168. package/src/ui/components/sla-scorecard.js +32 -0
  169. package/src/ui/components/styles.css +30 -0
  170. package/src/ui/components/table-expander.js +34 -0
  171. package/src/ui/integration/worker-ui-integration.js +565 -0
  172. package/src/ui/router/ui.ts +99 -0
  173. package/src/ui/services/workerApi.js +240 -0
  174. package/src/ui/types/worker-ui.ts +283 -0
  175. package/src/ui/utils/jsonValidator.js +444 -0
  176. package/src/ui/workers/index.html +202 -0
  177. package/src/ui/workers/main.js +1781 -0
  178. package/src/ui/workers/styles.css +1350 -0
@@ -0,0 +1,785 @@
1
+ import { ErrorFactory, Logger } from '@zintrust/core';
2
+ import { WorkerFactory } from '../WorkerFactory';
3
+ import { WorkerMetrics as WorkerMetricsManager } from '../WorkerMetrics';
4
+ import type { WorkerRecord } from '../storage/WorkerStore';
5
+ import type {
6
+ GetWorkersQuery,
7
+ QueueData,
8
+ RawWorkerData,
9
+ WorkerConfiguration,
10
+ WorkerData,
11
+ WorkerDriver,
12
+ WorkerHealth,
13
+ WorkerHealthCheckStatus,
14
+ WorkerHealthStatus,
15
+ WorkerMetrics,
16
+ WorkersListResponse,
17
+ } from './types';
18
+
19
+ const DEFAULT_PAGE_SIZE = 100;
20
+ const MAX_PAGE_SIZE = 200;
21
+
22
+ type PersistenceResult = {
23
+ workers: WorkerData[];
24
+ total: number;
25
+ drivers: WorkerDriver[];
26
+ effectiveLimit: number;
27
+ prePaginated: boolean;
28
+ };
29
+
30
+ export async function getWorkers(query: GetWorkersQuery): Promise<WorkersListResponse> {
31
+ const page = Math.max(1, query.page || 1);
32
+ const limit = Math.min(MAX_PAGE_SIZE, Math.max(1, query.limit || DEFAULT_PAGE_SIZE));
33
+ const offset = (page - 1) * limit;
34
+
35
+ // Get workers from persistence based on configuration
36
+ const persistence = await getWorkersFromPersistence(page, limit, query.driver);
37
+
38
+ // Apply filters
39
+ let filteredWorkers = applyFilters(persistence.workers, query);
40
+
41
+ // Apply search
42
+ if (query.search) {
43
+ filteredWorkers = applySearch(filteredWorkers, query.search);
44
+ }
45
+
46
+ // Apply sorting
47
+ filteredWorkers = applySorting(filteredWorkers, query.sortBy, query.sortOrder);
48
+
49
+ // Get queue data
50
+ const queueData = await getQueueData();
51
+
52
+ // Apply pagination
53
+ const paginatedWorkers = persistence.prePaginated
54
+ ? filteredWorkers
55
+ : filteredWorkers.slice(offset, offset + persistence.effectiveLimit);
56
+
57
+ const workersWithMetrics = await enrichWithMetrics(paginatedWorkers);
58
+
59
+ // Include details if requested
60
+ if (query.includeDetails) {
61
+ const enrichedWorkers = await enrichWithDetails(workersWithMetrics);
62
+ return {
63
+ workers: enrichedWorkers,
64
+ queueData,
65
+ pagination: {
66
+ page,
67
+ limit: persistence.effectiveLimit,
68
+ total: persistence.prePaginated ? persistence.total : filteredWorkers.length,
69
+ totalPages: Math.ceil(
70
+ (persistence.prePaginated ? persistence.total : filteredWorkers.length) /
71
+ persistence.effectiveLimit
72
+ ),
73
+ hasNext:
74
+ offset + persistence.effectiveLimit <
75
+ (persistence.prePaginated ? persistence.total : filteredWorkers.length),
76
+ hasPrev: page > 1,
77
+ },
78
+ drivers: persistence.drivers,
79
+ };
80
+ }
81
+
82
+ return {
83
+ workers: workersWithMetrics,
84
+ queueData,
85
+ pagination: {
86
+ page,
87
+ limit: persistence.effectiveLimit,
88
+ total: persistence.prePaginated ? persistence.total : filteredWorkers.length,
89
+ totalPages: Math.ceil(
90
+ (persistence.prePaginated ? persistence.total : filteredWorkers.length) /
91
+ persistence.effectiveLimit
92
+ ),
93
+ hasNext:
94
+ offset + persistence.effectiveLimit <
95
+ (persistence.prePaginated ? persistence.total : filteredWorkers.length),
96
+ hasPrev: page > 1,
97
+ },
98
+ drivers: persistence.drivers,
99
+ };
100
+ }
101
+
102
+ async function getWorkersFromPersistence(
103
+ page: number,
104
+ limit: number,
105
+ driverFilter?: WorkerDriver
106
+ ): Promise<PersistenceResult> {
107
+ const offset = (page - 1) * limit;
108
+
109
+ const persistenceDriver = process.env['WORKER_PERSISTENCE_DRIVER'] ?? 'memory';
110
+ const isMixedPersistence = persistenceDriver === 'database' || persistenceDriver === 'db';
111
+
112
+ if (driverFilter) {
113
+ return getWorkersByDriverFilter(driverFilter, offset, limit);
114
+ }
115
+
116
+ if (isMixedPersistence) {
117
+ return getWorkersFromMixedPersistence(offset, limit);
118
+ }
119
+
120
+ return getWorkersFromSinglePersistence(persistenceDriver, offset, limit);
121
+ }
122
+
123
+ async function getWorkersByDriverFilter(
124
+ driverFilter: WorkerDriver,
125
+ offset: number,
126
+ limit: number
127
+ ): Promise<PersistenceResult> {
128
+ try {
129
+ const driverRecords = await WorkerFactory.listPersistedRecords(
130
+ { driver: driverFilter },
131
+ { offset, limit }
132
+ );
133
+ const workers = transformToWorkerData(driverRecords, driverFilter);
134
+
135
+ return {
136
+ workers,
137
+ total: driverRecords.length === limit ? offset + limit + 100 : offset + driverRecords.length,
138
+ drivers: getAvailableDriversFromDrivers([driverFilter]),
139
+ effectiveLimit: limit,
140
+ prePaginated: true,
141
+ };
142
+ } catch (error) {
143
+ Logger.error(`Error fetching workers from ${driverFilter}:`, error);
144
+ return {
145
+ workers: [],
146
+ total: 0,
147
+ drivers: getAvailableDriversFromDrivers([driverFilter]),
148
+ effectiveLimit: limit,
149
+ prePaginated: true,
150
+ };
151
+ }
152
+ }
153
+
154
+ async function getWorkersFromMixedPersistence(
155
+ offset: number,
156
+ limit: number
157
+ ): Promise<PersistenceResult> {
158
+ try {
159
+ const dbRecords = await WorkerFactory.listPersistedRecords(
160
+ { driver: 'database' },
161
+ { offset, limit }
162
+ );
163
+ const redisRecords = await WorkerFactory.listPersistedRecords(
164
+ { driver: 'redis' },
165
+ { offset, limit }
166
+ );
167
+
168
+ const workers = [
169
+ ...transformToWorkerData(dbRecords, 'database'),
170
+ ...transformToWorkerData(redisRecords, 'redis'),
171
+ ];
172
+
173
+ return {
174
+ workers,
175
+ total:
176
+ dbRecords.length + redisRecords.length >= limit
177
+ ? offset + limit * 2
178
+ : offset + dbRecords.length + redisRecords.length,
179
+ drivers: getAvailableDriversFromDrivers(['database', 'redis']),
180
+ effectiveLimit: Math.min(MAX_PAGE_SIZE, limit * 2),
181
+ prePaginated: true,
182
+ };
183
+ } catch (error) {
184
+ Logger.error('Error fetching workers from mixed persistence:', error);
185
+ return {
186
+ workers: [],
187
+ total: 0,
188
+ drivers: getAvailableDriversFromDrivers(['database', 'redis']),
189
+ effectiveLimit: Math.min(MAX_PAGE_SIZE, limit * 2),
190
+ prePaginated: true,
191
+ };
192
+ }
193
+ }
194
+
195
+ async function getWorkersFromSinglePersistence(
196
+ persistenceDriver: string,
197
+ offset: number,
198
+ limit: number
199
+ ): Promise<PersistenceResult> {
200
+ try {
201
+ const normalizedDriver = normalizeDriver(persistenceDriver);
202
+ const driverRecords = await WorkerFactory.listPersistedRecords(
203
+ { driver: normalizedDriver },
204
+ { offset, limit }
205
+ );
206
+ const workers = transformToWorkerData(driverRecords, normalizedDriver);
207
+
208
+ return {
209
+ workers,
210
+ total: driverRecords.length === limit ? offset + limit + 100 : offset + driverRecords.length,
211
+ drivers: getAvailableDriversFromDrivers([normalizedDriver]),
212
+ effectiveLimit: limit,
213
+ prePaginated: true,
214
+ };
215
+ } catch (error) {
216
+ Logger.error(`Error fetching workers from ${persistenceDriver}:`, error);
217
+ return {
218
+ workers: [],
219
+ total: 0,
220
+ drivers: getAvailableDriversFromDrivers([normalizeDriver(persistenceDriver)]),
221
+ effectiveLimit: limit,
222
+ prePaginated: false,
223
+ };
224
+ }
225
+ }
226
+
227
+ const normalizeDriver = (driver: string): WorkerDriver => {
228
+ if (driver === 'db' || driver === 'database') return 'database';
229
+ if (driver === 'redis') return 'redis';
230
+ return 'memory';
231
+ };
232
+
233
+ const getAvailableDriversFromDrivers = (drivers: WorkerDriver[]): WorkerDriver[] => {
234
+ const uniqueDrivers = new Set(drivers);
235
+ return Array.from(uniqueDrivers);
236
+ };
237
+
238
+ function transformToWorkerData(
239
+ workers: (string | RawWorkerData | WorkerRecord)[],
240
+ driver: WorkerDriver
241
+ ): WorkerData[] {
242
+ return workers.map((worker) => {
243
+ if (typeof worker === 'string') {
244
+ return buildWorkerFromRaw({ name: worker }, driver);
245
+ }
246
+
247
+ if (isWorkerRecord(worker)) {
248
+ return buildWorkerFromRecord(worker, driver);
249
+ }
250
+
251
+ return buildWorkerFromRaw(worker, driver);
252
+ });
253
+ }
254
+
255
+ const isWorkerRecord = (worker: RawWorkerData | WorkerRecord): worker is WorkerRecord => {
256
+ return 'autoStart' in worker && 'queueName' in worker && 'createdAt' in worker;
257
+ };
258
+
259
+ const buildWorkerFromRecord = (record: WorkerRecord, driver: WorkerDriver): WorkerData => {
260
+ const status = normalizeStatus(record.status);
261
+ const rawData: RawWorkerData = {
262
+ name: record.name,
263
+ queueName: record.queueName,
264
+ status,
265
+ version: record.version ?? '1.0.0',
266
+ autoStart: record.autoStart,
267
+ lastError: record.lastError,
268
+ };
269
+
270
+ return buildWorkerFromRaw(rawData, driver);
271
+ };
272
+
273
+ const buildWorkerFromRaw = (workerData: RawWorkerData, driver: WorkerDriver): WorkerData => {
274
+ const status = normalizeStatus(workerData.status ?? 'stopped');
275
+ return {
276
+ name: workerData.name,
277
+ queueName: workerData.queueName || `${workerData.name}-queue`,
278
+ status,
279
+ health: determineHealth({ ...workerData, status }),
280
+ driver,
281
+ version: workerData.version || '1.0.0',
282
+ processed: workerData.processed || 0,
283
+ avgTime: workerData.avgTime || 0,
284
+ memory: workerData.memory || 0,
285
+ autoStart: workerData.autoStart || false,
286
+ details: workerData.details || {
287
+ configuration: {} as WorkerConfiguration,
288
+ health: {} as WorkerHealth,
289
+ metrics: {} as WorkerMetrics,
290
+ recentLogs: [],
291
+ },
292
+ };
293
+ };
294
+
295
+ const normalizeStatus = (status: string): WorkerData['status'] => {
296
+ if (status === 'running' || status === 'stopped' || status === 'error' || status === 'paused') {
297
+ return status;
298
+ }
299
+ return 'stopped';
300
+ };
301
+
302
+ function determineHealth(worker: RawWorkerData): WorkerHealth {
303
+ let status: WorkerHealthStatus = 'healthy';
304
+ const checks: Array<{ name: string; status: WorkerHealthCheckStatus; message?: string }> = [];
305
+ const lastCheck = new Date().toISOString();
306
+
307
+ if (worker.status === 'error') {
308
+ status = 'unhealthy';
309
+ checks.push({
310
+ name: 'worker-status',
311
+ status: 'fail',
312
+ message: 'Worker is in error state',
313
+ });
314
+ } else if (worker.status === 'stopped') {
315
+ status = 'warning';
316
+ checks.push({
317
+ name: 'worker-status',
318
+ status: 'warn',
319
+ message: 'Worker is stopped',
320
+ });
321
+ }
322
+
323
+ if (worker.lastError) {
324
+ const timeSinceLastError = Date.now() - new Date(worker.lastError).getTime();
325
+ if (timeSinceLastError < 300000) {
326
+ status = 'warning';
327
+ checks.push({
328
+ name: 'recent-error',
329
+ status: 'warn',
330
+ message: `Last error occurred ${Math.round(timeSinceLastError / 1000)} seconds ago`,
331
+ });
332
+ }
333
+ }
334
+
335
+ return {
336
+ status,
337
+ checks,
338
+ lastCheck,
339
+ };
340
+ }
341
+
342
+ function applyFilters(workers: WorkerData[], query: GetWorkersQuery): WorkerData[] {
343
+ let filtered = [...workers];
344
+
345
+ if (query.status) {
346
+ filtered = filtered.filter((w) => w.status === query.status);
347
+ }
348
+
349
+ if (query.driver) {
350
+ filtered = filtered.filter((w) => w.driver === query.driver);
351
+ }
352
+
353
+ return filtered;
354
+ }
355
+
356
+ function applySearch(workers: WorkerData[], searchTerm: string): WorkerData[] {
357
+ const term = searchTerm.toLowerCase();
358
+ return workers.filter(
359
+ (worker) =>
360
+ worker.name.toLowerCase().includes(term) ||
361
+ worker.queueName.toLowerCase().includes(term) ||
362
+ worker.version.toLowerCase().includes(term)
363
+ );
364
+ }
365
+
366
+ function applySorting(
367
+ workers: WorkerData[],
368
+ sortBy?: string,
369
+ sortOrder: 'asc' | 'desc' = 'asc'
370
+ ): WorkerData[] {
371
+ if (!sortBy) return workers;
372
+
373
+ return [...workers].sort((a, b) => {
374
+ let aVal: string | number;
375
+ let bVal: string | number;
376
+
377
+ switch (sortBy) {
378
+ case 'name':
379
+ aVal = a.name.toLowerCase();
380
+ bVal = b.name.toLowerCase();
381
+ break;
382
+ case 'status':
383
+ aVal = a.status;
384
+ bVal = b.status;
385
+ break;
386
+ case 'driver':
387
+ aVal = a.driver;
388
+ bVal = b.driver;
389
+ break;
390
+ case 'health':
391
+ aVal = a.health.status;
392
+ bVal = b.health.status;
393
+ break;
394
+ case 'version':
395
+ aVal = a.version;
396
+ bVal = b.version;
397
+ break;
398
+ case 'processed':
399
+ aVal = a.processed;
400
+ bVal = b.processed;
401
+ break;
402
+ default:
403
+ return 0;
404
+ }
405
+
406
+ if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1;
407
+ if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1;
408
+ return 0;
409
+ });
410
+ }
411
+
412
+ async function getQueueData(): Promise<QueueData> {
413
+ const queueDriver = process.env.QUEUE_DRIVER || 'redis';
414
+
415
+ try {
416
+ // Get queue statistics based on QUEUE_DRIVER
417
+ switch (queueDriver) {
418
+ case 'redis':
419
+ return getRedisQueueData();
420
+ case 'database':
421
+ return getDatabaseQueueData();
422
+ case 'db':
423
+ return getDatabaseQueueData();
424
+ default:
425
+ return getMemoryQueueData();
426
+ }
427
+ } catch (error) {
428
+ Logger.error('Error fetching queue data:', error);
429
+ return getMemoryQueueData();
430
+ }
431
+ }
432
+
433
+ async function getRedisQueueData(): Promise<QueueData> {
434
+ try {
435
+ // Use existing queue monitor infrastructure
436
+ const { QueueMonitor } = await import('@zintrust/queue-monitor');
437
+ const { queueConfig } = await import('@zintrust/core');
438
+
439
+ const redisConfig = queueConfig.drivers.redis;
440
+ if (redisConfig?.driver !== 'redis') {
441
+ throw ErrorFactory.createConfigError('Redis driver not configured');
442
+ }
443
+
444
+ const monitor = QueueMonitor.create({ redis: redisConfig });
445
+ const snapshot = await monitor.getSnapshot();
446
+
447
+ let totalJobs = 0;
448
+ let processingJobs = 0;
449
+ let failedJobs = 0;
450
+
451
+ // Aggregate stats from all queues
452
+ for (const queue of snapshot.queues) {
453
+ totalJobs +=
454
+ (queue.counts.waiting || 0) +
455
+ (queue.counts.active || 0) +
456
+ (queue.counts.completed || 0) +
457
+ (queue.counts.failed || 0);
458
+ processingJobs += queue.counts.active || 0;
459
+ failedJobs += queue.counts.failed || 0;
460
+ }
461
+
462
+ return {
463
+ driver: 'redis',
464
+ totalQueues: snapshot.queues.length,
465
+ totalJobs,
466
+ processingJobs,
467
+ failedJobs,
468
+ };
469
+ } catch (error) {
470
+ Logger.error('Error fetching Redis queue data:', error);
471
+ return {
472
+ driver: 'redis',
473
+ totalQueues: 0,
474
+ totalJobs: 0,
475
+ processingJobs: 0,
476
+ failedJobs: 0,
477
+ };
478
+ }
479
+ }
480
+
481
+ async function getDatabaseQueueData(): Promise<QueueData> {
482
+ try {
483
+ // For database queues, use the existing database connection
484
+ const { useEnsureDbConnected } = await import('@zintrust/core');
485
+ const db = await useEnsureDbConnected();
486
+
487
+ // Get queue statistics from actual database tables using proper query builder
488
+ const queueStats = (await db
489
+ .table('queue_jobs')
490
+ .select('COUNT(DISTINCT queue) as totalQueues')
491
+ .selectAs('COUNT(*)', 'totalJobs')
492
+ .selectAs(
493
+ 'SUM(CASE WHEN reserved_at IS NOT NULL AND failed_at IS NULL THEN 1 ELSE 0 END)',
494
+ 'processingJobs'
495
+ )
496
+ .selectAs('SUM(CASE WHEN failed_at IS NOT NULL THEN 1 ELSE 0 END)', 'failedJobs')
497
+ .first()) as {
498
+ totalQueues: number;
499
+ totalJobs: number;
500
+ processingJobs: number;
501
+ failedJobs: number;
502
+ } | null;
503
+
504
+ const stats = queueStats || {
505
+ totalQueues: 0,
506
+ totalJobs: 0,
507
+ processingJobs: 0,
508
+ failedJobs: 0,
509
+ };
510
+
511
+ return {
512
+ driver: 'database',
513
+ totalQueues: Number(stats.totalQueues) || 0,
514
+ totalJobs: Number(stats.totalJobs) || 0,
515
+ processingJobs: Number(stats.processingJobs) || 0,
516
+ failedJobs: Number(stats.failedJobs) || 0,
517
+ };
518
+ } catch (error) {
519
+ Logger.error('Error fetching database queue data:', error);
520
+ return {
521
+ driver: 'database',
522
+ totalQueues: 0,
523
+ totalJobs: 0,
524
+ processingJobs: 0,
525
+ failedJobs: 0,
526
+ };
527
+ }
528
+ }
529
+
530
+ async function getMemoryQueueData(): Promise<QueueData> {
531
+ // For memory queues, we need to access the in-memory queue registry
532
+ // This is a simplified implementation - in practice you'd need to
533
+ // access the actual queue registry from the queue system
534
+ // Since memory queues don't persist, we return basic info
535
+ // In a real implementation, you'd track active memory queues
536
+ return {
537
+ driver: 'memory',
538
+ totalQueues: 0, // Memory queues are not persisted
539
+ totalJobs: 0,
540
+ processingJobs: 0,
541
+ failedJobs: 0,
542
+ };
543
+ }
544
+
545
+ async function enrichWithMetrics(workers: WorkerData[]): Promise<WorkerData[]> {
546
+ const now = Date.now();
547
+ const oneHourAgo = new Date(now - 60 * 60 * 1000);
548
+ const endDate = new Date(now);
549
+
550
+ if (workers.length === 0) return workers;
551
+
552
+ const metricRequests = workers.flatMap((worker) => [
553
+ {
554
+ workerName: worker.name,
555
+ metricType: 'processed' as const,
556
+ granularity: 'hourly' as const,
557
+ startDate: oneHourAgo,
558
+ endDate,
559
+ },
560
+ {
561
+ workerName: worker.name,
562
+ metricType: 'duration' as const,
563
+ granularity: 'hourly' as const,
564
+ startDate: oneHourAgo,
565
+ endDate,
566
+ },
567
+ {
568
+ workerName: worker.name,
569
+ metricType: 'memory' as const,
570
+ granularity: 'hourly' as const,
571
+ startDate: oneHourAgo,
572
+ endDate,
573
+ },
574
+ ]);
575
+
576
+ try {
577
+ const results = await WorkerMetricsManager.aggregateBatch(metricRequests);
578
+
579
+ return workers.map((worker, index) => {
580
+ const baseIdx = index * 3;
581
+ const processedMetric = results[baseIdx];
582
+ const durationMetric = results[baseIdx + 1];
583
+ const memoryMetric = results[baseIdx + 2];
584
+
585
+ const processed =
586
+ processedMetric && Number.isFinite(processedMetric.total)
587
+ ? Math.round(processedMetric.total)
588
+ : worker.processed;
589
+ const avgTime =
590
+ durationMetric && Number.isFinite(durationMetric.average)
591
+ ? Math.round(durationMetric.average)
592
+ : worker.avgTime;
593
+ const memory =
594
+ memoryMetric && Number.isFinite(memoryMetric.average)
595
+ ? Math.round(memoryMetric.average)
596
+ : worker.memory;
597
+
598
+ return {
599
+ ...worker,
600
+ processed,
601
+ avgTime,
602
+ memory,
603
+ };
604
+ });
605
+ } catch (error) {
606
+ Logger.debug('Batch metrics unavailable', error);
607
+ return workers;
608
+ }
609
+ }
610
+
611
+ async function enrichWithDetails(workers: WorkerData[]): Promise<WorkerData[]> {
612
+ return Promise.all(workers.map((worker) => buildWorkerDetails(worker)));
613
+ }
614
+
615
+ async function buildWorkerDetails(worker: WorkerData): Promise<WorkerData> {
616
+ try {
617
+ const persistenceOverride = resolvePersistenceOverride(worker.driver);
618
+ const persisted = await WorkerFactory.getPersisted(worker.name, persistenceOverride);
619
+ const health = await getWorkerHealthSnapshot(worker.name, worker.health);
620
+ const metrics = await getWorkerMetricsSnapshot(worker.name, worker);
621
+ const configuration = buildWorkerConfiguration(worker, persisted);
622
+
623
+ return {
624
+ ...worker,
625
+ processed: metrics.processed,
626
+ avgTime: metrics.avgTime,
627
+ memory: metrics.memory,
628
+ details: {
629
+ configuration,
630
+ health,
631
+ metrics,
632
+ recentLogs: worker.details?.recentLogs ?? [],
633
+ },
634
+ };
635
+ } catch (error) {
636
+ Logger.error(`Error fetching details for worker ${worker.name}:`, error);
637
+ return worker;
638
+ }
639
+ }
640
+
641
+ function buildWorkerConfiguration(
642
+ worker: WorkerData,
643
+ persisted: Awaited<ReturnType<typeof WorkerFactory.getPersisted>>
644
+ ): WorkerConfiguration {
645
+ if (!persisted) {
646
+ return {
647
+ queueName: worker.queueName,
648
+ concurrency: null,
649
+ region: null,
650
+ processorPath: null,
651
+ version: worker.version,
652
+ features: null,
653
+ infrastructure: null,
654
+ datacenter: null,
655
+ };
656
+ }
657
+
658
+ return {
659
+ queueName: persisted.queueName ?? worker.queueName,
660
+ concurrency: persisted.concurrency ?? null,
661
+ region: persisted.region ?? null,
662
+ processorPath: persisted.processorPath ?? null,
663
+ version: persisted.version ?? worker.version,
664
+ features: persisted.features ?? null,
665
+ infrastructure: persisted.infrastructure ?? null,
666
+ datacenter: persisted.datacenter ?? null,
667
+ };
668
+ }
669
+
670
+ const resolvePersistenceOverride = (driver: WorkerDriver): { driver: WorkerDriver } => {
671
+ if (driver === 'database') return { driver: 'database' } as const;
672
+ if (driver === 'redis') return { driver: 'redis' } as const;
673
+ return { driver: 'memory' } as const;
674
+ };
675
+
676
+ const getWorkerHealthSnapshot = async (
677
+ name: string,
678
+ fallback: WorkerHealth
679
+ ): Promise<WorkerHealth> => {
680
+ try {
681
+ const health = (await WorkerFactory.getHealth(name)) as WorkerHealth | null;
682
+ if (health && typeof health.status === 'string') {
683
+ return health;
684
+ }
685
+ } catch (error) {
686
+ Logger.debug(`Health snapshot unavailable for worker ${name}`, error);
687
+ }
688
+ return fallback;
689
+ };
690
+
691
+ const getWorkerMetricsSnapshot = async (
692
+ name: string,
693
+ fallback: WorkerData
694
+ ): Promise<WorkerMetrics> => {
695
+ const now = Date.now();
696
+ const oneHourAgo = new Date(now - 60 * 60 * 1000);
697
+ const endDate = new Date(now);
698
+
699
+ try {
700
+ const [processedMetric, durationMetric, memoryMetric] = await Promise.all([
701
+ WorkerMetricsManager.aggregate({
702
+ workerName: name,
703
+ metricType: 'processed',
704
+ granularity: 'hourly',
705
+ startDate: oneHourAgo,
706
+ endDate,
707
+ }),
708
+ WorkerMetricsManager.aggregate({
709
+ workerName: name,
710
+ metricType: 'duration',
711
+ granularity: 'hourly',
712
+ startDate: oneHourAgo,
713
+ endDate,
714
+ }),
715
+ WorkerMetricsManager.aggregate({
716
+ workerName: name,
717
+ metricType: 'memory',
718
+ granularity: 'hourly',
719
+ startDate: oneHourAgo,
720
+ endDate,
721
+ }),
722
+ ]);
723
+
724
+ return {
725
+ processed: Number.isFinite(processedMetric.total)
726
+ ? Math.round(processedMetric.total)
727
+ : fallback.processed,
728
+ failed: 0,
729
+ avgTime: Number.isFinite(durationMetric.average)
730
+ ? Math.round(durationMetric.average)
731
+ : fallback.avgTime,
732
+ memory: Number.isFinite(memoryMetric.average)
733
+ ? Math.round(memoryMetric.average)
734
+ : fallback.memory,
735
+ cpu: 0,
736
+ uptime: 0,
737
+ };
738
+ } catch (error) {
739
+ Logger.debug(`Metrics snapshot unavailable for worker ${name}`, error);
740
+ return {
741
+ processed: fallback.processed,
742
+ failed: 0,
743
+ avgTime: fallback.avgTime,
744
+ memory: fallback.memory,
745
+ cpu: 0,
746
+ uptime: 0,
747
+ };
748
+ }
749
+ };
750
+
751
+ export async function toggleAutoStart(name: string, enabled: boolean): Promise<void> {
752
+ await WorkerFactory.setAutoStart(name, enabled);
753
+ }
754
+
755
+ export async function getWorkerDetails(name: string, driver?: string): Promise<WorkerData> {
756
+ const persistenceDriver = (driver || process.env['WORKER_PERSISTENCE_DRIVER']) ?? 'memory';
757
+ const isMixedPersistence = persistenceDriver === 'database';
758
+
759
+ let worker: WorkerData | undefined;
760
+
761
+ if (isMixedPersistence) {
762
+ const dbRecord = await WorkerFactory.getPersisted(name, { driver: 'database' });
763
+ if (dbRecord) {
764
+ worker = buildWorkerFromRecord(dbRecord, 'database');
765
+ } else {
766
+ const redisRecord = await WorkerFactory.getPersisted(name, { driver: 'redis' });
767
+ if (redisRecord) {
768
+ worker = buildWorkerFromRecord(redisRecord, 'redis');
769
+ }
770
+ }
771
+ } else {
772
+ const normalizedDriver = normalizeDriver(persistenceDriver);
773
+ const record = await WorkerFactory.getPersisted(name, { driver: normalizedDriver });
774
+ if (record) {
775
+ worker = buildWorkerFromRecord(record, normalizedDriver);
776
+ }
777
+ }
778
+
779
+ if (!worker) {
780
+ throw ErrorFactory.createWorkerError(`Worker ${name} not found`);
781
+ }
782
+
783
+ const enrichedWorkers = await enrichWithDetails([worker]);
784
+ return enrichedWorkers[0];
785
+ }