queasy 0.3.0 → 0.3.1

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 (92) hide show
  1. package/.github/workflows/check.yml +3 -0
  2. package/.github/workflows/publish.yml +3 -0
  3. package/CLAUDE.md +5 -4
  4. package/biome.json +5 -1
  5. package/dist/client.d.ts +33 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +199 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/constants.d.ts +10 -0
  10. package/dist/constants.d.ts.map +1 -0
  11. package/{src → dist}/constants.js +1 -9
  12. package/dist/constants.js.map +1 -0
  13. package/dist/errors.d.ts +7 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/{src → dist}/errors.js +1 -13
  16. package/dist/errors.js.map +1 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +3 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/manager.d.ts +19 -0
  22. package/dist/manager.d.ts.map +1 -0
  23. package/dist/manager.js +67 -0
  24. package/dist/manager.js.map +1 -0
  25. package/dist/pool.d.ts +29 -0
  26. package/dist/pool.d.ts.map +1 -0
  27. package/{src → dist}/pool.js +23 -82
  28. package/dist/pool.js.map +1 -0
  29. package/dist/queasy.lua +390 -0
  30. package/dist/queue.d.ts +22 -0
  31. package/dist/queue.d.ts.map +1 -0
  32. package/dist/queue.js +81 -0
  33. package/dist/queue.js.map +1 -0
  34. package/dist/types.d.ts +92 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +2 -0
  37. package/dist/types.js.map +1 -0
  38. package/dist/utils.d.ts +4 -0
  39. package/dist/utils.d.ts.map +1 -0
  40. package/dist/utils.js +24 -0
  41. package/dist/utils.js.map +1 -0
  42. package/dist/worker.d.ts +2 -0
  43. package/dist/worker.d.ts.map +1 -0
  44. package/dist/worker.js +42 -0
  45. package/dist/worker.js.map +1 -0
  46. package/fuzztest/{fuzz.js → fuzz.ts} +53 -51
  47. package/fuzztest/handlers/{cascade-a.js → cascade-a.ts} +11 -15
  48. package/fuzztest/handlers/{cascade-b.js → cascade-b.ts} +8 -9
  49. package/fuzztest/handlers/{fail-handler.js → fail-handler.ts} +7 -12
  50. package/fuzztest/handlers/{periodic.js → periodic.ts} +11 -15
  51. package/fuzztest/{process.js → process.ts} +15 -15
  52. package/fuzztest/shared/{chaos.js → chaos.ts} +5 -4
  53. package/fuzztest/shared/{stream.js → stream.ts} +7 -7
  54. package/package.json +7 -5
  55. package/src/{client.js → client.ts} +86 -128
  56. package/src/constants.ts +33 -0
  57. package/src/errors.ts +13 -0
  58. package/src/index.ts +2 -0
  59. package/src/manager.ts +78 -0
  60. package/src/pool.ts +129 -0
  61. package/src/queue.ts +108 -0
  62. package/src/types.ts +1 -0
  63. package/src/{utils.js → utils.ts} +3 -20
  64. package/src/{worker.js → worker.ts} +5 -12
  65. package/test/{client.test.js → client.test.ts} +6 -7
  66. package/test/{errors.test.js → errors.test.ts} +1 -1
  67. package/test/fixtures/always-fail-handler.ts +5 -0
  68. package/test/fixtures/data-logger-handler.ts +11 -0
  69. package/test/fixtures/failure-handler.ts +6 -0
  70. package/test/fixtures/permanent-error-handler.ts +6 -0
  71. package/test/fixtures/slow-handler.ts +6 -0
  72. package/test/fixtures/success-handler.js +0 -5
  73. package/test/fixtures/success-handler.ts +6 -0
  74. package/test/fixtures/with-failure-handler.ts +5 -0
  75. package/test/{guards.test.js → guards.test.ts} +9 -12
  76. package/test/{manager.test.js → manager.test.ts} +23 -33
  77. package/test/{pool.test.js → pool.test.ts} +10 -14
  78. package/test/{queue.test.js → queue.test.ts} +16 -17
  79. package/test/{redis-functions.test.js → redis-functions.test.ts} +14 -20
  80. package/test/{utils.test.js → utils.test.ts} +1 -1
  81. package/tsconfig.json +20 -0
  82. package/jsconfig.json +0 -17
  83. package/src/index.js +0 -2
  84. package/src/manager.js +0 -94
  85. package/src/queue.js +0 -154
  86. package/test/fixtures/always-fail-handler.js +0 -8
  87. package/test/fixtures/data-logger-handler.js +0 -19
  88. package/test/fixtures/failure-handler.js +0 -9
  89. package/test/fixtures/permanent-error-handler.js +0 -10
  90. package/test/fixtures/slow-handler.js +0 -9
  91. package/test/fixtures/with-failure-handler.js +0 -8
  92. /package/test/fixtures/{no-handle-handler.js → no-handle-handler.ts} +0 -0
