@holo-js/queue-redis 0.3.27 → 0.4.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Holo-JS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ daae375d958d2b145cce1230cbb95158b7edb1913d9273e96512972fc44c2320
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ declare class RedisQueueDriver implements QueueAsyncDriver {
31
31
  readonly name: string;
32
32
  readonly driver: "redis";
33
33
  readonly mode: "async";
34
+ readonly acceptsJobId = true;
34
35
  private readonly connection;
35
36
  private readonly bullConnection;
36
37
  private readonly managedRedisConnection?;
@@ -42,6 +43,9 @@ declare class RedisQueueDriver implements QueueAsyncDriver {
42
43
  constructor(connection: NormalizedQueueRedisConnectionConfig, context: QueueDriverFactoryContext);
43
44
  private getQueuePrefix;
44
45
  private getQueue;
46
+ private getLockDuration;
47
+ private reportFailure;
48
+ private renewReservation;
45
49
  private createWorkerKey;
46
50
  private normalizeReserveBlockFor;
47
51
  private resolveWorkerDrainDelay;
@@ -60,6 +64,9 @@ declare class RedisQueueDriver implements QueueAsyncDriver {
60
64
  acknowledge(job: QueueReservedJob): Promise<void>;
61
65
  release(job: QueueReservedJob, options?: QueueReleaseOptions): Promise<void>;
62
66
  delete(job: QueueReservedJob): Promise<void>;
67
+ abandon(job: QueueReservedJob): void;
68
+ private countLegacyPausedJobs;
69
+ private countQueuedJobs;
63
70
  clear(input?: {
64
71
  readonly queueNames?: readonly string[];
65
72
  }): Promise<number>;
@@ -78,4 +85,5 @@ declare const redisQueueDriverInternals: {
78
85
  wrapRedisError: typeof wrapRedisError;
79
86
  };
80
87
 
81
- export { type QueueDriverFactory, RedisQueueDriver, RedisQueueDriverError, redisQueueDriverFactory, redisQueueDriverInternals };
88
+ export { RedisQueueDriver, RedisQueueDriverError, redisQueueDriverFactory, redisQueueDriverInternals };
89
+ export type { QueueDriverFactory };
package/dist/index.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/index.ts
2
+ import { reportError } from "@holo-js/kernel/error-reporting";
2
3
  import { randomUUID } from "crypto";
3
4
  import {
4
5
  Queue as BullQueue,
6
+ UnrecoverableError,
5
7
  Worker as BullWorker
6
8
  } from "bullmq";
7
9
  import Redis from "ioredis";
@@ -38,7 +40,7 @@ function parseClusterNodeUrl(url, label) {
38
40
  ...parsed.protocol === "rediss:" ? { tls: {} } : {}
39
41
  };
40
42
  } catch (error) {
41
- throw new Error(`[Holo Queue] ${label} is invalid: ${error instanceof Error ? error.message : String(error)}`);
43
+ throw new Error(`[Holo Queue] ${label} is invalid: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
42
44
  }
43
45
  }
44
46
  function resolveClusterStartupNodes(connection) {
@@ -132,9 +134,11 @@ var RedisQueueDriver = class {
132
134
  this.bullConnection = bullConnection;
133
135
  this.managedRedisConnection = bullConnection instanceof Redis || bullConnection instanceof Redis.Cluster ? bullConnection : void 0;
134
136
  }
137
+ context;
135
138
  name;
136
139
  driver = "redis";
137
140
  mode = "async";
141
+ acceptsJobId = true;
138
142
  connection;
139
143
  bullConnection;
140
144
  managedRedisConnection;
@@ -162,6 +166,26 @@ var RedisQueueDriver = class {
162
166
  this.queues.set(queueName, queue);
163
167
  return queue;
164
168
  }
169
+ getLockDuration() {
170
+ return this.connection.retryAfter * 1e3;
171
+ }
172
+ reportFailure(action, operation, error) {
173
+ reportError(wrapRedisError(this.name, action, error), { capability: "queue", operation });
174
+ }
175
+ renewReservation(job, token) {
176
+ const lockDuration = this.getLockDuration();
177
+ const renewal = setInterval(() => {
178
+ job.extendLock(token, lockDuration).then((extended) => {
179
+ if (extended === 0) {
180
+ clearInterval(renewal);
181
+ }
182
+ }, (error) => {
183
+ this.reportFailure("renew job reservation", "redis.renewReservation", error);
184
+ });
185
+ }, lockDuration / 2);
186
+ renewal.unref();
187
+ return renewal;
188
+ }
165
189
  createWorkerKey(queueName, blockFor) {
166
190
  return `${queueName}:${blockFor}`;
167
191
  }
@@ -196,7 +220,9 @@ var RedisQueueDriver = class {
196
220
  connection: this.bullConnection,
197
221
  prefix: this.getQueuePrefix(queueName),
198
222
  drainDelay: this.resolveWorkerDrainDelay(blockFor),
199
- lockDuration: this.connection.retryAfter * 1e3,
223
+ lockDuration: this.getLockDuration(),
224
+ stalledInterval: this.getLockDuration(),
225
+ maxStalledCount: Number.MAX_SAFE_INTEGER,
200
226
  removeOnComplete: { count: 0 },
201
227
  removeOnFail: { count: 0 }
202
228
  }
@@ -205,7 +231,13 @@ var RedisQueueDriver = class {
205
231
  queueName,
206
232
  worker
207
233
  };
208
- const ready = worker.waitUntilReady().then(() => workerRecord).catch(async (error) => {
234
+ worker.on("error", (error) => {
235
+ this.reportFailure("recover abandoned reservations", "redis.recoverAbandonedReservations", error);
236
+ });
237
+ const ready = worker.waitUntilReady().then(async () => {
238
+ await worker.startStalledCheckTimer();
239
+ return workerRecord;
240
+ }).catch(async (error) => {
209
241
  if (this.workers.get(workerKey) === workerRecord) {
210
242
  this.workers.delete(workerKey);
211
243
  }
@@ -265,7 +297,8 @@ var RedisQueueDriver = class {
265
297
  });
266
298
  this.reservations.set(token, {
267
299
  job,
268
- token
300
+ token,
301
+ renewal: this.renewReservation(job, token)
269
302
  });
270
303
  return {
271
304
  reservationId: token,
@@ -282,6 +315,7 @@ var RedisQueueDriver = class {
282
315
  }
283
316
  async settleReservation(reserved, action, callback) {
284
317
  const reservation = this.getReservation(reserved);
318
+ clearInterval(reservation.renewal);
285
319
  try {
286
320
  await callback(reservation);
287
321
  } catch (error) {
@@ -353,18 +387,40 @@ var RedisQueueDriver = class {
353
387
  }
354
388
  async delete(job) {
355
389
  await this.settleReservation(job, "delete job", async (reservation) => {
356
- reservation.job.discard();
357
- await reservation.job.moveToFailed(new Error("[Holo Queue] Job deleted."), reservation.token, false);
390
+ await reservation.job.moveToFailed(new UnrecoverableError("[Holo Queue] Job deleted."), reservation.token, false);
358
391
  });
359
392
  }
393
+ abandon(job) {
394
+ const reservation = this.reservations.get(job.reservationId);
395
+ if (!reservation) {
396
+ return;
397
+ }
398
+ clearInterval(reservation.renewal);
399
+ this.reservations.delete(job.reservationId);
400
+ }
401
+ async countLegacyPausedJobs(queueName) {
402
+ const client = await this.getQueue(queueName).getBackend().client;
403
+ const pausedKey = `${this.getQueuePrefix(queueName)}:${queueName}:paused`;
404
+ const count = await client.llen(pausedKey);
405
+ if (count === 0) {
406
+ return 0;
407
+ }
408
+ const [last] = await client.lrange(pausedKey, -1, -1);
409
+ return last?.startsWith("0:") ? count - 1 : count;
410
+ }
411
+ async countQueuedJobs(queueName) {
412
+ const counted = await this.getQueue(queueName).getJobCountByTypes("wait", "prioritized", "delayed");
413
+ return counted + await this.countLegacyPausedJobs(queueName);
414
+ }
360
415
  async clear(input) {
361
416
  try {
362
417
  const queueNames = this.normalizeQueueNames(input?.queueNames);
363
418
  let cleared = 0;
364
419
  for (const queueName of queueNames) {
365
- const queue = this.getQueue(queueName);
366
- cleared += await queue.getJobCountByTypes("wait", "paused", "prioritized", "delayed");
367
- await queue.drain(true);
420
+ const before = await this.countQueuedJobs(queueName);
421
+ await this.getQueue(queueName).drain(true);
422
+ const after = await this.countQueuedJobs(queueName);
423
+ cleared += Math.max(0, before - after);
368
424
  }
369
425
  return cleared;
370
426
  } catch (error) {
@@ -377,6 +433,9 @@ var RedisQueueDriver = class {
377
433
  ...this.queues.values()
378
434
  ];
379
435
  const workerReadyPromises = [...this.workerReadyPromises.values()];
436
+ for (const reservation of this.reservations.values()) {
437
+ clearInterval(reservation.renewal);
438
+ }
380
439
  this.reservations.clear();
381
440
  this.workerReadyPromises.clear();
382
441
  this.workers.clear();
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@holo-js/queue-redis",
3
- "version": "0.3.27",
3
+ "version": "0.4.0",
4
4
  "description": "Holo-JS Framework - Redis queue driver",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cobraprojects/holo-js.git",
10
+ "directory": "packages/queue-redis"
11
+ },
7
12
  "exports": {
8
13
  ".": {
9
14
  "types": "./dist/index.d.ts",
@@ -14,7 +19,8 @@
14
19
  "main": "./dist/index.mjs",
15
20
  "types": "./dist/index.d.ts",
16
21
  "files": [
17
- "dist"
22
+ "dist",
23
+ "LICENSE"
18
24
  ],
19
25
  "scripts": {
20
26
  "build": "tsup",
@@ -23,21 +29,22 @@
23
29
  "test": "vitest --run"
24
30
  },
25
31
  "peerDependencies": {
26
- "@holo-js/queue": "^0.3.27"
32
+ "@holo-js/queue": "^0.4.0"
27
33
  },
28
34
  "dependencies": {
29
- "bullmq": "^5.71.0",
30
- "ioredis": "^5.4.2",
35
+ "@holo-js/kernel": "^0.4.0",
36
+ "bullmq": "^6.4.0",
37
+ "ioredis": "^6.0.0",
31
38
  "tslib": "^2.8.1"
32
39
  },
33
40
  "devDependencies": {
34
- "@holo-js/queue": "^0.3.27",
35
- "@types/node": "^22.10.2",
36
- "tsup": "^8.3.5",
37
- "typescript": "^5.7.2",
38
- "vitest": "^4.1.5"
41
+ "@holo-js/queue": "^0.4.0",
42
+ "@types/node": "^22.20.5",
43
+ "tsup": "^9.0.0",
44
+ "typescript": "^6.0.3",
45
+ "vitest": "^5.0.5"
39
46
  },
40
47
  "engines": {
41
- "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
48
+ "node": "^22.18.0 || ^24.0.0 || ^26.0.0"
42
49
  }
43
50
  }