@zintrust/queue-redis 2.4.1 → 2.4.3

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.
@@ -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,7 +8,7 @@ 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[];
@@ -6,8 +6,20 @@ 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 { Queue } from 'bullmq';
10
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;
22
+ };
11
23
  /**
12
24
  * BullMQ Redis Queue Driver
13
25
  *
@@ -178,7 +190,8 @@ export const BullMQRedisQueue = (() => {
178
190
  await closeSharedConnection(sharedConnection);
179
191
  }
180
192
  };
181
- const getQueue = (queueName) => {
193
+ const getQueue = async (queueName) => {
194
+ const QueueCtor = await ensureBullmqLoaded();
182
195
  // Check if queue exists in cache
183
196
  if (queues.has(queueName)) {
184
197
  const existingQueue = queues.get(queueName);
@@ -210,7 +223,7 @@ export const BullMQRedisQueue = (() => {
210
223
  const backoffDelay = Env.getInt('BULLMQ_BACKOFF_DELAY', 2000);
211
224
  const backoffType = Env.get('BULLMQ_BACKOFF_TYPE', 'exponential');
212
225
  const prefix = getBullMQSafeQueueName();
213
- const queue = new Queue(queueName, {
226
+ const queue = new QueueCtor(queueName, {
214
227
  connection: connection,
215
228
  prefix,
216
229
  defaultJobOptions: {
@@ -385,7 +398,7 @@ export const BullMQRedisQueue = (() => {
385
398
  }
386
399
  let requestedJobId;
387
400
  try {
388
- const q = getQueue(queue);
401
+ const q = await getQueue(queue);
389
402
  // Extract BullMQ options from payload with proper typing
390
403
  const payloadData = payload;
391
404
  const jobOptions = createJobOptions(payloadData);
@@ -422,7 +435,7 @@ export const BullMQRedisQueue = (() => {
422
435
  return RedisRpcQueueDriver.dequeue(queue);
423
436
  }
424
437
  try {
425
- const q = getQueue(queue);
438
+ const q = await getQueue(queue);
426
439
  const jobs = await q.getJobs(['waiting'], 0, 1);
427
440
  if (jobs.length === 0)
428
441
  return undefined;
@@ -454,7 +467,7 @@ export const BullMQRedisQueue = (() => {
454
467
  return;
455
468
  }
456
469
  try {
457
- const q = getQueue(queue);
470
+ const q = await getQueue(queue);
458
471
  const job = await q.getJob(id);
459
472
  if (job) {
460
473
  await job.moveToCompleted('acknowledged', PULL_WORKER_TOKEN, false);
@@ -473,7 +486,7 @@ export const BullMQRedisQueue = (() => {
473
486
  return RedisRpcQueueDriver.length(queue);
474
487
  }
475
488
  try {
476
- const q = getQueue(queue);
489
+ const q = await getQueue(queue);
477
490
  const counts = await q.getJobCounts();
478
491
  return counts['waiting'] || 0;
479
492
  }
@@ -488,7 +501,7 @@ export const BullMQRedisQueue = (() => {
488
501
  return;
489
502
  }
490
503
  try {
491
- const q = getQueue(queue);
504
+ const q = await getQueue(queue);
492
505
  await q.drain();
493
506
  Logger.debug(`BullMQ: Queue ${queue} drained`);
494
507
  }
@@ -83,7 +83,10 @@ export const createRedisPublishClient = async () => {
83
83
  */
