@zintrust/queue-redis 2.1.4 → 2.4.2
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/README.md +9 -2
- package/dist/BullMQRedisQueue.d.ts +2 -4
- package/dist/BullMQRedisQueue.js +32 -51
- package/dist/QueueHttpGateway.d.ts +1 -1
- package/dist/QueueHttpGateway.js +56 -78
- package/dist/RedisPublishClient.js +7 -2
- package/dist/RedisRpcQueueDriver.d.ts +11 -0
- package/dist/RedisRpcQueueDriver.js +131 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +9 -3
- package/dist/HttpQueueDriver.d.ts +0 -17
- package/dist/HttpQueueDriver.js +0 -258
package/README.md
CHANGED
|
@@ -16,9 +16,16 @@ npm i @zintrust/queue-redis
|
|
|
16
16
|
import '@zintrust/queue-redis/register';
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Then set `QUEUE_DRIVER=redis` and configure
|
|
19
|
+
Then set `QUEUE_DRIVER=redis` and configure your Redis connection.
|
|
20
20
|
|
|
21
|
-
For Cloudflare Workers
|
|
21
|
+
For Cloudflare Workers or any runtime without Redis TCP access, run Redis RPC from a Node.js backend and configure the Worker with both flags:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
USE_REDIS_PROXY=true
|
|
25
|
+
REDIS_RPC_URL=https://queues.example.com
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Start the backend with `zin redis-rpc` or `zin s redis-rpc`. See [`@zintrust/redis-rpc`](https://www.npmjs.com/package/@zintrust/redis-rpc).
|
|
22
29
|
|
|
23
30
|
## When to use
|
|
24
31
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BullMQPayload, QueueMessage } from '@zintrust/core/queue';
|
|
2
|
-
import { Queue } from 'bullmq';
|
|
2
|
+
import type { Queue } from 'bullmq';
|
|
3
3
|
interface IQueueDriver {
|
|
4
4
|
enqueue(queue: string, payload: BullMQPayload): Promise<string>;
|
|
5
5
|
dequeue<T = unknown>(queue: string): Promise<QueueMessage<T> | undefined>;
|
|
@@ -8,13 +8,11 @@ interface IQueueDriver {
|
|
|
8
8
|
drain(queue: string): Promise<void>;
|
|
9
9
|
}
|
|
10
10
|
interface IBullMQRedisQueue extends IQueueDriver {
|
|
11
|
-
getQueue(queueName: string): Queue
|
|
11
|
+
getQueue(queueName: string): Promise<Queue>;
|
|
12
12
|
shutdown(): Promise<void>;
|
|
13
13
|
closeQueue(queueName: string): Promise<void>;
|
|
14
14
|
getQueueNames(): string[];
|
|
15
15
|
}
|
|
16
|
-
export declare const shouldUseHttpProxyDriver: () => boolean;
|
|
17
|
-
export declare const runWithDirectQueueDriver: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
18
16
|
/**
|
|
19
17
|
* BullMQ Redis Queue Driver
|
|
20
18
|
*
|
package/dist/BullMQRedisQueue.js
CHANGED
|
@@ -6,24 +6,19 @@ import { Logger } from '@zintrust/core/logger';
|
|
|
6
6
|
import { createLockProvider, getLockProvider, registerLockProvider, resolveDeduplicationLockKey, resolveLockPrefix, } from '@zintrust/core/queue';
|
|
7
7
|
import { createRedisConnection, getBullMQSafeQueueName } from '@zintrust/core/redis';
|
|
8
8
|
import { generateUuid, ZintrustLang } from '@zintrust/core/utils';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return await fn();
|
|
23
|
-
}
|
|
24
|
-
finally {
|
|
25
|
-
directModeDepth = Math.max(0, directModeDepth - 1);
|
|
26
|
-
}
|
|
9
|
+
import { RedisRpcQueueDriver, shouldUseRedisRpcQueueDriver } from './RedisRpcQueueDriver.js';
|
|
10
|
+
// Lazy BullMQ loader keyed on a variable specifier so bundlers (esbuild/wrangler)
|
|
11
|
+
// do not inline bullmq/ioredis into the Workers bundle. Every public method routes
|
|
12
|
+
// through RedisRpcQueueDriver first when shouldUseRedisRpcQueueDriver() is true
|
|
13
|
+
// (always, on Workers), so this loader never runs there.
|
|
14
|
+
let QueueCtor;
|
|
15
|
+
const ensureBullmqLoaded = async () => {
|
|
16
|
+
if (QueueCtor !== undefined)
|
|
17
|
+
return QueueCtor;
|
|
18
|
+
const bullmqPkg = 'bullmq';
|
|
19
|
+
const loaded = (await import(bullmqPkg)).Queue;
|
|
20
|
+
QueueCtor = loaded;
|
|
21
|
+
return loaded;
|
|
27
22
|
};
|
|
28
23
|
/**
|
|
29
24
|
* BullMQ Redis Queue Driver
|
|
@@ -37,17 +32,6 @@ export const BullMQRedisQueue = (() => {
|
|
|
37
32
|
let lockProviderCache = null;
|
|
38
33
|
const PULL_WORKER_TOKEN = 'pull-worker';
|
|
39
34
|
const SHARED_CONNECTION_SHUTDOWN_TIMEOUT_MS = 100;
|
|
40
|
-
const isRedisProxyEnabled = () => {
|
|
41
|
-
return Env.USE_REDIS_PROXY === true || Env.get('REDIS_PROXY_URL', '').trim() !== '';
|
|
42
|
-
};
|
|
43
|
-
const assertProxyAndWorkersCompatibility = (isWorkersRuntime) => {
|
|
44
|
-
if (isRedisProxyEnabled() && shouldUseHttpProxyDriver() === false) {
|
|
45
|
-
throw ErrorFactory.createConfigError('BullMQ Redis driver does not support REDIS proxy transport directly. Enable QUEUE_HTTP_PROXY_ENABLED=true for queue proxy mode, or disable REDIS proxy mode for direct BullMQ access.');
|
|
46
|
-
}
|
|
47
|
-
if (isWorkersRuntime && Cloudflare.isCloudflareSocketsEnabled() === false) {
|
|
48
|
-
throw ErrorFactory.createConfigError('BullMQ Redis driver requires ENABLE_CLOUDFLARE_SOCKETS=true in Cloudflare Workers. To use HTTP queue proxy mode, set QUEUE_HTTP_PROXY_ENABLED=true and QUEUE_HTTP_PROXY_URL.');
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
35
|
const resolveQueueRedisConfig = () => {
|
|
52
36
|
let workersHost = Cloudflare.getWorkersVar('WORKERS_REDIS_HOST');
|
|
53
37
|
let workersPortRaw = Cloudflare.getWorkersVar('WORKERS_REDIS_PORT');
|
|
@@ -75,12 +59,11 @@ export const BullMQRedisQueue = (() => {
|
|
|
75
59
|
const assertWorkersHostIsReachable = (isWorkersRuntime, redisConfig) => {
|
|
76
60
|
if (isWorkersRuntime &&
|
|
77
61
|
(redisConfig.host === 'localhost' || redisConfig.host === '127.0.0.1')) {
|
|
78
|
-
throw ErrorFactory.createConfigError('Redis host cannot be localhost in Cloudflare Workers. Use a public Redis host
|
|
62
|
+
throw ErrorFactory.createConfigError('Redis host cannot be localhost in Cloudflare Workers. Use a public Redis host.');
|
|
79
63
|
}
|
|
80
64
|
};
|
|
81
65
|
const createSharedBullMqConnection = () => {
|
|
82
66
|
const isWorkersRuntime = Cloudflare.getWorkersEnv() !== null;
|
|
83
|
-
assertProxyAndWorkersCompatibility(isWorkersRuntime);
|
|
84
67
|
const redisConfig = resolveQueueRedisConfig();
|
|
85
68
|
assertWorkersHostIsReachable(isWorkersRuntime, redisConfig);
|
|
86
69
|
return createRedisConnection({
|
|
@@ -207,10 +190,8 @@ export const BullMQRedisQueue = (() => {
|
|
|
207
190
|
await closeSharedConnection(sharedConnection);
|
|
208
191
|
}
|
|
209
192
|
};
|
|
210
|
-
const getQueue = (queueName) => {
|
|
211
|
-
|
|
212
|
-
throw ErrorFactory.createConfigError('BullMQ queue instance is not available when QUEUE_HTTP_PROXY mode is active.');
|
|
213
|
-
}
|
|
193
|
+
const getQueue = async (queueName) => {
|
|
194
|
+
const QueueCtor = await ensureBullmqLoaded();
|
|
214
195
|
// Check if queue exists in cache
|
|
215
196
|
if (queues.has(queueName)) {
|
|
216
197
|
const existingQueue = queues.get(queueName);
|
|
@@ -242,7 +223,7 @@ export const BullMQRedisQueue = (() => {
|
|
|
242
223
|
const backoffDelay = Env.getInt('BULLMQ_BACKOFF_DELAY', 2000);
|
|
243
224
|
const backoffType = Env.get('BULLMQ_BACKOFF_TYPE', 'exponential');
|
|
244
225
|
const prefix = getBullMQSafeQueueName();
|
|
245
|
-
const queue = new
|
|
226
|
+
const queue = new QueueCtor(queueName, {
|
|
246
227
|
connection: connection,
|
|
247
228
|
prefix,
|
|
248
229
|
defaultJobOptions: {
|
|
@@ -412,12 +393,12 @@ export const BullMQRedisQueue = (() => {
|
|
|
412
393
|
closeQueue,
|
|
413
394
|
getQueueNames,
|
|
414
395
|
async enqueue(queue, payload) {
|
|
415
|
-
if (
|
|
416
|
-
return
|
|
396
|
+
if (shouldUseRedisRpcQueueDriver()) {
|
|
397
|
+
return RedisRpcQueueDriver.enqueue(queue, payload);
|
|
417
398
|
}
|
|
418
399
|
let requestedJobId;
|
|
419
400
|
try {
|
|
420
|
-
const q = getQueue(queue);
|
|
401
|
+
const q = await getQueue(queue);
|
|
421
402
|
// Extract BullMQ options from payload with proper typing
|
|
422
403
|
const payloadData = payload;
|
|
423
404
|
const jobOptions = createJobOptions(payloadData);
|
|
@@ -450,11 +431,11 @@ export const BullMQRedisQueue = (() => {
|
|
|
450
431
|
}
|
|
451
432
|
},
|
|
452
433
|
async dequeue(queue) {
|
|
453
|
-
if (
|
|
454
|
-
return
|
|
434
|
+
if (shouldUseRedisRpcQueueDriver()) {
|
|
435
|
+
return RedisRpcQueueDriver.dequeue(queue);
|
|
455
436
|
}
|
|
456
437
|
try {
|
|
457
|
-
const q = getQueue(queue);
|
|
438
|
+
const q = await getQueue(queue);
|
|
458
439
|
const jobs = await q.getJobs(['waiting'], 0, 1);
|
|
459
440
|
if (jobs.length === 0)
|
|
460
441
|
return undefined;
|
|
@@ -481,12 +462,12 @@ export const BullMQRedisQueue = (() => {
|
|
|
481
462
|
}
|
|
482
463
|
},
|
|
483
464
|
async ack(queue, id) {
|
|
484
|
-
if (
|
|
485
|
-
await
|
|
465
|
+
if (shouldUseRedisRpcQueueDriver()) {
|
|
466
|
+
await RedisRpcQueueDriver.ack(queue, id);
|
|
486
467
|
return;
|
|
487
468
|
}
|
|
488
469
|
try {
|
|
489
|
-
const q = getQueue(queue);
|
|
470
|
+
const q = await getQueue(queue);
|
|
490
471
|
const job = await q.getJob(id);
|
|
491
472
|
if (job) {
|
|
492
473
|
await job.moveToCompleted('acknowledged', PULL_WORKER_TOKEN, false);
|
|
@@ -501,11 +482,11 @@ export const BullMQRedisQueue = (() => {
|
|
|
501
482
|
}
|
|
502
483
|
},
|
|
503
484
|
async length(queue) {
|
|
504
|
-
if (
|
|
505
|
-
return
|
|
485
|
+
if (shouldUseRedisRpcQueueDriver()) {
|
|
486
|
+
return RedisRpcQueueDriver.length(queue);
|
|
506
487
|
}
|
|
507
488
|
try {
|
|
508
|
-
const q = getQueue(queue);
|
|
489
|
+
const q = await getQueue(queue);
|
|
509
490
|
const counts = await q.getJobCounts();
|
|
510
491
|
return counts['waiting'] || 0;
|
|
511
492
|
}
|
|
@@ -515,12 +496,12 @@ export const BullMQRedisQueue = (() => {
|
|
|
515
496
|
}
|
|
516
497
|
},
|
|
517
498
|
async drain(queue) {
|
|
518
|
-
if (
|
|
519
|
-
await
|
|
499
|
+
if (shouldUseRedisRpcQueueDriver()) {
|
|
500
|
+
await RedisRpcQueueDriver.drain(queue);
|
|
520
501
|
return;
|
|
521
502
|
}
|
|
522
503
|
try {
|
|
523
|
-
const q = getQueue(queue);
|
|
504
|
+
const q = await getQueue(queue);
|
|
524
505
|
await q.drain();
|
|
525
506
|
Logger.debug(`BullMQ: Queue ${queue} drained`);
|
|
526
507
|
}
|
package/dist/QueueHttpGateway.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ErrorFactory } from '@zintrust/core/errors';
|
|
|
3
3
|
import { Router } from '@zintrust/core/http';
|
|
4
4
|
import { Logger } from '@zintrust/core/logger';
|
|
5
5
|
import { SignedRequest } from '@zintrust/core/security';
|
|
6
|
-
import BullMQRedisQueue
|
|
6
|
+
import BullMQRedisQueue from './BullMQRedisQueue.js';
|
|
7
7
|
const nonces = new Map();
|
|
8
8
|
const nowMs = () => Date.now();
|
|
9
9
|
const normalizePath = (value) => {
|
|
@@ -31,9 +31,8 @@ const readSettings = () => {
|
|
|
31
31
|
const cleanupExpiredNonces = () => {
|
|
32
32
|
const current = nowMs();
|
|
33
33
|
for (const [nonceKey, expiresAt] of nonces.entries()) {
|
|
34
|
-
if (expiresAt <= current)
|
|
34
|
+
if (expiresAt <= current)
|
|
35
35
|
nonces.delete(nonceKey);
|
|
36
|
-
}
|
|
37
36
|
}
|
|
38
37
|
};
|
|
39
38
|
const storeNonce = async (keyId, nonce, ttlMs) => {
|
|
@@ -52,18 +51,14 @@ const getBodyRecord = (req) => {
|
|
|
52
51
|
return {};
|
|
53
52
|
};
|
|
54
53
|
const getRawBody = (req) => {
|
|
55
|
-
const rawText = req.context['rawBodyText'];
|
|
54
|
+
const rawText = req.context?.['rawBodyText'];
|
|
56
55
|
if (typeof rawText === 'string')
|
|
57
56
|
return rawText;
|
|
58
57
|
return JSON.stringify(getBodyRecord(req));
|
|
59
58
|
};
|
|
60
59
|
const toIncomingHeaders = (req) => {
|
|
61
60
|
const headers = req.getHeaders();
|
|
62
|
-
const normalize = (value) =>
|
|
63
|
-
if (Array.isArray(value))
|
|
64
|
-
return value.join(',');
|
|
65
|
-
return value;
|
|
66
|
-
};
|
|
61
|
+
const normalize = (value) => Array.isArray(value) ? value.join(',') : value;
|
|
67
62
|
return {
|
|
68
63
|
'x-zt-key-id': normalize(headers['x-zt-key-id']),
|
|
69
64
|
'x-zt-timestamp': normalize(headers['x-zt-timestamp']),
|
|
@@ -73,63 +68,51 @@ const toIncomingHeaders = (req) => {
|
|
|
73
68
|
};
|
|
74
69
|
};
|
|
75
70
|
const sendFailure = (res, requestId, status, code, message, details) => {
|
|
76
|
-
|
|
71
|
+
res.status(status).json({
|
|
77
72
|
ok: false,
|
|
78
73
|
requestId,
|
|
79
74
|
result: null,
|
|
80
75
|
error: { code, message, details },
|
|
81
|
-
};
|
|
82
|
-
res.status(status).json(payload);
|
|
76
|
+
});
|
|
83
77
|
};
|
|
84
78
|
const sendSuccess = (res, requestId, result) => {
|
|
85
|
-
|
|
86
|
-
ok: true,
|
|
87
|
-
requestId,
|
|
88
|
-
result,
|
|
89
|
-
error: null,
|
|
90
|
-
};
|
|
91
|
-
res.status(200).json(payload);
|
|
79
|
+
res.status(200).json({ ok: true, requestId, result, error: null });
|
|
92
80
|
};
|
|
93
81
|
const readQueueName = (payload) => {
|
|
94
|
-
const value = payload
|
|
95
|
-
if (typeof value !== 'string')
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return
|
|
82
|
+
const value = payload['queue'];
|
|
83
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
84
|
+
throw ErrorFactory.createValidationError('payload.queue is required');
|
|
85
|
+
}
|
|
86
|
+
return value.trim();
|
|
99
87
|
};
|
|
100
88
|
const executeAction = async (request) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
case 'enqueue': {
|
|
108
|
-
const payload = request.payload.payload;
|
|
109
|
-
if (!payload || typeof payload !== 'object') {
|
|
110
|
-
throw ErrorFactory.createValidationError('payload.payload is required for enqueue');
|
|
111
|
-
}
|
|
112
|
-
return BullMQRedisQueue.enqueue(queueName, payload);
|
|
89
|
+
const queueName = readQueueName(request.payload);
|
|
90
|
+
switch (request.action) {
|
|
91
|
+
case 'enqueue': {
|
|
92
|
+
const payload = request.payload['payload'];
|
|
93
|
+
if (!payload || typeof payload !== 'object') {
|
|
94
|
+
throw ErrorFactory.createValidationError('payload.payload is required for enqueue');
|
|
113
95
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return null;
|
|
96
|
+
return BullMQRedisQueue.enqueue(queueName, payload);
|
|
97
|
+
}
|
|
98
|
+
case 'dequeue':
|
|
99
|
+
return BullMQRedisQueue.dequeue(queueName);
|
|
100
|
+
case 'ack': {
|
|
101
|
+
const id = request.payload['id'];
|
|
102
|
+
if (typeof id !== 'string' || id.trim() === '') {
|
|
103
|
+
throw ErrorFactory.createValidationError('payload.id is required for ack');
|
|
123
104
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
case 'drain':
|
|
127
|
-
await BullMQRedisQueue.drain(queueName);
|
|
128
|
-
return null;
|
|
129
|
-
default:
|
|
130
|
-
throw ErrorFactory.createValidationError(`Unsupported action: ${String(request.action)}`);
|
|
105
|
+
await BullMQRedisQueue.ack(queueName, id);
|
|
106
|
+
return null;
|
|
131
107
|
}
|
|
132
|
-
|
|
108
|
+
case 'length':
|
|
109
|
+
return BullMQRedisQueue.length(queueName);
|
|
110
|
+
case 'drain':
|
|
111
|
+
await BullMQRedisQueue.drain(queueName);
|
|
112
|
+
return null;
|
|
113
|
+
default:
|
|
114
|
+
throw ErrorFactory.createValidationError(`Unsupported action: ${String(request.action)}`);
|
|
115
|
+
}
|
|
133
116
|
};
|
|
134
117
|
const verifyRequest = async (req, bodyText, settings) => {
|
|
135
118
|
if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
|
|
@@ -140,30 +123,25 @@ const verifyRequest = async (req, bodyText, settings) => {
|
|
|
140
123
|
message: 'Queue HTTP gateway signing credentials are not configured',
|
|
141
124
|
};
|
|
142
125
|
}
|
|
143
|
-
const url = new URL(req.getPath(), 'http://localhost');
|
|
144
126
|
const verifyResult = await SignedRequest.verify({
|
|
145
127
|
method: req.getMethod(),
|
|
146
|
-
url,
|
|
128
|
+
url: new URL(req.getPath(), 'http://localhost'),
|
|
147
129
|
body: bodyText,
|
|
148
130
|
headers: toIncomingHeaders(req),
|
|
149
131
|
nowMs: nowMs(),
|
|
150
132
|
windowMs: settings.signingWindowMs,
|
|
151
133
|
verifyNonce: async (keyId, nonce) => storeNonce(keyId, nonce, settings.nonceTtlMs),
|
|
152
|
-
getSecretForKeyId: async (keyId) =>
|
|
153
|
-
if (keyId === settings.keyId)
|
|
154
|
-
return settings.secret;
|
|
155
|
-
return undefined;
|
|
156
|
-
},
|
|
134
|
+
getSecretForKeyId: async (keyId) => (keyId === settings.keyId ? settings.secret : undefined),
|
|
157
135
|
});
|
|
158
136
|
if (verifyResult.ok === true)
|
|
159
137
|
return { ok: true };
|
|
160
|
-
const
|
|
161
|
-
const
|
|
138
|
+
const code = 'code' in verifyResult ? verifyResult.code : 'INVALID_SIGNATURE';
|
|
139
|
+
const message = 'message' in verifyResult ? verifyResult.message : 'Invalid signature';
|
|
162
140
|
return {
|
|
163
141
|
ok: false,
|
|
164
|
-
code
|
|
165
|
-
status:
|
|
166
|
-
message
|
|
142
|
+
code,
|
|
143
|
+
status: code === 'EXPIRED' || code === 'REPLAYED' ? 401 : 403,
|
|
144
|
+
message,
|
|
167
145
|
};
|
|
168
146
|
};
|
|
169
147
|
const createHandler = (settings) => {
|
|
@@ -178,37 +156,37 @@ const createHandler = (settings) => {
|
|
|
178
156
|
sendFailure(res, requestId, auth.status, auth.code, auth.message);
|
|
179
157
|
return;
|
|
180
158
|
}
|
|
181
|
-
|
|
182
|
-
const payload = body['payload'];
|
|
183
|
-
if (typeof action !== 'string') {
|
|
159
|
+
if (typeof body['action'] !== 'string') {
|
|
184
160
|
sendFailure(res, requestId, 400, 'VALIDATION_ERROR', 'action is required');
|
|
185
161
|
return;
|
|
186
162
|
}
|
|
187
|
-
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
163
|
+
if (!body['payload'] || typeof body['payload'] !== 'object' || Array.isArray(body['payload'])) {
|
|
188
164
|
sendFailure(res, requestId, 400, 'VALIDATION_ERROR', 'payload must be an object');
|
|
189
165
|
return;
|
|
190
166
|
}
|
|
191
|
-
const normalizedRequest = {
|
|
192
|
-
action: action,
|
|
193
|
-
requestId,
|
|
194
|
-
payload: payload,
|
|
195
|
-
};
|
|
196
167
|
try {
|
|
197
|
-
const result = await executeAction(
|
|
168
|
+
const result = await executeAction({
|
|
169
|
+
action: body['action'],
|
|
170
|
+
requestId,
|
|
171
|
+
payload: body['payload'],
|
|
172
|
+
});
|
|
198
173
|
sendSuccess(res, requestId, result);
|
|
199
174
|
}
|
|
200
175
|
catch (error) {
|
|
201
176
|
Logger.error('Queue HTTP gateway action failed', error);
|
|
202
|
-
sendFailure(res, requestId, 500, 'QUEUE_ERROR', 'Queue operation failed',
|
|
177
|
+
sendFailure(res, requestId, 500, 'QUEUE_ERROR', 'Queue operation failed', {
|
|
178
|
+
message: error instanceof Error ? error.message : String(error),
|
|
179
|
+
});
|
|
203
180
|
}
|
|
204
181
|
};
|
|
205
182
|
};
|
|
206
183
|
export const QueueHttpGateway = Object.freeze({
|
|
207
184
|
create(config) {
|
|
185
|
+
const defaults = readSettings();
|
|
208
186
|
const settings = {
|
|
209
|
-
...
|
|
187
|
+
...defaults,
|
|
210
188
|
...config,
|
|
211
|
-
basePath: normalizePath(config?.basePath ??
|
|
189
|
+
basePath: normalizePath(config?.basePath ?? defaults.basePath),
|
|
212
190
|
};
|
|
213
191
|
const routeOptions = settings.middleware.length > 0 ? { middleware: settings.middleware } : undefined;
|
|
214
192
|
return {
|
|
@@ -83,7 +83,10 @@ export const createRedisPublishClient = async () => {
|
|
|
83
83
|
*/
|
|
84
84
|
const tryCreateRedisClient = async (url) => {
|
|
85
85
|
try {
|
|
86
|
-
|
|
86
|
+
// Variable specifier so bundlers (esbuild/wrangler) do not inline node-redis
|
|
87
|
+
// into the Workers bundle — this path never runs on Workers (RPC proxy is used).
|
|
88
|
+
const redisPkg = 'redis';
|
|
89
|
+
const mod = (await import(redisPkg));
|
|
87
90
|
const client = mod.createClient({ url });
|
|
88
91
|
if (typeof client.connect === 'function') {
|
|
89
92
|
await connectClient(client, 'Redis publish client failed to connect');
|
|
@@ -99,7 +102,9 @@ const tryCreateRedisClient = async (url) => {
|
|
|
99
102
|
*/
|
|
100
103
|
const tryCreateIoRedisClient = async (url) => {
|
|
101
104
|
try {
|
|
102
|
-
|
|
105
|
+
// Variable specifier so bundlers do not inline ioredis into the Workers bundle.
|
|
106
|
+
const ioredisPkg = 'ioredis';
|
|
107
|
+
const mod = (await import(ioredisPkg));
|
|
103
108
|
const redis = mod.default(url);
|
|
104
109
|
const client = {
|
|
105
110
|
publish: (channel, message) => redis.publish(channel, message),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BullMQPayload, QueueMessage } from '@zintrust/core/queue';
|
|
2
|
+
export interface IQueueDriver {
|
|
3
|
+
enqueue(queue: string, payload: BullMQPayload): Promise<string>;
|
|
4
|
+
dequeue<T = unknown>(queue: string): Promise<QueueMessage<T> | undefined>;
|
|
5
|
+
ack(queue: string, id: string): Promise<void>;
|
|
6
|
+
length(queue: string): Promise<number>;
|
|
7
|
+
drain(queue: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare const shouldUseRedisRpcQueueDriver: () => boolean;
|
|
10
|
+
export declare const RedisRpcQueueDriver: IQueueDriver;
|
|
11
|
+
export default RedisRpcQueueDriver;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Env } from '@zintrust/core/config';
|
|
2
|
+
import { ErrorFactory } from '@zintrust/core/errors';
|
|
3
|
+
import { JobStateTracker, TimeoutManager } from '@zintrust/core/queue';
|
|
4
|
+
import { generateUuid } from '@zintrust/core/utils';
|
|
5
|
+
const REDIS_RPC_PACKAGE = '@zintrust/redis-rpc';
|
|
6
|
+
export const shouldUseRedisRpcQueueDriver = () => {
|
|
7
|
+
return Env.USE_REDIS_PROXY === true && Env.get('REDIS_RPC_URL', '').trim() !== '';
|
|
8
|
+
};
|
|
9
|
+
const resolveRpcBaseUrl = () => {
|
|
10
|
+
const configured = Env.get('REDIS_RPC_URL', '').trim();
|
|
11
|
+
if (configured.length > 0)
|
|
12
|
+
return configured;
|
|
13
|
+
const host = Env.get('REDIS_RPC_HOST', '127.0.0.1').trim() || '127.0.0.1';
|
|
14
|
+
const port = Env.getInt('REDIS_RPC_PORT', 8794);
|
|
15
|
+
return `http://${host}:${port}`;
|
|
16
|
+
};
|
|
17
|
+
const createRpcClient = async () => {
|
|
18
|
+
try {
|
|
19
|
+
const mod = (await import(REDIS_RPC_PACKAGE));
|
|
20
|
+
return mod.createRedisRpcClient({
|
|
21
|
+
baseUrl: resolveRpcBaseUrl(),
|
|
22
|
+
secret: Env.get('REDIS_RPC_SECRET', Env.get('REDIS_PROXY_SECRET', Env.APP_KEY)),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw ErrorFactory.createConfigError('@zintrust/redis-rpc is required when USE_REDIS_PROXY=true and REDIS_RPC_URL is configured', error);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const resolveRequestedJobId = (payloadData) => {
|
|
30
|
+
if (typeof payloadData?.jobId === 'string' && payloadData.jobId.trim().length > 0) {
|
|
31
|
+
return payloadData.jobId.trim();
|
|
32
|
+
}
|
|
33
|
+
return generateUuid();
|
|
34
|
+
};
|
|
35
|
+
const createJobOptions = (payloadData) => ({
|
|
36
|
+
jobId: resolveRequestedJobId(payloadData),
|
|
37
|
+
delay: payloadData.delay,
|
|
38
|
+
attempts: payloadData.attempts,
|
|
39
|
+
priority: payloadData.priority,
|
|
40
|
+
removeOnComplete: payloadData.removeOnComplete || 100,
|
|
41
|
+
removeOnFail: payloadData.removeOnFail || 50,
|
|
42
|
+
backoff: payloadData.backoff || {
|
|
43
|
+
type: 'exponential',
|
|
44
|
+
delay: 2000,
|
|
45
|
+
},
|
|
46
|
+
repeat: payloadData.repeat,
|
|
47
|
+
lifo: payloadData.lifo ?? false,
|
|
48
|
+
});
|
|
49
|
+
const resolveJobId = (result, fallback) => {
|
|
50
|
+
if (typeof result === 'object' && result !== null) {
|
|
51
|
+
const id = result.id;
|
|
52
|
+
if (typeof id === 'string' && id.trim().length > 0)
|
|
53
|
+
return id;
|
|
54
|
+
if (typeof id === 'number' && Number.isFinite(id))
|
|
55
|
+
return String(id);
|
|
56
|
+
}
|
|
57
|
+
return fallback;
|
|
58
|
+
};
|
|
59
|
+
const markPendingRecoveryFallback = async (input) => {
|
|
60
|
+
const payload = input.payload;
|
|
61
|
+
const currentAttempts = typeof payload['_currentAttempts'] === 'number' && Number.isFinite(payload['_currentAttempts'])
|
|
62
|
+
? Math.max(0, Math.floor(payload['_currentAttempts']))
|
|
63
|
+
: 0;
|
|
64
|
+
const maxAttempts = typeof payload['attempts'] === 'number' && Number.isFinite(payload['attempts'])
|
|
65
|
+
? Math.max(1, Math.floor(payload['attempts']))
|
|
66
|
+
: undefined;
|
|
67
|
+
const idempotencyKey = typeof payload['uniqueId'] === 'string' && payload['uniqueId'].trim().length > 0
|
|
68
|
+
? payload['uniqueId'].trim()
|
|
69
|
+
: undefined;
|
|
70
|
+
await JobStateTracker.enqueued({
|
|
71
|
+
queueName: input.queue,
|
|
72
|
+
jobId: input.fallbackJobId,
|
|
73
|
+
payload: input.payload,
|
|
74
|
+
attempts: currentAttempts,
|
|
75
|
+
maxAttempts,
|
|
76
|
+
idempotencyKey,
|
|
77
|
+
});
|
|
78
|
+
const pendingRecoveryApi = JobStateTracker;
|
|
79
|
+
await pendingRecoveryApi.pendingRecovery?.({
|
|
80
|
+
queueName: input.queue,
|
|
81
|
+
jobId: input.fallbackJobId,
|
|
82
|
+
reason: 'Redis RPC enqueue failed; marked pending recovery',
|
|
83
|
+
error: input.error,
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
export const RedisRpcQueueDriver = Object.freeze({
|
|
87
|
+
async enqueue(queue, payload) {
|
|
88
|
+
const fallbackJobId = resolveRequestedJobId(payload);
|
|
89
|
+
const timeoutMs = Env.getInt('REDIS_RPC_TIMEOUT_MS', Env.getInt('QUEUE_HTTP_PROXY_TIMEOUT_MS', 10000));
|
|
90
|
+
const options = createJobOptions({ ...payload, jobId: fallbackJobId });
|
|
91
|
+
try {
|
|
92
|
+
return await TimeoutManager.withTimeoutRetry(async () => {
|
|
93
|
+
const client = await createRpcClient();
|
|
94
|
+
const result = await client.queue('add', {
|
|
95
|
+
target: queue,
|
|
96
|
+
args: [`${queue}-job`, payload, options],
|
|
97
|
+
});
|
|
98
|
+
return resolveJobId(result, fallbackJobId);
|
|
99
|
+
}, {
|
|
100
|
+
timeoutMs,
|
|
101
|
+
maxRetries: Math.max(0, Env.getInt('REDIS_RPC_RETRY_MAX', Env.getInt('QUEUE_HTTP_PROXY_RETRY_MAX', 2))),
|
|
102
|
+
retryDelayMs: Math.max(0, Env.getInt('REDIS_RPC_RETRY_DELAY_MS', Env.getInt('QUEUE_HTTP_PROXY_RETRY_DELAY_MS', 500))),
|
|
103
|
+
operationName: `redis-rpc-queue-enqueue:${queue}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
await markPendingRecoveryFallback({ queue, fallbackJobId, payload, error });
|
|
108
|
+
return fallbackJobId;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
async dequeue(queue) {
|
|
112
|
+
const client = await createRpcClient();
|
|
113
|
+
return client.queue('dequeue', {
|
|
114
|
+
target: queue,
|
|
115
|
+
visibilityTimeoutMs: Env.getInt('QUEUE_REDIS_VISIBILITY_TIMEOUT_MS', 30_000),
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
async ack(queue, id) {
|
|
119
|
+
const client = await createRpcClient();
|
|
120
|
+
await client.queue('ack', { target: queue, args: [id] });
|
|
121
|
+
},
|
|
122
|
+
async length(queue) {
|
|
123
|
+
const client = await createRpcClient();
|
|
124
|
+
return client.queue('length', { target: queue });
|
|
125
|
+
},
|
|
126
|
+
async drain(queue) {
|
|
127
|
+
const client = await createRpcClient();
|
|
128
|
+
await client.queue('drain', { target: queue });
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
export default RedisRpcQueueDriver;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { BullMQRedisQueue } from './BullMQRedisQueue';
|
|
2
|
-
export { HttpQueueDriver } from './HttpQueueDriver';
|
|
3
2
|
export { QueueHttpGateway } from './QueueHttpGateway';
|
|
3
|
+
export { RedisRpcQueueDriver, shouldUseRedisRpcQueueDriver } from './RedisRpcQueueDriver';
|
|
4
4
|
export { createRedisPublishClient, resetPublishClient, type RedisPublishClient, } from './RedisPublishClient';
|
|
5
5
|
export type { QueueMessage } from '@zintrust/core/queue';
|
|
6
6
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { BullMQRedisQueue } from './BullMQRedisQueue.js';
|
|
2
|
-
export { HttpQueueDriver } from './HttpQueueDriver.js';
|
|
3
2
|
export { QueueHttpGateway } from './QueueHttpGateway.js';
|
|
3
|
+
export { RedisRpcQueueDriver, shouldUseRedisRpcQueueDriver } from './RedisRpcQueueDriver.js';
|
|
4
4
|
export { createRedisPublishClient, resetPublishClient, } from './RedisPublishClient.js';
|
|
5
5
|
/**
|
|
6
6
|
* Package version and build metadata
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/queue-redis",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "Redis queue driver for ZinTrust.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,13 @@
|
|
|
23
23
|
"node": ">=20.0.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@zintrust/core": "*"
|
|
26
|
+
"@zintrust/core": "*",
|
|
27
|
+
"@zintrust/redis-rpc": "*"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"@zintrust/redis-rpc": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
27
33
|
},
|
|
28
34
|
"publishConfig": {
|
|
29
35
|
"access": "public"
|
|
@@ -40,7 +46,7 @@
|
|
|
40
46
|
"prepublishOnly": "npm run build"
|
|
41
47
|
},
|
|
42
48
|
"dependencies": {
|
|
43
|
-
"ioredis": "^5.
|
|
49
|
+
"ioredis": "^5.11.0",
|
|
44
50
|
"redis": "^5.12.1"
|
|
45
51
|
}
|
|
46
52
|
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { BullMQPayload, QueueMessage } from '@zintrust/core/queue';
|
|
2
|
-
export type QueueRpcAction = 'enqueue' | 'dequeue' | 'ack' | 'length' | 'drain';
|
|
3
|
-
export interface IQueueDriver {
|
|
4
|
-
enqueue(queue: string, payload: BullMQPayload): Promise<string>;
|
|
5
|
-
dequeue<T = unknown>(queue: string): Promise<QueueMessage<T> | undefined>;
|
|
6
|
-
ack(queue: string, id: string): Promise<void>;
|
|
7
|
-
length(queue: string): Promise<number>;
|
|
8
|
-
drain(queue: string): Promise<void>;
|
|
9
|
-
}
|
|
10
|
-
export declare const HttpQueueDriver: Readonly<{
|
|
11
|
-
enqueue(queue: string, payload: BullMQPayload): Promise<string>;
|
|
12
|
-
dequeue<T = unknown>(queue: string): Promise<QueueMessage<T> | undefined>;
|
|
13
|
-
ack(queue: string, id: string): Promise<void>;
|
|
14
|
-
length(queue: string): Promise<number>;
|
|
15
|
-
drain(queue: string): Promise<void>;
|
|
16
|
-
}>;
|
|
17
|
-
export default HttpQueueDriver;
|
package/dist/HttpQueueDriver.js
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { Env } from '@zintrust/core/config';
|
|
2
|
-
import { ErrorFactory } from '@zintrust/core/errors';
|
|
3
|
-
import { Logger } from '@zintrust/core/logger';
|
|
4
|
-
import { JobStateTracker, TimeoutManager } from '@zintrust/core/queue';
|
|
5
|
-
import { SignedRequest } from '@zintrust/core/security';
|
|
6
|
-
import { generateUuid } from '@zintrust/core/utils';
|
|
7
|
-
const DEFAULT_PROXY_URL = 'http://127.0.0.1:7772';
|
|
8
|
-
const DEFAULT_ROUTE_PATH = '/api/_sys/queue/rpc';
|
|
9
|
-
const createTimeoutSignal = (timeoutMs) => {
|
|
10
|
-
if (timeoutMs <= 0)
|
|
11
|
-
return undefined;
|
|
12
|
-
const timeout = AbortSignal.timeout;
|
|
13
|
-
return typeof timeout === 'function' ? timeout(timeoutMs) : undefined;
|
|
14
|
-
};
|
|
15
|
-
const toBodyText = (payload) => JSON.stringify(payload);
|
|
16
|
-
const normalizeBaseUrl = (value) => {
|
|
17
|
-
const raw = value.trim();
|
|
18
|
-
return raw === '' ? DEFAULT_PROXY_URL : raw;
|
|
19
|
-
};
|
|
20
|
-
const normalizeRoutePath = (value) => {
|
|
21
|
-
const trimmed = value.trim();
|
|
22
|
-
if (trimmed === '')
|
|
23
|
-
return DEFAULT_ROUTE_PATH;
|
|
24
|
-
if (trimmed.startsWith('/'))
|
|
25
|
-
return trimmed;
|
|
26
|
-
return `/${trimmed}`;
|
|
27
|
-
};
|
|
28
|
-
const resolveSigningPrefix = (baseUrl) => {
|
|
29
|
-
try {
|
|
30
|
-
const parsed = new URL(baseUrl);
|
|
31
|
-
const path = parsed.pathname.endsWith('/') ? parsed.pathname.slice(0, -1) : parsed.pathname;
|
|
32
|
-
if (path === '' || path === '/')
|
|
33
|
-
return undefined;
|
|
34
|
-
return path;
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
const buildSigningUrl = (requestUrl, baseUrl) => {
|
|
41
|
-
const prefix = resolveSigningPrefix(baseUrl);
|
|
42
|
-
if (!prefix)
|
|
43
|
-
return requestUrl;
|
|
44
|
-
if (requestUrl.pathname === prefix || requestUrl.pathname.startsWith(`${prefix}/`)) {
|
|
45
|
-
const signingUrl = new URL(requestUrl.toString());
|
|
46
|
-
const stripped = requestUrl.pathname.slice(prefix.length);
|
|
47
|
-
signingUrl.pathname = stripped.startsWith('/') ? stripped : `/${stripped}`;
|
|
48
|
-
return signingUrl;
|
|
49
|
-
}
|
|
50
|
-
return requestUrl;
|
|
51
|
-
};
|
|
52
|
-
const resolveSettings = () => {
|
|
53
|
-
const baseUrl = normalizeBaseUrl(Env.get('QUEUE_HTTP_PROXY_URL', DEFAULT_PROXY_URL));
|
|
54
|
-
const routePath = normalizeRoutePath(Env.get('QUEUE_HTTP_PROXY_PATH', DEFAULT_ROUTE_PATH));
|
|
55
|
-
const keyId = Env.get('QUEUE_HTTP_PROXY_KEY_ID', Env.APP_NAME || 'zintrust').trim();
|
|
56
|
-
const configuredSecret = Env.get('QUEUE_HTTP_PROXY_KEY', '').trim();
|
|
57
|
-
const secret = configuredSecret === '' ? Env.APP_KEY : configuredSecret;
|
|
58
|
-
const timeoutMs = Env.getInt('QUEUE_HTTP_PROXY_TIMEOUT_MS', 10000);
|
|
59
|
-
if (secret.trim() === '') {
|
|
60
|
-
throw ErrorFactory.createConfigError('QUEUE_HTTP_PROXY_KEY or APP_KEY is required');
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
baseUrl,
|
|
64
|
-
routePath,
|
|
65
|
-
keyId,
|
|
66
|
-
secret,
|
|
67
|
-
timeoutMs,
|
|
68
|
-
};
|
|
69
|
-
};
|
|
70
|
-
const buildRpcUrl = (settings) => {
|
|
71
|
-
const url = new URL(settings.baseUrl);
|
|
72
|
-
const basePath = url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname;
|
|
73
|
-
const routePath = settings.routePath.startsWith('/')
|
|
74
|
-
? settings.routePath
|
|
75
|
-
: `/${settings.routePath}`;
|
|
76
|
-
url.pathname = `${basePath}${routePath}`;
|
|
77
|
-
url.search = '';
|
|
78
|
-
return url;
|
|
79
|
-
};
|
|
80
|
-
const parseJsonResponse = async (response) => {
|
|
81
|
-
const text = await response.text();
|
|
82
|
-
if (text.trim() === '') {
|
|
83
|
-
return {
|
|
84
|
-
ok: false,
|
|
85
|
-
error: { code: 'EMPTY_RESPONSE', message: 'Empty response from queue gateway' },
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
return JSON.parse(text);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return {
|
|
93
|
-
ok: false,
|
|
94
|
-
error: { code: 'INVALID_JSON', message: text },
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const ensureSuccessfulResponse = (response, requestId) => {
|
|
99
|
-
if (!response.ok) {
|
|
100
|
-
const code = response.error?.code || 'QUEUE_HTTP_PROXY_ERROR';
|
|
101
|
-
const message = response.error?.message || 'Queue gateway returned an error';
|
|
102
|
-
const details = {
|
|
103
|
-
code,
|
|
104
|
-
requestId,
|
|
105
|
-
gatewayRequestId: response.requestId,
|
|
106
|
-
details: response.error?.details,
|
|
107
|
-
};
|
|
108
|
-
throw ErrorFactory.createTryCatchError(message, details);
|
|
109
|
-
}
|
|
110
|
-
return response.result;
|
|
111
|
-
};
|
|
112
|
-
const callGateway = async (action, payload) => {
|
|
113
|
-
const settings = resolveSettings();
|
|
114
|
-
const url = buildRpcUrl(settings);
|
|
115
|
-
const requestId = generateUuid();
|
|
116
|
-
const requestBody = {
|
|
117
|
-
action,
|
|
118
|
-
requestId,
|
|
119
|
-
payload,
|
|
120
|
-
};
|
|
121
|
-
const bodyText = toBodyText(requestBody);
|
|
122
|
-
const signingUrl = buildSigningUrl(url, settings.baseUrl);
|
|
123
|
-
const params = {
|
|
124
|
-
method: 'POST',
|
|
125
|
-
url: signingUrl,
|
|
126
|
-
body: bodyText,
|
|
127
|
-
keyId: settings.keyId,
|
|
128
|
-
secret: settings.secret,
|
|
129
|
-
};
|
|
130
|
-
const signedHeaders = await SignedRequest.createHeaders(params);
|
|
131
|
-
try {
|
|
132
|
-
const response = await fetch(url, {
|
|
133
|
-
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
-
'Content-Type': 'application/json',
|
|
136
|
-
...signedHeaders,
|
|
137
|
-
},
|
|
138
|
-
body: bodyText,
|
|
139
|
-
signal: createTimeoutSignal(settings.timeoutMs),
|
|
140
|
-
});
|
|
141
|
-
const parsed = await parseJsonResponse(response);
|
|
142
|
-
if (!response.ok && parsed.ok === false) {
|
|
143
|
-
throw ErrorFactory.createConnectionError(parsed.error?.message || `Queue gateway HTTP error (${response.status})`, {
|
|
144
|
-
status: response.status,
|
|
145
|
-
requestId,
|
|
146
|
-
error: parsed.error,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
return ensureSuccessfulResponse(parsed, requestId);
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
153
|
-
throw ErrorFactory.createConnectionError('Queue gateway request timed out', {
|
|
154
|
-
timeoutMs: settings.timeoutMs,
|
|
155
|
-
requestId,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
throw error;
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
const resolveFallbackJobId = (payload) => {
|
|
162
|
-
if (typeof payload.jobId === 'string' && payload.jobId.trim().length > 0) {
|
|
163
|
-
return payload.jobId.trim();
|
|
164
|
-
}
|
|
165
|
-
return generateUuid();
|
|
166
|
-
};
|
|
167
|
-
const resolveCurrentAttempts = (payload) => {
|
|
168
|
-
const raw = payload['_currentAttempts'];
|
|
169
|
-
if (typeof raw !== 'number' || Number.isFinite(raw) === false)
|
|
170
|
-
return 0;
|
|
171
|
-
return Math.max(0, Math.floor(raw));
|
|
172
|
-
};
|
|
173
|
-
const resolveMaxAttempts = (payload) => {
|
|
174
|
-
if (typeof payload.attempts === 'number' && Number.isFinite(payload.attempts)) {
|
|
175
|
-
return Math.max(1, Math.floor(payload.attempts));
|
|
176
|
-
}
|
|
177
|
-
return undefined;
|
|
178
|
-
};
|
|
179
|
-
const resolveIdempotencyKey = (payload) => {
|
|
180
|
-
if (typeof payload.uniqueId !== 'string')
|
|
181
|
-
return undefined;
|
|
182
|
-
const trimmed = payload.uniqueId.trim();
|
|
183
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
184
|
-
};
|
|
185
|
-
const markPendingRecoveryFallback = async (input) => {
|
|
186
|
-
const currentAttempts = resolveCurrentAttempts(input.payload);
|
|
187
|
-
const maxAttempts = resolveMaxAttempts(input.payload);
|
|
188
|
-
const idempotencyKey = resolveIdempotencyKey(input.payload);
|
|
189
|
-
// Compatibility shim: consumers of `@zintrust/core` may have a narrower
|
|
190
|
-
// `JobStateTracker.enqueued` type than the runtime implementation.
|
|
191
|
-
const enqueuedApi = JobStateTracker;
|
|
192
|
-
await enqueuedApi.enqueued({
|
|
193
|
-
queueName: input.queue,
|
|
194
|
-
jobId: input.fallbackJobId,
|
|
195
|
-
payload: input.payload,
|
|
196
|
-
attempts: currentAttempts,
|
|
197
|
-
maxAttempts,
|
|
198
|
-
idempotencyKey,
|
|
199
|
-
});
|
|
200
|
-
const pendingRecoveryApi = JobStateTracker;
|
|
201
|
-
if (typeof pendingRecoveryApi.pendingRecovery === 'function') {
|
|
202
|
-
await pendingRecoveryApi.pendingRecovery({
|
|
203
|
-
queueName: input.queue,
|
|
204
|
-
jobId: input.fallbackJobId,
|
|
205
|
-
reason: 'HTTP queue proxy enqueue failed; marked pending recovery',
|
|
206
|
-
error: input.error,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
export const HttpQueueDriver = Object.freeze({
|
|
211
|
-
async enqueue(queue, payload) {
|
|
212
|
-
const fallbackJobId = resolveFallbackJobId(payload);
|
|
213
|
-
const timeoutMs = Env.getInt('QUEUE_HTTP_PROXY_TIMEOUT_MS', 10000);
|
|
214
|
-
try {
|
|
215
|
-
return await TimeoutManager.withTimeoutRetry(async () => callGateway('enqueue', { queue, payload }), {
|
|
216
|
-
timeoutMs,
|
|
217
|
-
maxRetries: Math.max(0, Env.getInt('QUEUE_HTTP_PROXY_RETRY_MAX', 2)),
|
|
218
|
-
retryDelayMs: Math.max(0, Env.getInt('QUEUE_HTTP_PROXY_RETRY_DELAY_MS', 500)),
|
|
219
|
-
operationName: `http-queue-enqueue:${queue}`,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
Logger.warn('HTTP queue enqueue failed; storing tracker fallback in memory', {
|
|
224
|
-
...Logger.withTraceSkipContext({
|
|
225
|
-
queue,
|
|
226
|
-
fallbackJobId,
|
|
227
|
-
error: error instanceof Error ? error.message : String(error),
|
|
228
|
-
}),
|
|
229
|
-
});
|
|
230
|
-
await markPendingRecoveryFallback({
|
|
231
|
-
queue,
|
|
232
|
-
fallbackJobId,
|
|
233
|
-
payload,
|
|
234
|
-
error,
|
|
235
|
-
});
|
|
236
|
-
Logger.warn('Job marked pending recovery in tracker', {
|
|
237
|
-
...Logger.withTraceSkipContext({
|
|
238
|
-
queue,
|
|
239
|
-
jobId: fallbackJobId,
|
|
240
|
-
}),
|
|
241
|
-
});
|
|
242
|
-
return fallbackJobId;
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
async dequeue(queue) {
|
|
246
|
-
return callGateway('dequeue', { queue });
|
|
247
|
-
},
|
|
248
|
-
async ack(queue, id) {
|
|
249
|
-
await callGateway('ack', { queue, id });
|
|
250
|
-
},
|
|
251
|
-
async length(queue) {
|
|
252
|
-
return callGateway('length', { queue });
|
|
253
|
-
},
|
|
254
|
-
async drain(queue) {
|
|
255
|
-
await callGateway('drain', { queue });
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
export default HttpQueueDriver;
|