@zintrust/workers 0.1.31 → 0.1.52

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 (56) hide show
  1. package/dist/ClusterLock.js +3 -2
  2. package/dist/DeadLetterQueue.js +3 -2
  3. package/dist/HealthMonitor.js +24 -13
  4. package/dist/Observability.js +8 -0
  5. package/dist/WorkerFactory.d.ts +4 -0
  6. package/dist/WorkerFactory.js +409 -42
  7. package/dist/WorkerInit.js +122 -43
  8. package/dist/WorkerMetrics.js +5 -1
  9. package/dist/WorkerRegistry.js +8 -0
  10. package/dist/WorkerShutdown.d.ts +0 -13
  11. package/dist/WorkerShutdown.js +1 -44
  12. package/dist/build-manifest.json +101 -85
  13. package/dist/config/workerConfig.d.ts +1 -0
  14. package/dist/config/workerConfig.js +7 -1
  15. package/dist/createQueueWorker.js +281 -42
  16. package/dist/dashboard/workers-api.js +8 -1
  17. package/dist/http/WorkerController.js +90 -35
  18. package/dist/http/WorkerMonitoringService.js +29 -2
  19. package/dist/http/middleware/FeaturesValidator.js +5 -4
  20. package/dist/index.d.ts +1 -2
  21. package/dist/index.js +0 -1
  22. package/dist/routes/workers.js +10 -7
  23. package/dist/storage/WorkerStore.d.ts +6 -3
  24. package/dist/storage/WorkerStore.js +16 -0
  25. package/dist/telemetry/api/TelemetryMonitoringService.js +29 -2
  26. package/dist/ui/router/ui.js +58 -29
  27. package/dist/ui/workers/index.html +202 -0
  28. package/dist/ui/workers/main.js +1952 -0
  29. package/dist/ui/workers/styles.css +1350 -0
  30. package/dist/ui/workers/zintrust.svg +30 -0
  31. package/package.json +5 -5
  32. package/src/ClusterLock.ts +13 -7
  33. package/src/ComplianceManager.ts +3 -2
  34. package/src/DeadLetterQueue.ts +6 -4
  35. package/src/HealthMonitor.ts +33 -17
  36. package/src/Observability.ts +11 -0
  37. package/src/WorkerFactory.ts +480 -43
  38. package/src/WorkerInit.ts +167 -48
  39. package/src/WorkerMetrics.ts +14 -8
  40. package/src/WorkerRegistry.ts +11 -0
  41. package/src/WorkerShutdown.ts +1 -69
  42. package/src/config/workerConfig.ts +9 -1
  43. package/src/createQueueWorker.ts +428 -43
  44. package/src/dashboard/workers-api.ts +8 -1
  45. package/src/http/WorkerController.ts +111 -36
  46. package/src/http/WorkerMonitoringService.ts +35 -2
  47. package/src/http/middleware/FeaturesValidator.ts +8 -19
  48. package/src/index.ts +2 -3
  49. package/src/routes/workers.ts +10 -8
  50. package/src/storage/WorkerStore.ts +21 -3
  51. package/src/telemetry/api/TelemetryMonitoringService.ts +35 -2
  52. package/src/types/queue-monitor.d.ts +2 -1
  53. package/src/ui/components/WorkerExpandPanel.js +0 -8
  54. package/src/ui/router/EmbeddedAssets.ts +3 -0
  55. package/src/ui/router/ui.ts +57 -39
  56. package/src/WorkerShutdownDurableObject.ts +0 -64
@@ -0,0 +1,30 @@
1
+ <svg width="120" height="120" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="zt-g2d" x1="10" y1="50" x2="90" y2="50" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#22c55e" />
5
+ <stop offset="1" stop-color="#38bdf8" />
6
+ </linearGradient>
7
+ </defs>
8
+ <circle cx="50" cy="50" r="34" stroke="rgba(255,255,255,0.16)" stroke-width="4" />
9
+ <ellipse cx="50" cy="50" rx="40" ry="18" stroke="url(#zt-g2d)" stroke-width="4" />
10
+ <ellipse cx="50" cy="50" rx="18" ry="40" stroke="url(#zt-g2d)" stroke-width="4" opacity="0.75" />
11
+ <circle cx="50" cy="50" r="6" fill="url(#zt-g2d)" />
12
+ <path
13
+ d="M40 52C35 52 32 49 32 44C32 39 35 36 40 36H48"
14
+ stroke="white"
15
+ stroke-width="6"
16
+ stroke-linecap="round"
17
+ />
18
+ <path
19
+ d="M60 48C65 48 68 51 68 56C68 61 65 64 60 64H52"
20
+ stroke="white"
21
+ stroke-width="6"
22
+ stroke-linecap="round"
23
+ />
24
+ <path
25
+ d="M44 50H56"
26
+ stroke="rgba(255,255,255,0.22)"
27
+ stroke-width="6"
28
+ stroke-linecap="round"
29
+ />
30
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/workers",
3
- "version": "0.1.31",
3
+ "version": "0.1.52",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,13 +35,13 @@
35
35
  "node": ">=20.0.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@zintrust/core": "^0.1.40"