84
84
  const tryCreateRedisClient = async (url) => {
85
85
  try {
86
- const mod = (await import('redis'));
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
- const mod = (await import('ioredis'));
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),
@@ -2,7 +2,6 @@ import { Env } from '@zintrust/core/config';
2
2
  import { ErrorFactory } from '@zintrust/core/errors';
3
3
  import { JobStateTracker, TimeoutManager } from '@zintrust/core/queue';
4
4
  import { generateUuid } from '@zintrust/core/utils';
5
- const REDIS_RPC_PACKAGE = '@zintrust/redis-rpc';
6
5
  export const shouldUseRedisRpcQueueDriver = () => {
7
6
  return Env.USE_REDIS_PROXY === true && Env.get('REDIS_RPC_URL', '').trim() !== '';
8
7
  };
@@ -16,8 +15,8 @@ const resolveRpcBaseUrl = () => {
16
15
  };
17
16
  const createRpcClient = async () => {
18
17
  try {
19
- const mod = (await import(REDIS_RPC_PACKAGE));
20
- return mod.createRedisRpcClient({
18
+ const { createRedisRpcClient } = await import('@zintrust/redis-rpc/client');
19
+ return createRedisRpcClient({
21
20
  baseUrl: resolveRpcBaseUrl(),
22
21
  secret: Env.get('REDIS_RPC_SECRET', Env.get('REDIS_PROXY_SECRET', Env.APP_KEY)),
23
22
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/queue-redis",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "description": "Redis queue driver for ZinTrust.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -49,4 +49,4 @@
49
49
  "ioredis": "^5.11.0",
50
50
  "redis": "^5.12.1"
51
51
  }
52
- }
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;
@@ -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;
@@ -1,89 +0,0 @@
1
- {
2
- "name": "@zintrust/queue-redis",
3
- "version": "2.4.1",
4
- "buildDate": "2026-05-31T11:38:17.470Z",
5
- "buildEnvironment": {
6
- "node": "v22.22.1",
7
- "platform": "darwin",
8
- "arch": "arm64"
9
- },
10
- "git": {
11
- "commit": "e97b7b3d",
12
- "branch": "release"
13
- },
14
- "package": {
15
- "engines": {
16
- "node": ">=20.0.0"
17
- },
18
- "dependencies": [
19
- "ioredis",
20
- "redis"
21
- ],
22
- "peerDependencies": [
23
- "@zintrust/core",
24
- "@zintrust/redis-rpc"
25
- ]
26
- },
27
- "files": {
28
- "BullMQRedisQueue.d.ts": {
29
- "size": 918,
30
- "sha256": "fdcd271e7aa241cc3af89c979a4ee2538541d1442024bd6dda91c8b12f712c6f"
31
- },
32
- "BullMQRedisQueue.js": {
33
- "size": 21906,
34
- "sha256": "87fd1eb1a320ea85b4cffde44e6f86ec487332cf8cd7052b11fd1b89caf8a1e5"
35
- },
36
- "HttpQueueDriver.d.ts": {
37
- "size": 841,
38
- "sha256": "5ef4a0a87a0df070df1fd56c499996eeb6c280af7b018c3e0ee84de67fb401cb"
39
- },
40
- "HttpQueueDriver.js": {
41
- "size": 9322,
42
- "sha256": "228ef02377b6f72f1e1eea0c2ac463828d55cc97b4ec6bbb235e31e5c35cada1"
43
- },
44
- "QueueHttpGateway.d.ts": {
45
- "size": 437,
46
- "sha256": "9c45a8643136415b248828322442aac16cd4b2e0d22355c3b8ecf6188973ffe7"
47
- },
48
- "QueueHttpGateway.js": {
49
- "size": 7665,
50
- "sha256": "352a1c2de6bbbaf7cd684bb428454bdd3293aff070956b3ee912403fe1394992"
51
- },
52
- "RedisPublishClient.d.ts": {
53
- "size": 451,
54
- "sha256": "341a68a3b8603b453146dc721f9d2eaf0d65bb6a1acb67908a9c6f9542b9fd2d"
55
- },
56
- "RedisPublishClient.js": {
57
- "size": 5589,
58
- "sha256": "aed9cee5023addfdbbf1026d0a45c86d29a8f3e65ffbd01637c1e0e7f994213b"
59
- },
60
- "RedisRpcQueueDriver.d.ts": {
61
- "size": 549,
62
- "sha256": "19dc10ffc29e813a241a9c69184e988d94d1c16ffc82010c75cadc29a4833a93"
63
- },
64
- "RedisRpcQueueDriver.js": {
65
- "size": 5357,
66
- "sha256": "24bb11e1a9f8a163ec79b08dc326e0c337297f312d393347dad91d38b692a9ea"
67
- },
68
- "build-manifest.json": {
69
- "size": 2273,
70
- "sha256": "774ebf83d6b9fc3222555c6ace7c7318e4cdeba9e4f439bce38cfc99cef9cf66"
71
- },
72
- "index.d.ts": {
73
- "size": 609,
74
- "sha256": "c74e26117ad588f7d28dda0c71fdd17588e6f58a0c68ede56af2fb2795352726"
75
- },
76
- "index.js": {
77
- "size": 714,
78
- "sha256": "ea00335dbb0386bc3863d61def64aebcc330f8dcef348a663bae51055d1bcc20"
79
- },
80
- "register.d.ts": {
81
- "size": 170,
82
- "sha256": "e10ca97976730171bfaaaa25f484a30d06c6dca57c7cd85d700c202e0785a0b7"
83
- },
84
- "register.js": {
85
- "size": 593,
86
- "sha256": "9a4ca56fc5d0ae9ca9c5d6c89731b3331749d3e254e583992f311e3ec7add80b"
87
- }
88
- }
89
- }