@@ -1,15 +1,14 @@
1
1
  import assert from 'node:assert';
2
2
  import { afterEach, beforeEach, describe, it, mock } from 'node:test';
3
3
  import { createClient } from 'redis';
4
- import { Client } from '../src/index.js';
4
+ import type { RedisClientType } from 'redis';
5
+ import { Client } from '../src/index.ts';
5
6
 
6
7
  const QUEUE_NAME = 'test';
7
8
 
8
9
  describe('Queue E2E', () => {
9
- /** @type {import('redis').RedisClientType} */
10
- let redis;
11
- /** @type {import('../src/client.js').Client}*/
12
- let client;
10
+ let redis: RedisClientType;
11
+ let client: Client;
13
12
 
14
13
  beforeEach(async () => {
15
14
  redis = createClient();
@@ -129,7 +128,7 @@ describe('Queue E2E', () => {
129
128
  const q = client.queue(QUEUE_NAME);
130
129
  const jobId = await q.dispatch({ greeting: 'hello' });
131
130
 
132
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
131
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
133
132
  await q.listen(handlerPath);
134
133
  await (await q.dequeue(1)).promise;
135
134
 
@@ -149,7 +148,7 @@ describe('Queue E2E', () => {
149
148
  q.dispatch({ id: 3 }),
150
149
  ]);
151
150
 
152
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
151
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
153
152
  await q.listen(handlerPath);
154
153
  await (await q.dequeue(5)).promise;
155
154
 
@@ -166,7 +165,7 @@ describe('Queue E2E', () => {
166
165
  const futureTime = Date.now() + 10000;
167
166
  const jobId = await q.dispatch({ task: 'future' }, { runAt: futureTime });
168
167
 
169
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
168
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
170
169
  await q.listen(handlerPath);
171
170
  await (await q.dequeue(1)).promise;
172
171
 
@@ -185,7 +184,7 @@ describe('Queue E2E', () => {
185
184
  const cancelled = await q.cancel(jobId1);
186
185
  assert.ok(cancelled);
187
186
 
188
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
187
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
189
188
  await q.listen(handlerPath);
190
189
  await (await q.dequeue(1)).promise;
191
190
 
@@ -213,7 +212,7 @@ describe('Queue E2E', () => {
213
212
  const jobId1 = await queue1.dispatch({ queue: 1 });
214
213
  const jobId2 = await queue2.dispatch({ queue: 2 });
215
214
 
216
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
215
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
217
216
  await queue1.listen(handlerPath);
218
217
  await queue2.listen(handlerPath);
219
218
  // First wait for these jobs to be dequeued and sent to workers
@@ -246,9 +245,9 @@ describe('Queue E2E', () => {
246
245
  const q = client.queue('fail-opt');
247
246
  await q.dispatch({ task: 'will-fail' });
248
247
 
249
- const mainHandler = new URL('./fixtures/with-failure-handler.js', import.meta.url)
248
+ const mainHandler = new URL('./fixtures/with-failure-handler.ts', import.meta.url)
250
249
  .pathname;
251
- const failHandler = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
250
+ const failHandler = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
252
251
 
253
252
  // listen with failHandler option — this covers queue.js lines 60-63
254
253
  await q.listen(mainHandler, { maxRetries: 0, failHandler });
@@ -271,7 +270,7 @@ describe('Queue E2E', () => {
271
270
  const q = client.queue(QUEUE_NAME);
272
271
  const jobId = await q.dispatch({ task: 'will-fail' });
273
272
 
274
- const handlerPath = new URL('./fixtures/always-fail-handler.js', import.meta.url)
273
+ const handlerPath = new URL('./fixtures/always-fail-handler.ts', import.meta.url)
275
274
  .pathname;
276
275
  await q.listen(handlerPath, { maxRetries: 2, minBackoff: 1000, maxBackoff: 10000 });
277
276
 
@@ -296,7 +295,7 @@ describe('Queue E2E', () => {
296
295
  // Manually set stall_count to exceed maxStalls (default 3)
297
296
  await redis.hSet(`{stall-test}:waiting_job:${jobId}`, 'stall_count', '5');
298
297
 
299
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
298
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
300
299
  await q.listen(handlerPath, { maxStalls: 3 });
301
300
  q.failKey = '{stall-test}-fail';
302
301
 
@@ -333,7 +332,7 @@ describe('Queue E2E', () => {
333
332
 
334
333
  await redis.hSet(`{stall-nofail}:waiting_job:${jobId}`, 'stall_count', '5');
335
334
 
336
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
335
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
337
336
  await q.listen(handlerPath, { maxStalls: 3 });
338
337
  // No failKey set — job should just be finished
339
338
 
@@ -360,7 +359,7 @@ describe('Queue E2E', () => {
360
359
  const q = client.queue('bad-handler');
361
360
  await q.dispatch({ task: 'test' });
362
361
 
363
- const handlerPath = new URL('./fixtures/no-handle-handler.js', import.meta.url)
362
+ const handlerPath = new URL('./fixtures/no-handle-handler.ts', import.meta.url)
364
363
  .pathname;
365
364
  await q.listen(handlerPath, { maxRetries: 0 });
366
365
  q.failKey = '{bad-handler}-fail';
@@ -398,7 +397,7 @@ describe('Queue E2E', () => {
398
397
  const q = client.queue('fail-test');
399
398
  const jobId = await q.dispatch({ task: 'will-fail' });
400
399
 
401
- const handlerPath = new URL('./fixtures/with-failure-handler.js', import.meta.url)
400
+ const handlerPath = new URL('./fixtures/with-failure-handler.ts', import.meta.url)
402
401
  .pathname;
403
402
 
404
403
  // Listen without failHandler so no fail queue listener races us
@@ -4,13 +4,13 @@ import { dirname, join } from 'node:path';
4
4
  import { after, afterEach, before, describe, it } from 'node:test';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { createClient } from 'redis';
7
+ import type { RedisClientType } from 'redis';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = dirname(__filename);
10
11
 
11
12
  describe('Redis Lua functions', () => {
12
- /** @type {import('redis').RedisClientType} */
13
- let redis;
13
+ let redis: RedisClientType;
14
14
  const QUEUE_NAME = '{test-queue}';
15
15
 
16
16
  before(async () => {
@@ -281,12 +281,10 @@ describe('Redis Lua functions', () => {
281
281
 
282
282
  // Dequeue
283
283
  const expiryTime = now + 10000;
284
- const result = /** @type {string[][]} */ (
285
- await redis.fCall('queasy_dequeue', {
286
- keys: [QUEUE_NAME],
287
- arguments: [clientId, now.toString(), expiryTime.toString(), '10'],
288
- })
289
- );
284
+ const result = await redis.fCall('queasy_dequeue', {
285
+ keys: [QUEUE_NAME],
286
+ arguments: [clientId, now.toString(), expiryTime.toString(), '10'],
287
+ }) as string[][];
290
288
 
291
289
  assert.equal(result.length, 2);
292
290
  assert.deepEqual(result[0], ['id', jobId1]);
@@ -318,12 +316,10 @@ describe('Redis Lua functions', () => {
318
316
  await redis.hSet(`${QUEUE_NAME}:waiting_job:${jobId}`, 'id', jobId);
319
317
 
320
318
  // Try to dequeue
321
- const result = /** @type {string[][]} */ (
322
- await redis.fCall('queasy_dequeue', {
323
- keys: [QUEUE_NAME],
324
- arguments: [clientId, now.toString(), String(now + 10000), '10'],
325
- })
326
- );
319
+ const result = await redis.fCall('queasy_dequeue', {
320
+ keys: [QUEUE_NAME],
321
+ arguments: [clientId, now.toString(), String(now + 10000), '10'],
322
+ }) as string[][];
327
323
 
328
324
  assert.equal(result.length, 0);
329
325
  });
@@ -338,12 +334,10 @@ describe('Redis Lua functions', () => {
338
334
  await redis.hSet(`${QUEUE_NAME}:waiting_job:${jobId}`, 'id', jobId);
339
335
 
340
336
  // Try to dequeue
341
- const result = /** @type {string[][]} */ (
342
- await redis.fCall('queasy_dequeue', {
343
- keys: [QUEUE_NAME],
344
- arguments: [clientId, now.toString(), String(now + 10000), '10'],
345
- })
346
- );
337
+ const result = await redis.fCall('queasy_dequeue', {
338
+ keys: [QUEUE_NAME],
339
+ arguments: [clientId, now.toString(), String(now + 10000), '10'],
340
+ }) as string[][];
347
341
 
348
342
  assert.equal(result.length, 0);
349
343
  });
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert';
2
2
  import { describe, it } from 'node:test';
3
- import { compareSemver, parseVersion } from '../src/utils.js';
3
+ import { compareSemver, parseVersion } from '../src/utils.ts';
4
4
 
5
5
  describe('parseVersion', () => {
6
6
  it('should parse a simple major.minor version', () => {
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "target": "esnext",
5
+ "module": "nodenext",
6
+ "moduleResolution": "nodenext",
7
+ "rewriteRelativeImportExtensions": true,
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "declaration": true,
11
+ "declarationMap": true,
12
+ "sourceMap": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "erasableSyntaxOnly": true,
16
+ "resolveJsonModule": true,
17
+ "lib": ["ES2022"]
18
+ },
19
+ "include": ["src/**/*.ts"]
20
+ }
package/jsconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "checkJs": true,
4
- "strict": true,
5
- "target": "ES2022",
6
- "module": "ESNext",
7
- "moduleResolution": "node",
8
- "allowSyntheticDefaultImports": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true,
13
- "noEmit": true,
14
- "lib": ["ES2022"]
15
- },
16
- "include": ["src/**/*.js", "test/**/*.js"]
17
- }
package/src/index.js DELETED
@@ -1,2 +0,0 @@
1
- export { Client } from './client.js';
2
- export { PermanentError, StallError } from './errors.js';
package/src/manager.js DELETED
@@ -1,94 +0,0 @@
1
- /**
2
- * This class manages resource allocation between
3
- * different queues based on the size of the queue
4
- */
5
-
6
- import { DEQUEUE_INTERVAL } from './constants.js';
7
-
8
- /** @typedef {import('./pool').Pool} Pool */
9
- /** @typedef {import('./queue').ProcessingQueue} Queue */
10
- /** @typedef {{ queue: Queue, lastDequeuedAt: number, isBusy: boolean }} QueueEntry */
11
-
12
- export class Manager {
13
- /** @param {Pool} pool */
14
- constructor(pool) {
15
- this.pool = pool;
16
-
17
- /** @type {Array<QueueEntry>} */
18
- this.queues = [];
19
-
20
- /** @type {NodeJS.Timeout?} */
21
- this.timer = null;
22
-
23
- this.busyCount = 0;
24
- }
25
-
26
- /** @param {Queue} queue */
27
- addQueue(queue) {
28
- // Add this at the beginning so we dequeue it at the next available opportunity.
29
- this.queues.unshift({ queue, lastDequeuedAt: 0, isBusy: false });
30
- this.busyCount += 1;
31
-
32
- // This delay is required for queue listen tests
33
- // as they need to be able to control dequeueing
34
- this.next();
35
- }
36
-
37
- async next() {
38
- // If this function is called while the previous execution is in progress,
39
- // we do not want both executions to use the same queue.
40
- const entry = this.queues.shift();
41
- if (!entry) return;
42
- if (this.timer) {
43
- clearTimeout(this.timer);
44
- this.timer = null;
45
- }
46
-
47
- const size = entry.queue.handlerOptions.size;
48
- if (this.pool.capacity < size) return;
49
-
50
- const batchSize = Math.max(1, Math.floor(this.pool.capacity / this.busyCount / size));
51
- entry.lastDequeuedAt = Date.now(); // We store the time just before the call to dequeue.
52
- const { count } = await entry.queue.dequeue(batchSize);
53
-
54
- // Update
55
- const nowBusy = count >= batchSize;
56
- this.busyCount += Number(nowBusy) - Number(entry.isBusy);
57
- entry.isBusy = nowBusy;
58
-
59
- this.queues.push(entry);
60
- this.queues.sort(compareQueueEntries);
61
-
62
- if (!this.timer && this.queues.length) {
63
- const { isBusy, lastDequeuedAt } = this.queues[0];
64
- // If the current top queue is busy, retry now.
65
- const delay = isBusy ? 0 : Math.max(0, lastDequeuedAt - Date.now() + DEQUEUE_INTERVAL);
66
- this.timer = setTimeout(() => this.next(), delay);
67
- }
68
- }
69
-
70
- close() {
71
- if (this.timer) clearTimeout(this.timer);
72
- }
73
- }
74
-
75
- /**
76
- * @param {QueueEntry} a
77
- * @param {QueueEntry} b
78
- * @returns -1 | 0 | 1
79
- */
80
- function compareQueueEntries(a, b) {
81
- if (a.isBusy > b.isBusy) return -1; // a busy, b not -> a first
82
- if (a.isBusy < b.isBusy) return 1; // a free, b busy -> b first
83
-
84
- if (a.queue.handlerOptions.priority > b.queue.handlerOptions.priority) return 1; // a higher -> a first
85
- if (a.queue.handlerOptions.priority < b.queue.handlerOptions.priority) return -1; // b higher -> b first
86
-
87
- if (a.lastDequeuedAt > b.lastDequeuedAt) return -1; // a newer -> b first
88
- if (a.lastDequeuedAt < b.lastDequeuedAt) return 1; // a older -> a first
89
-
90
- if (a.queue.handlerOptions.size > b.queue.handlerOptions.size) return 1; // a larger -> a first
91
- if (a.queue.handlerOptions.size < b.queue.handlerOptions.size) return -1; // b larger -> b first
92
-
93
- return 0;
94
- }
package/src/queue.js DELETED
@@ -1,154 +0,0 @@
1
- import { DEFAULT_RETRY_OPTIONS, FAILJOB_RETRY_OPTIONS } from './constants.js';
2
- import { generateId } from './utils.js';
3
-
4
- // Import types:
5
- /** @typedef {import('redis').RedisClientType} RedisClient */
6
- /** @typedef {import('./types').HandlerOptions} HandlerOptions */
7
- /** @typedef {import('./types').ListenOptions} ListenOptions */
8
- /** @typedef {import('./types').JobOptions} JobOptions */
9
- /** @typedef {import('./types').Job} Job */
10
- /** @typedef {import('./types').DoneMessage} DoneMessage */
11
- /** @typedef {import('./client').Client} Client */
12
- /** @typedef {import('./pool').Pool} Pool */
13
- /** @typedef {import('./manager').Manager} Manager */
14
-
15
- /** @typedef {Required<Partial<Pick<Queue, keyof Queue>>>} ProcessingQueue */
16
-
17
- /**
18
- * Queue instance for managing a named job queue
19
- */
20
- export class Queue {
21
- /**
22
- * @param {string} key - Queue key
23
- * @param {Client} client - Redis client wrapper
24
- * @param {Pool | undefined} pool - Worker pool
25
- * @param {Manager | undefined} manager - Capacity allocation manager
26
- */
27
- constructor(key, client, pool, manager) {
28
- this.key = key;
29
- this.client = client;
30
- this.pool = pool;
31
- this.manager = manager;
32
-
33
- /** @type {NodeJS.Timeout | undefined} */
34
- // this.dequeueInterval = undefined;
35
-
36
- /** @type {Required<HandlerOptions> | undefined} */
37
- this.handlerOptions = undefined;
38
-
39
- /** @type {string | undefined} */
40
- this.handlerPath = undefined;
41
-
42
- /** @type {string | undefined} */
43
- this.failKey = undefined;
44
- }
45
-
46
- /**
47
- * Attach handlers to process jobs from this queue
48
- * @param {string} handlerPath - Path to handler module
49
- * @param {ListenOptions} [options] - Retry strategy options and failure handler
50
- * @returns {Promise<void>}
51
- */
52
- async listen(handlerPath, { failHandler, failRetryOptions, ...retryOptions } = {}) {
53
- if (this.client.disconnected) throw new Error('Can’t listen: client disconnected');
54
- if (!this.pool || !this.manager) throw new Error('Can’t listen: non-processing client');
55
-
56
- this.handlerPath = handlerPath;
57
- this.handlerOptions = { ...DEFAULT_RETRY_OPTIONS, ...retryOptions };
58
-
59
- // Initialize failure handler on all workers if provided
60
- if (failHandler) {
61
- this.failKey = `${this.key}-fail`;
62
- const failQueue = this.client.queue(this.failKey, true);
63
- failQueue.listen(failHandler, { ...FAILJOB_RETRY_OPTIONS, ...failRetryOptions });
64
- }
65
-
66
- this.manager.addQueue(/** @type {ProcessingQueue} */ (this));
67
-
68
- // if (!this.dequeueInterval) {
69
- // this.dequeueInterval = setInterval(() => this.dequeue(), DEQUEUE_INTERVAL);
70
- // }
71
- }
72
-
73
- /**
74
- * Add a job to the queue
75
- * @param {any} data - Job data (any JSON-serializable value)
76
- * @param {JobOptions} [options] - Job options
77
- * @returns {Promise<string>} Job ID
78
- */
79
- async dispatch(data, options = {}) {
80
- if (this.client.disconnected) throw new Error('Can’t dispatch: client disconnected');
81
- const {
82
- id = generateId(),
83
- runAt = 0,
84
- updateData = false,
85
- updateRunAt = false,
86
- resetCounts = false,
87
- } = options;
88
-
89
- await this.client.dispatch(this.key, id, runAt, data, updateData, updateRunAt, resetCounts);
90
- return id;
91
- }
92
-
93
- /**
94
- * Cancel a waiting job
95
- * @param {string} id - Job ID
96
- * @returns {Promise<boolean>} True if job was cancelled
97
- */
98
- async cancel(id) {
99
- if (this.client.disconnected) throw new Error('Can’t cancel: client disconnected');
100
- return await this.client.cancel(this.key, id);
101
- }
102
-
103
- /**
104
- * Picks jobs from the queue and processes them
105
- * @param {number} count
106
- * @returns {Promise<{count: number, promise: Promise<Array<unknown>>}>}
107
- */
108
-
109
- async dequeue(count) {
110
- const { pool, handlerPath, handlerOptions } = /** @type {ProcessingQueue} */ (this);
111
- const { maxRetries, maxStalls, maxBackoff, minBackoff, size, timeout } = handlerOptions;
112
-
113
- // const capacity = pool.getCapacity(size);
114
- // if (capacity <= 0) return;
115
-
116
- const jobs = await this.client.dequeue(this.key, count);
117
-
118
- const promise = Promise.all(
119
- jobs.map(async (job) => {
120
- // Check if job has exceeded stall limit
121
- if (job.stallCount >= maxStalls) {
122
- // Job has stalled too many times - fail it permanently
123
- if (!this.failKey) return this.client.finish(this.key, job.id);
124
-
125
- const failJobData = [job.id, job.data, { message: 'Max stalls exceeded' }];
126
- return this.client.fail(this.key, this.failKey, job.id, failJobData);
127
- }
128
-
129
- try {
130
- await pool.process(handlerPath, job, size, timeout);
131
- await this.client.finish(this.key, job.id);
132
- } catch (message) {
133
- const { error } = /** @type {Required<DoneMessage>} */ (message);
134
- const { retryAt = 0, kind } = error;
135
-
136
- if (kind === 'permanent' || job.retryCount >= maxRetries) {
137
- if (!this.failKey) return this.client.finish(this.key, job.id);
138
-
139
- const failJobData = [job.id, job.data, error];
140
- return this.client.fail(this.key, this.failKey, job.id, failJobData);
141
- }
142
-
143
- const backoffUntil =
144
- Date.now() + Math.min(maxBackoff, minBackoff * 2 ** job.retryCount);
145
-
146
- // Retriable error: call retry
147
- await this.client.retry(this.key, job.id, Math.max(retryAt, backoffUntil));
148
- }
149
- })
150
- );
151
-
152
- return { count: jobs.length, promise };
153
- }
154
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * Handler that always fails with a retriable error
3
- * @param {any} _data - Job data
4
- * @param {import('../../src/types.js').Job} _job - Job metadata
5
- */
6
- export async function handle(_data, _job) {
7
- throw new Error('Always fails');
8
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * Handler that logs received data to a global array (for testing)
3
- * @param {any} data - Job data
4
- * @param {import('../../src/types.js').Job} job - Job metadata
5
- */
6
- /** @type {{ data: any, job: import('../../src/types.js').Job }[]} */
7
- export const receivedJobs = [];
8
-
9
- /**
10
- * @param {any} data - Job data
11
- * @param {import('../../src/types.js').Job} job - Job metadata
12
- */
13
- export async function handle(data, job) {
14
- receivedJobs.push({ data, job });
15
- }
16
-
17
- export function clearLogs() {
18
- receivedJobs.length = 0;
19
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Failure handler
3
- * @param {any} data - Failure data [originalJobId, originalData, error]
4
- * @param {import('../../src/types.js').Job} _job - Job metadata
5
- */
6
- export async function handle(data, _job) {
7
- // Just succeed to process the failure
8
- console.log('Failure handler called with:', data);
9
- }
@@ -1,10 +0,0 @@
1
- import { PermanentError } from '../../src/errors.js';
2
-
3
- /**
4
- * Handler that throws a PermanentError
5
- * @param {any} _data - Job data
6
- * @param {import('../../src/types.js').Job} _job - Job metadata
7
- */
8
- export async function handle(_data, _job) {
9
- throw new PermanentError('Permanent failure');
10
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Handler that takes some time to complete (for testing heartbeat)
3
- * @param {any} data - Job data
4
- * @param {import('../../src/types.js').Job} _job - Job metadata
5
- */
6
- export async function handle(data, _job) {
7
- const delay = data?.delay || 1000;
8
- await new Promise((resolve) => setTimeout(resolve, delay));
9
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * Handler that always fails
3
- * @param {any} _data - Job data
4
- * @param {import('../../src/types.js').Job} _job - Job metadata
5
- */
6
- export async function handle(_data, _job) {
7
- throw new Error('Always fails');
8
- }