38
+ "@zintrust/core": "^0.1.52"
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
42
42
  },
43
43
  "scripts": {
44
- "build": "tsc -p tsconfig.json",
44
+ "build": "node scripts/generate-embedded-assets.mjs && tsc -p tsconfig.json",
45
45
  "prepublishOnly": "npm run build"
46
46
  },
47
47
  "dependencies": {
@@ -53,7 +53,7 @@
53
53
  "simple-statistics": "^7.8.8"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@zintrust/queue-redis": "^0.1.27",
57
- "@zintrust/queue-monitor": "^0.1.27"
56
+ "@zintrust/queue-monitor": "^0.1.27",
57
+ "@zintrust/queue-redis": "^0.1.27"
58
58
  }
59
59
  }
@@ -12,7 +12,8 @@ import {
12
12
  generateUuid,
13
13
  type RedisConfig,
14
14
  } from '@zintrust/core';
15
- import type IORedis from 'ioredis';
15
+
16
+ type RedisConnection = ReturnType<typeof createRedisConnection>;
16
17
 
17
18
  export type LockAcquisitionOptions = {
18
19
  lockKey: string;
@@ -60,7 +61,7 @@ const LOCK_PREFIX = 'worker:lock:';
60
61
  const AUDIT_PREFIX = 'worker:audit:lock:';
61
62
 
62
63
  // Internal state
63
- let redisClient: IORedis | null = null;
64
+ let redisClient: RedisConnection | null = null;
64
65
  let heartbeatInterval: NodeJS.Timeout | null = null;
65
66
  const activeLocks = new Map<string, LockInfo>();
66
67
 
@@ -81,7 +82,7 @@ const getAuditKey = (lockKey: string): string => {
81
82
  /**
82
83
  * Helper: Store audit log entry in Redis
83
84
  */
84
- const auditLockOperation = async (client: IORedis, entry: AuditLogEntry): Promise<void> => {
85
+ const auditLockOperation = async (client: RedisConnection, entry: AuditLogEntry): Promise<void> => {
85
86
  try {
86
87
  const auditKey = getAuditKey(entry.lockKey);
87
88
  const auditData = JSON.stringify(entry);
@@ -103,7 +104,11 @@ const auditLockOperation = async (client: IORedis, entry: AuditLogEntry): Promis
103
104
  /**
104
105
  * Helper: Extend lock TTL
105
106
  */
106
- const extendLockTTL = async (client: IORedis, lockKey: string, ttl: number): Promise<boolean> => {
107
+ const extendLockTTL = async (
108
+ client: RedisConnection,
109
+ lockKey: string,
110
+ ttl: number
111
+ ): Promise<boolean> => {
107
112
  const redisKey = getLockKey(lockKey);
108
113
  const value = await client.get(redisKey);
109
114
 
@@ -118,7 +123,7 @@ const extendLockTTL = async (client: IORedis, lockKey: string, ttl: number): Pro
118
123
  /**
119
124
  * Helper: Start heartbeat for lock extension
120
125
  */
121
- const startHeartbeat = (client: IORedis): void => {
126
+ const startHeartbeat = (client: RedisConnection): void => {
122
127
  if (heartbeatInterval) {
123
128
  return; // Already running
124
129
  }
@@ -188,8 +193,9 @@ export const ClusterLock = Object.freeze({
188
193
  return;
189
194
  }
190
195
 
191
- redisClient = createRedisConnection(config);
192
- startHeartbeat(redisClient);
196
+ const client = createRedisConnection(config);
197
+ redisClient = client;
198
+ startHeartbeat(client);
193
199
 
194
200
  Logger.info('ClusterLock initialized', { instanceId: getInstanceId() });
195
201
  },
@@ -11,7 +11,8 @@ import {
11
11
  createRedisConnection,
12
12
  type RedisConfig,
13
13
  } from '@zintrust/core';
14
- import type IORedis from 'ioredis';
14
+
15
+ type RedisConnection = ReturnType<typeof createRedisConnection>;
15
16
 
16
17
  type CryptoAdapter = {
17
18
  createCipheriv: typeof NodeSingletons.createCipheriv;
@@ -134,7 +135,7 @@ const VIOLATION_PREFIX = 'compliance:violation:';
134
135
  const CONSENT_PREFIX = 'compliance:consent:';
135
136
 
136
137
  // Internal state
137
- let redisClient: IORedis | null = null;
138
+ let redisClient: RedisConnection | null = null;
138
139
  let complianceConfig: ComplianceConfig | null = null;
139
140
 
140
141
  // Default configuration
@@ -5,7 +5,9 @@
5
5
  */
6
6
 
7
7
  import { ErrorFactory, Logger, createRedisConnection, type RedisConfig } from '@zintrust/core';
8
- import type IORedis from 'ioredis';
8
+ import { keyPrefix } from './config/workerConfig';
9
+
10
+ type RedisConnection = ReturnType<typeof createRedisConnection>;
9
11
 
10
12
  export type FailedJobEntry = {
11
13
  id: string;
@@ -72,15 +74,15 @@ export type DLQStats = {
72
74
 
73
75
  // Redis key prefixes - using workers package prefix system
74
76
  const getDLQPrefix = (): string => {
75
- return 'worker:dlq:';
77
+ return keyPrefix() + ':dlq:';
76
78
  };
77
79
 
78
80
  const getAuditPrefix = (): string => {
79
- return 'worker:dlq:audit:';
81
+ return keyPrefix() + ':dlq:audit:';
80
82
  };
81
83
 
82
84
  // Internal state
83
- let redisClient: IORedis | null = null;
85
+ let redisClient: RedisConnection | null = null;
84
86
  let retentionPolicy: RetentionPolicy | null = null;
85
87
  let cleanupInterval: NodeJS.Timeout | null = null;
86
88
 
@@ -70,11 +70,7 @@ const persistStatusChange = async (
70
70
  }
71
71
  };
72
72
 
73
- const verifyWorkerHealth = async (
74
- worker: Worker,
75
- _name: string,
76
- _queueName: string
77
- ): Promise<boolean> => {
73
+ const verifyWorkerHealth = async (worker: Worker): Promise<boolean> => {
78
74
  // Check if isClosing exists (isClosing check safe for mocks)
79
75
  const workerAny = worker as unknown as Record<string, unknown>;
80
76
  const isClosingFn = workerAny['isClosing'];
@@ -94,10 +90,38 @@ const verifyWorkerHealth = async (
94
90
  if (pingResult !== 'PONG') {
95
91
  throw ErrorFactory.createWorkerError(`Redis ping failed: ${pingResult}`);
96
92
  }
97
- Logger.debug(`Worker health verification passed for ${_name} ${_queueName}`);
98
93
  return true;
99
94
  };
100
95
 
96
+ const withTimeout = async <T>(
97
+ promise: Promise<T>,
98
+ timeoutMs: number,
99
+ onTimeout: () => Error
100
+ ): Promise<T> => {
101
+ let timeoutId: NodeJS.Timeout | null = null;
102
+
103
+ return await new Promise<T>((resolve, reject) => {
104
+ timeoutId = globalThis.setTimeout(() => {
105
+ if (timeoutId) {
106
+ clearTimeout(timeoutId);
107
+ timeoutId = null;
108
+ }
109
+ reject(onTimeout());
110
+ }, timeoutMs);
111
+ timeoutId.unref();
112
+
113
+ promise
114
+ .then(resolve)
115
+ .catch(reject)
116
+ .finally(() => {
117
+ if (timeoutId) {
118
+ clearTimeout(timeoutId);
119
+ timeoutId = null;
120
+ }
121
+ });
122
+ });
123
+ };
124
+
101
125
  const updateState = (
102
126
  state: WorkerHealthState,
103
127
  isHealthy: boolean,
@@ -177,17 +201,9 @@ const performCheck = async (state: WorkerHealthState): Promise<void> => {
177
201
  throw ErrorFactory.createWorkerError('Worker instance not available');
178
202
  }
179
203
 
180
- isHealthy = await Promise.race([
181
- verifyWorkerHealth(state.worker, state.name, state.queueName || 'unknown'),
182
- new Promise<boolean>((_, reject) => {
183
- // eslint-disable-next-line
184
- const id = setTimeout(() => {
185
- reject(ErrorFactory.createWorkerError('Health check timeout'));
186
- }, config.checkTimeoutMs);
187
- // Unref to prevent holding event loop if everything else finishes
188
- id.unref();
189
- }),
190
- ]);
204
+ isHealthy = await withTimeout(verifyWorkerHealth(state.worker), config.checkTimeoutMs, () =>
205
+ ErrorFactory.createWorkerError('Health check timeout')
206
+ );
191
207
  } catch (err) {
192
208
  isHealthy = false;
193
209
  errorMsg = (err as Error).message;
@@ -64,6 +64,13 @@ let spanSweepInterval: NodeJS.Timeout | null = null;
64
64
  const MAX_ACTIVE_SPANS = 1000;
65
65
  const SPAN_TTL_MS = 5 * 60 * 1000;
66
66
 
67
+ type UnrefableTimer = { unref: () => void };
68
+
69
+ const isUnrefableTimer = (value: unknown): value is UnrefableTimer => {
70
+ if (typeof value !== 'object' || value === null) return false;
71
+ return 'unref' in value && typeof (value as UnrefableTimer).unref === 'function';
72
+ };
73
+
67
74
  const cleanupStaleSpans = (): void => {
68
75
  const now = Date.now();
69
76
  for (const [spanId, entry] of activeSpans.entries()) {
@@ -237,6 +244,10 @@ export const Observability = Object.freeze({
237
244
  spanSweepInterval = setInterval(() => {
238
245
  cleanupStaleSpans();
239
246
  }, SPAN_TTL_MS);
247
+
248
+ if (isUnrefableTimer(spanSweepInterval)) {
249
+ spanSweepInterval.unref();
250
+ }
240
251
  }
241
252
 
242
253
  Logger.info('Observability initialized', {