@zintrust/workers 0.1.30 → 0.1.43
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.
- package/dist/ClusterLock.js +3 -2
- package/dist/DeadLetterQueue.js +3 -2
- package/dist/HealthMonitor.js +24 -13
- package/dist/Observability.js +8 -0
- package/dist/WorkerFactory.d.ts +4 -0
- package/dist/WorkerFactory.js +384 -42
- package/dist/WorkerInit.js +122 -43
- package/dist/WorkerMetrics.js +5 -1
- package/dist/WorkerRegistry.js +8 -0
- package/dist/WorkerShutdown.d.ts +0 -13
- package/dist/WorkerShutdown.js +1 -44
- package/dist/build-manifest.json +99 -83
- package/dist/config/workerConfig.d.ts +1 -0
- package/dist/config/workerConfig.js +7 -1
- package/dist/createQueueWorker.js +281 -42
- package/dist/dashboard/workers-api.js +8 -1
- package/dist/http/WorkerController.js +90 -35
- package/dist/http/WorkerMonitoringService.js +29 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/routes/workers.js +10 -7
- package/dist/storage/WorkerStore.d.ts +6 -3
- package/dist/storage/WorkerStore.js +16 -0
- package/dist/telemetry/api/TelemetryMonitoringService.js +29 -2
- package/dist/ui/router/ui.js +58 -29
- package/dist/ui/workers/index.html +202 -0
- package/dist/ui/workers/main.js +1952 -0
- package/dist/ui/workers/styles.css +1350 -0
- package/dist/ui/workers/zintrust.svg +30 -0
- package/package.json +5 -5
- package/src/ClusterLock.ts +13 -7
- package/src/ComplianceManager.ts +3 -2
- package/src/DeadLetterQueue.ts +6 -4
- package/src/HealthMonitor.ts +33 -17
- package/src/Observability.ts +11 -0
- package/src/WorkerFactory.ts +446 -43
- package/src/WorkerInit.ts +167 -48
- package/src/WorkerMetrics.ts +14 -8
- package/src/WorkerRegistry.ts +11 -0
- package/src/WorkerShutdown.ts +1 -69
- package/src/config/workerConfig.ts +9 -1
- package/src/createQueueWorker.ts +428 -43
- package/src/dashboard/workers-api.ts +8 -1
- package/src/http/WorkerController.ts +111 -36
- package/src/http/WorkerMonitoringService.ts +35 -2
- package/src/index.ts +2 -3
- package/src/routes/workers.ts +10 -8
- package/src/storage/WorkerStore.ts +21 -3
- package/src/telemetry/api/TelemetryMonitoringService.ts +35 -2
- package/src/types/queue-monitor.d.ts +2 -1
- package/src/ui/router/EmbeddedAssets.ts +3 -0
- package/src/ui/router/ui.ts +57 -39
- 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.
|
|
3
|
+
"version": "0.1.43",
|
|
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": "
|
|
38
|
+
"@zintrust/core": "^0.1.43"
|
|
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-
|
|
57
|
-
"@zintrust/queue-
|
|
56
|
+
"@zintrust/queue-monitor": "^0.1.27",
|
|
57
|
+
"@zintrust/queue-redis": "^0.1.27"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/ClusterLock.ts
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
generateUuid,
|
|
13
13
|
type RedisConfig,
|
|
14
14
|
} from '@zintrust/core';
|
|
15
|
-
|
|
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:
|
|
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:
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
192
|
-
|
|
196
|
+
const client = createRedisConnection(config);
|
|
197
|
+
redisClient = client;
|
|
198
|
+
startHeartbeat(client);
|
|
193
199
|
|
|
194
200
|
Logger.info('ClusterLock initialized', { instanceId: getInstanceId() });
|
|
195
201
|
},
|
package/src/ComplianceManager.ts
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
createRedisConnection,
|
|
12
12
|
type RedisConfig,
|
|
13
13
|
} from '@zintrust/core';
|
|
14
|
-
|
|
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:
|
|
138
|
+
let redisClient: RedisConnection | null = null;
|
|
138
139
|
let complianceConfig: ComplianceConfig | null = null;
|
|
139
140
|
|
|
140
141
|
// Default configuration
|
package/src/DeadLetterQueue.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { ErrorFactory, Logger, createRedisConnection, type RedisConfig } from '@zintrust/core';
|
|
8
|
-
import
|
|
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 '
|
|
77
|
+
return keyPrefix() + ':dlq:';
|
|
76
78
|
};
|
|
77
79
|
|
|
78
80
|
const getAuditPrefix = (): string => {
|
|
79
|
-
return '
|
|
81
|
+
return keyPrefix() + ':dlq:audit:';
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
// Internal state
|
|
83
|
-
let redisClient:
|
|
85
|
+
let redisClient: RedisConnection | null = null;
|
|
84
86
|
let retentionPolicy: RetentionPolicy | null = null;
|
|
85
87
|
let cleanupInterval: NodeJS.Timeout | null = null;
|
|
86
88
|
|
package/src/HealthMonitor.ts
CHANGED
|
@@ -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
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
package/src/Observability.ts
CHANGED
|
@@ -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', {
|