queasy 0.2.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 (98) 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/Readme.md +9 -4
  5. package/biome.json +5 -1
  6. package/dist/client.d.ts +33 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +199 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/constants.d.ts +10 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/{src → dist}/constants.js +2 -10
  13. package/dist/constants.js.map +1 -0
  14. package/dist/errors.d.ts +7 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/{src → dist}/errors.js +1 -13
  17. package/dist/errors.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/manager.d.ts +19 -0
  23. package/dist/manager.d.ts.map +1 -0
  24. package/dist/manager.js +67 -0
  25. package/dist/manager.js.map +1 -0
  26. package/dist/pool.d.ts +29 -0
  27. package/dist/pool.d.ts.map +1 -0
  28. package/{src → dist}/pool.js +23 -82
  29. package/dist/pool.js.map +1 -0
  30. package/dist/queasy.lua +390 -0
  31. package/dist/queue.d.ts +22 -0
  32. package/dist/queue.d.ts.map +1 -0
  33. package/dist/queue.js +81 -0
  34. package/dist/queue.js.map +1 -0
  35. package/dist/types.d.ts +92 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/utils.d.ts +4 -0
  40. package/dist/utils.d.ts.map +1 -0
  41. package/dist/utils.js +24 -0
  42. package/dist/utils.js.map +1 -0
  43. package/dist/worker.d.ts +2 -0
  44. package/dist/worker.d.ts.map +1 -0
  45. package/dist/worker.js +42 -0
  46. package/dist/worker.js.map +1 -0
  47. package/docker-compose.yml +0 -2
  48. package/fuzztest/Readme.md +185 -0
  49. package/fuzztest/fuzz.ts +356 -0
  50. package/fuzztest/handlers/cascade-a.ts +90 -0
  51. package/fuzztest/handlers/cascade-b.ts +71 -0
  52. package/fuzztest/handlers/fail-handler.ts +47 -0
  53. package/fuzztest/handlers/periodic.ts +89 -0
  54. package/fuzztest/process.ts +100 -0
  55. package/fuzztest/shared/chaos.ts +29 -0
  56. package/fuzztest/shared/stream.ts +40 -0
  57. package/package.json +8 -7
  58. package/plans/redis-options.md +279 -0
  59. package/src/client.ts +246 -0
  60. package/src/constants.ts +33 -0
  61. package/src/errors.ts +13 -0
  62. package/src/index.ts +2 -0
  63. package/src/manager.ts +78 -0
  64. package/src/pool.ts +129 -0
  65. package/src/queasy.lua +2 -3
  66. package/src/queue.ts +108 -0
  67. package/src/types.ts +16 -0
  68. package/src/{utils.js → utils.ts} +3 -20
  69. package/src/{worker.js → worker.ts} +5 -12
  70. package/test/{client.test.js → client.test.ts} +6 -7
  71. package/test/{errors.test.js → errors.test.ts} +1 -1
  72. package/test/fixtures/always-fail-handler.ts +5 -0
  73. package/test/fixtures/data-logger-handler.ts +11 -0
  74. package/test/fixtures/failure-handler.ts +6 -0
  75. package/test/fixtures/permanent-error-handler.ts +6 -0
  76. package/test/fixtures/slow-handler.ts +6 -0
  77. package/test/fixtures/success-handler.js +0 -5
  78. package/test/fixtures/success-handler.ts +6 -0
  79. package/test/fixtures/with-failure-handler.ts +5 -0
  80. package/test/{guards.test.js → guards.test.ts} +21 -34
  81. package/test/{manager.test.js → manager.test.ts} +26 -34
  82. package/test/{pool.test.js → pool.test.ts} +14 -16
  83. package/test/{queue.test.js → queue.test.ts} +21 -21
  84. package/test/{redis-functions.test.js → redis-functions.test.ts} +14 -20
  85. package/test/{utils.test.js → utils.test.ts} +1 -1
  86. package/tsconfig.json +20 -0
  87. package/jsconfig.json +0 -17
  88. package/src/client.js +0 -258
  89. package/src/index.js +0 -2
  90. package/src/manager.js +0 -94
  91. package/src/queue.js +0 -154
  92. package/test/fixtures/always-fail-handler.js +0 -8
  93. package/test/fixtures/data-logger-handler.js +0 -19
  94. package/test/fixtures/failure-handler.js +0 -9
  95. package/test/fixtures/permanent-error-handler.js +0 -10
  96. package/test/fixtures/slow-handler.js +0 -9
  97. package/test/fixtures/with-failure-handler.js +0 -8
  98. /package/test/fixtures/{no-handle-handler.js → no-handle-handler.ts} +0 -0
@@ -1,15 +1,12 @@
1
1
  import { pathToFileURL } from 'node:url';
2
2
  import { parentPort, setEnvironmentData } from 'node:worker_threads';
3
- import { PermanentError } from './errors.js';
4
-
5
- /** @typedef {import('./types.js').ExecMessage} ExecMessage */
6
- /** @typedef {import('./types.js').DoneMessage} DoneMessage */
3
+ import { PermanentError } from './errors.ts';
4
+ import type { DoneMessage, ExecMessage } from './types.ts';
7
5
 
8
6
  if (!parentPort) throw new Error('Worker cannot be executed directly.');
9
7
  setEnvironmentData('queasy_worker_context', true);
10
8
 
11
- /** @param {ExecMessage} msg */
12
- parentPort.on('message', async (msg) => {
9
+ parentPort.on('message', async (msg: ExecMessage) => {
13
10
  const { handlerPath, job } = msg;
14
11
  try {
15
12
  const mod = await import(pathToFileURL(handlerPath).href);
@@ -20,7 +17,7 @@ parentPort.on('message', async (msg) => {
20
17
  await mod.handle(job.data, job);
21
18
  send({ op: 'done', jobId: job.id });
22
19
  } catch (err) {
23
- const { message, name, retryAt } = /** @type {Error & { retryAt?: number }} */ (err);
20
+ const { message, name, retryAt } = err as Error & { retryAt?: number };
24
21
 
25
22
  send({
26
23
  op: 'done',
@@ -35,10 +32,6 @@ parentPort.on('message', async (msg) => {
35
32
  }
36
33
  });
37
34
 
38
- /**
39
- * Send a message to the parentPort
40
- * @param {DoneMessage} message
41
- */
42
- function send(message) {
35
+ function send(message: DoneMessage): void {
43
36
  parentPort?.postMessage(message);
44
37
  }
@@ -1,20 +1,19 @@
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 = 'client-test';
7
8
 
8
- describe.skip('Client heartbeat', () => {
9
- /** @type {import('redis').RedisClientType} */
10
- let redis;
11
- /** @type {import('../src/client.js').Client} */
12
- let client;
9
+ describe('Client heartbeat', () => {
10
+ let redis: RedisClientType;
11
+ let client: Client;
13
12
 
14
13
  beforeEach(async () => {
15
14
  redis = createClient();
16
15
  await redis.connect();
17
- client = new Client(redis, 1);
16
+ client = await new Promise<Client>((res) => new Client({}, 1, res));
18
17
  client.scheduleBump = mock.fn();
19
18
  if (client.manager) client.manager.addQueue = mock.fn();
20
19
  });
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert';
2
2
  import { describe, it } from 'node:test';
3
- import { PermanentError, StallError } from '../src/errors.js';
3
+ import { PermanentError, StallError } from '../src/errors.ts';
4
4
 
5
5
  describe('Error classes', () => {
6
6
  it('PermanentError has correct name and message', () => {
@@ -0,0 +1,5 @@
1
+ import type { Job } from '../../src/types.ts';
2
+
3
+ export async function handle(_data: unknown, _job: Job): Promise<void> {
4
+ throw new Error('Always fails');
5
+ }
@@ -0,0 +1,11 @@
1
+ import type { Job } from '../../src/types.ts';
2
+
3
+ export const receivedJobs: { data: unknown; job: Job }[] = [];
4
+
5
+ export async function handle(data: unknown, job: Job): Promise<void> {
6
+ receivedJobs.push({ data, job });
7
+ }
8
+
9
+ export function clearLogs(): void {
10
+ receivedJobs.length = 0;
11
+ }
@@ -0,0 +1,6 @@
1
+ import type { Job } from '../../src/types.ts';
2
+
3
+ export async function handle(data: unknown, _job: Job): Promise<void> {
4
+ // Just succeed to process the failure
5
+ console.log('Failure handler called with:', data);
6
+ }
@@ -0,0 +1,6 @@
1
+ import { PermanentError } from '../../src/errors.ts';
2
+ import type { Job } from '../../src/types.ts';
3
+
4
+ export async function handle(_data: unknown, _job: Job): Promise<void> {
5
+ throw new PermanentError('Permanent failure');
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Job } from '../../src/types.ts';
2
+
3
+ export async function handle(data: { delay?: number }, _job: Job): Promise<void> {
4
+ const delay = data?.delay || 1000;
5
+ await new Promise((resolve) => setTimeout(resolve, delay));
6
+ }
@@ -1,8 +1,3 @@
1
- /**
2
- * Handler that always succeeds
3
- * @param {any} _data - Job data
4
- * @param {import('../../src/types.js').Job} _job - Job metadata
5
- */
6
1
  export async function handle(_data, _job) {
7
2
  // Job succeeds immediately
8
3
  return;
@@ -0,0 +1,6 @@
1
+ import type { Job } from '../../src/types.ts';
2
+
3
+ export async function handle(_data: unknown, _job: Job): Promise<void> {
4
+ // Job succeeds immediately
5
+ return;
6
+ }
@@ -0,0 +1,5 @@
1
+ import type { Job } from '../../src/types.ts';
2
+
3
+ export async function handle(_data: unknown, _job: Job): Promise<void> {
4
+ throw new Error('Always fails');
5
+ }
@@ -1,25 +1,20 @@
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
  describe('Client disconnect guard', () => {
7
- /** @type {import('redis').RedisClientType} */
8
- let redis;
9
- /** @type {import('../src/client.js').Client} */
10
- let client;
8
+ let client: Client;
11
9
 
12
- beforeEach(async () => {
13
- redis = createClient();
14
- await redis.connect();
10
+ beforeEach(() => {
15
11
  return new Promise((res) => {
16
- client = new Client(redis, 0, res);
12
+ client = new Client({}, 0, res);
17
13
  });
18
14
  });
19
15
 
20
16
  afterEach(async () => {
21
17
  await client.close();
22
- await redis.quit();
23
18
  });
24
19
 
25
20
  it('should throw from queue() when disconnected', () => {
@@ -29,23 +24,17 @@ describe('Client disconnect guard', () => {
29
24
  });
30
25
 
31
26
  describe('Queue disconnect guards', () => {
32
- /** @type {import('redis').RedisClientType} */
33
- let redis;
34
- /** @type {import('../src/client.js').Client} */
35
- let client;
27
+ let client: Client;
36
28
 
37
- beforeEach(async () => {
38
- redis = createClient();
39
- await redis.connect();
40
- client = new Client(redis, 1);
41
- if (client.manager) client.manager.addQueue = mock.fn();
42
- // Wait for installLuaFunctions to settle
43
- await new Promise((r) => setTimeout(r, 50));
29
+ beforeEach(() => {
30
+ return new Promise((res) => {
31
+ client = new Client({}, 1, res);
32
+ if (client.manager) client.manager.addQueue = mock.fn();
33
+ });
44
34
  });
45
35
 
46
36
  afterEach(async () => {
47
37
  await client.close();
48
- await redis.quit();
49
38
  });
50
39
 
51
40
  it('should throw from dispatch() when disconnected', async () => {
@@ -67,20 +56,16 @@ describe('Queue disconnect guards', () => {
67
56
  });
68
57
 
69
58
  it('should throw from listen() on non-processing client', async () => {
70
- const nonProcessingClient = new Client(redis, 0);
71
- // Wait for installLuaFunctions to settle
72
- await new Promise((r) => setTimeout(r, 50));
59
+ const nonProcessingClient = await new Promise<Client>((res) => new Client({}, 0, res));
73
60
  const q = nonProcessingClient.queue('guard-test');
74
61
  await assert.rejects(() => q.listen('/some/handler.js'), /non-processing/);
75
- await client.close();
62
+ await nonProcessingClient.close();
76
63
  });
77
64
  });
78
65
 
79
66
  describe('Client bump lost lock', () => {
80
- /** @type {import('redis').RedisClientType} */
81
- let redis;
82
- /** @type {import('../src/client.js').Client} */
83
- let client;
67
+ let redis: RedisClientType;
68
+ let client: Client;
84
69
 
85
70
  const QUEUE_NAME = 'bump-lock-test';
86
71
 
@@ -90,8 +75,10 @@ describe('Client bump lost lock', () => {
90
75
  const keys = await redis.keys(`{${QUEUE_NAME}}*`);
91
76
  if (keys.length > 0) await redis.del(keys);
92
77
 
93
- client = new Client(redis, 1);
94
- if (client.manager) client.manager.addQueue = mock.fn();
78
+ await new Promise((res) => {
79
+ client = new Client({}, 1, res);
80
+ if (client.manager) client.manager.addQueue = mock.fn();
81
+ });
95
82
  });
96
83
 
97
84
  afterEach(async () => {
@@ -104,7 +91,7 @@ describe('Client bump lost lock', () => {
104
91
  it('should close and emit disconnected when bump returns 0', async () => {
105
92
  const q = client.queue(QUEUE_NAME);
106
93
  await q.dispatch({ task: 'test' });
107
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
94
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
108
95
  await q.listen(handlerPath);
109
96
 
110
97
  // Dequeue to register the client in the expiry set and start heartbeats
@@ -127,7 +114,7 @@ describe('Client bump lost lock', () => {
127
114
  const q = client.queue(QUEUE_NAME);
128
115
  // Set up the queue entry so scheduleBump doesn't error
129
116
  await q.dispatch({ task: 'test' });
130
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
117
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
131
118
  await q.listen(handlerPath);
132
119
  await (await q.dequeue(1)).promise;
133
120
 
@@ -1,30 +1,28 @@
1
1
  import assert from 'node:assert';
2
2
  import { afterEach, beforeEach, describe, it, mock } from 'node:test';
3
+ import type { Mock } from 'node:test';
3
4
  import { createClient } from 'redis';
4
- import { Client } from '../src/index.js';
5
- import { Manager } from '../src/manager.js';
5
+ import type { RedisClientType } from 'redis';
6
+ import { Client } from '../src/index.ts';
7
+ import { Manager } from '../src/manager.ts';
8
+ import type { Pool } from '../src/pool.ts';
9
+ import type { Queue } from '../src/queue.ts';
10
+ import type { HandlerOptions } from '../src/types.ts';
6
11
 
7
12
  describe('Manager unit tests', () => {
8
- /**
9
- * @param {object} [overrides]
10
- * @returns {import('../src/manager.js').Manager}
11
- */
12
- function createManager(overrides = {}) {
13
- const pool = /** @type {import('../src/pool.js').Pool} */ ({
14
- capacity: 100,
15
- ...overrides,
16
- });
13
+ function createManager(overrides: Partial<Pool> = {}): Manager {
14
+ const pool = { capacity: 100, ...overrides } as Pool;
17
15
  return new Manager(pool);
18
16
  }
19
17
 
20
- /**
21
- * @param {{ handlerOptions?: Partial<import('../src/types.js').HandlerOptions>, dequeue?: import('node:test').Mock<any> }} [overrides]
22
- */
23
- function mockQueue(overrides = {}) {
24
- return /** @type {import('../src/queue.js').ProcessingQueue} */ ({
18
+ function mockQueue(overrides: {
19
+ handlerOptions?: Partial<HandlerOptions>;
20
+ dequeue?: Mock<() => Promise<{ count: number }>>;
21
+ } = {}): Queue {
22
+ return {
25
23
  handlerOptions: { size: 10, priority: 100, ...overrides.handlerOptions },
26
24
  dequeue: overrides.dequeue ?? mock.fn(async () => ({ count: 0 })),
27
- });
25
+ } as unknown as Queue;
28
26
  }
29
27
 
30
28
  it('should return early from next() when queues are empty', async () => {
@@ -41,7 +39,7 @@ describe('Manager unit tests', () => {
41
39
  // Wait for next() to run (addQueue calls next via setTimeout)
42
40
  await new Promise((r) => setTimeout(r, 50));
43
41
  // dequeue should NOT have been called since capacity (5) < size (10)
44
- assert.equal(/** @type {import('node:test').Mock<any>} */ (q.dequeue).mock.callCount(), 0);
42
+ assert.equal((q.dequeue as Mock<() => Promise<{ count: number }>>).mock.callCount(), 0);
45
43
  mgr.close();
46
44
  });
47
45
 
@@ -101,7 +99,7 @@ describe('Manager unit tests', () => {
101
99
  mgr.busyCount = 3;
102
100
  await mgr.next();
103
101
  // Lower priority value sorts before higher
104
- const priorities = mgr.queues.map((e) => e.queue.handlerOptions.priority);
102
+ const priorities = mgr.queues.map((e) => e.queue.handlerOptions!.priority);
105
103
  assert.ok(priorities.indexOf(50) < priorities.indexOf(200));
106
104
  mgr.close();
107
105
  });
@@ -142,7 +140,7 @@ describe('Manager unit tests', () => {
142
140
  mgr.busyCount = 3;
143
141
  await mgr.next();
144
142
  // Among entries with same priority and lastDequeuedAt: smaller size sorts first
145
- const sizes = mgr.queues.map((e) => e.queue.handlerOptions.size);
143
+ const sizes = mgr.queues.map((e) => e.queue.handlerOptions!.size);
146
144
  assert.ok(sizes.indexOf(5) < sizes.indexOf(20));
147
145
  mgr.close();
148
146
  });
@@ -151,10 +149,8 @@ describe('Manager unit tests', () => {
151
149
  const QUEUE_NAME = 'mgr-test';
152
150
 
153
151
  describe('Manager scheduling', () => {
154
- /** @type {import('redis').RedisClientType} */
155
- let redis;
156
- /** @type {import('../src/client.js').Client} */
157
- let client;
152
+ let redis: RedisClientType;
153
+ let client: Client;
158
154
 
159
155
  beforeEach(async () => {
160
156
  redis = createClient();
@@ -163,7 +159,9 @@ describe('Manager scheduling', () => {
163
159
  if (keys.length > 0) await redis.del(keys);
164
160
 
165
161
  // Do NOT mock manager.addQueue — let the manager drive dequeuing
166
- client = new Client(redis, 1);
162
+ await new Promise((res) => {
163
+ client = new Client({}, 1, res);
164
+ });
167
165
  });
168
166
 
169
167
  afterEach(async () => {
@@ -177,7 +175,7 @@ describe('Manager scheduling', () => {
177
175
  const q = client.queue(QUEUE_NAME);
178
176
  const jobId = await q.dispatch({ task: 'managed' });
179
177
 
180
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
178
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
181
179
  await q.listen(handlerPath);
182
180
 
183
181
  // Wait for manager to dequeue and process the job fully
@@ -197,7 +195,7 @@ describe('Manager scheduling', () => {
197
195
  const jobId1 = await q1.dispatch({ q: 1 });
198
196
  const jobId2 = await q2.dispatch({ q: 2 });
199
197
 
200
- const handlerPath = new URL('./fixtures/success-handler.js', import.meta.url).pathname;
198
+ const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
201
199
  await q1.listen(handlerPath);
202
200
  await q2.listen(handlerPath);
203
201
 
@@ -216,13 +214,7 @@ describe('Manager scheduling', () => {
216
214
  });
217
215
  });
218
216
 
219
- /**
220
- * Poll a condition until it returns true, with a timeout.
221
- * @param {() => Promise<boolean>} fn
222
- * @param {number} [timeout=5000]
223
- * @param {number} [interval=20]
224
- */
225
- async function poll(fn, timeout = 5000, interval = 20) {
217
+ async function poll(fn: () => Promise<boolean>, timeout = 5000, interval = 20): Promise<void> {
226
218
  const deadline = Date.now() + timeout;
227
219
  while (Date.now() < deadline) {
228
220
  if (await fn()) return;
@@ -1,8 +1,10 @@
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';
5
- import { Pool } from '../src/pool.js';
4
+ import type { RedisClientType } from 'redis';
5
+ import { Client } from '../src/index.ts';
6
+ import { Pool } from '../src/pool.ts';
7
+ import type { DoneMessage } from '../src/types.ts';
6
8
 
7
9
  describe('Pool unit tests', () => {
8
10
  /** Helper to create a fake job entry with a no-op timer */
@@ -11,12 +13,8 @@ describe('Pool unit tests', () => {
11
13
  return { resolve: () => {}, reject: () => {}, size, timer };
12
14
  }
13
15
 
14
- /** @param {Pool} pool */
15
- function getWorker(pool) {
16
- const entry = /** @type {import('../src/pool.js').WorkerEntry} */ (
17
- pool.workers.values().next().value
18
- );
19
- return entry;
16
+ function getWorker(pool: Pool) {
17
+ return pool.workers.values().next().value;
20
18
  }
21
19
 
22
20
  it('should warn and return on message with unknown job ID', () => {
@@ -100,10 +98,8 @@ describe('Pool unit tests', () => {
100
98
  const QUEUE_NAME = 'pool-test';
101
99
 
102
100
  describe('Pool stall and timeout', () => {
103
- /** @type {import('redis').RedisClientType} */
104
- let redis;
105
- /** @type {import('../src/client.js').Client} */
106
- let client;
101
+ let redis: RedisClientType;
102
+ let client: Client;
107
103
 
108
104
  beforeEach(async () => {
109
105
  redis = createClient();
@@ -111,8 +107,10 @@ describe('Pool stall and timeout', () => {
111
107
  const keys = await redis.keys(`{${QUEUE_NAME}}*`);
112
108
  if (keys.length > 0) await redis.del(keys);
113
109
 
114
- client = new Client(redis, 1);
115
- if (client.manager) client.manager.addQueue = mock.fn();
110
+ await new Promise((res) => {
111
+ client = new Client({}, 1, res);
112
+ if (client.manager) client.manager.addQueue = mock.fn();
113
+ });
116
114
  });
117
115
 
118
116
  afterEach(async () => {
@@ -126,7 +124,7 @@ describe('Pool stall and timeout', () => {
126
124
  const q = client.queue(QUEUE_NAME);
127
125
  const jobId = await q.dispatch({ delay: 500 });
128
126
 
129
- const handlerPath = new URL('./fixtures/slow-handler.js', import.meta.url).pathname;
127
+ const handlerPath = new URL('./fixtures/slow-handler.ts', import.meta.url).pathname;
130
128
  // Very short timeout (50ms) so pool.handleTimeout fires before the 500ms handler completes
131
129
  await q.listen(handlerPath, { timeout: 50, maxRetries: 5 });
132
130
 
@@ -141,7 +139,7 @@ describe('Pool stall and timeout', () => {
141
139
  const q = client.queue(QUEUE_NAME);
142
140
  const jobId = await q.dispatch({ delay: 2000 });
143
141
 
144
- const handlerPath = new URL('./fixtures/slow-handler.js', import.meta.url).pathname;
142
+ const handlerPath = new URL('./fixtures/slow-handler.ts', import.meta.url).pathname;
145
143
  await q.listen(handlerPath, { maxRetries: 5 });
146
144
 
147
145
  // Dequeue but don't await the promise — job is still processing
@@ -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();
@@ -19,10 +18,11 @@ describe('Queue E2E', () => {
19
18
  await redis.del(keys);
20
19
  }
21
20
 
22
- client = new Client(redis, 1);
23
-
24
- // Mock this so that no actual work is dequeued by the manager.
25
- if (client.manager) client.manager.addQueue = mock.fn();
21
+ await new Promise((res) => {
22
+ client = new Client({}, 1, res);
23
+ // Mock this so that no actual work is dequeued by the manager.
24
+ if (client.manager) client.manager.addQueue = mock.fn();
25
+ });
26
26
  });
27
27
 
28
28
  afterEach(async () => {
@@ -128,7 +128,7 @@ describe('Queue E2E', () => {
128
128
  const q = client.queue(QUEUE_NAME);
129
129
  const jobId = await q.dispatch({ greeting: 'hello' });
130
130
 
131
- 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;
132
132
  await q.listen(handlerPath);
133
133
  await (await q.dequeue(1)).promise;
134
134
 
@@ -148,7 +148,7 @@ describe('Queue E2E', () => {
148
148
  q.dispatch({ id: 3 }),
149
149
  ]);
150
150
 
151
- 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;
152
152
  await q.listen(handlerPath);
153
153
  await (await q.dequeue(5)).promise;
154
154
 
@@ -165,7 +165,7 @@ describe('Queue E2E', () => {
165
165
  const futureTime = Date.now() + 10000;
166
166
  const jobId = await q.dispatch({ task: 'future' }, { runAt: futureTime });
167
167
 
168
- 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;
169
169
  await q.listen(handlerPath);
170
170
  await (await q.dequeue(1)).promise;
171
171
 
@@ -184,7 +184,7 @@ describe('Queue E2E', () => {
184
184
  const cancelled = await q.cancel(jobId1);
185
185
  assert.ok(cancelled);
186
186
 
187
- 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;
188
188
  await q.listen(handlerPath);
189
189
  await (await q.dequeue(1)).promise;
190
190
 
@@ -212,7 +212,7 @@ describe('Queue E2E', () => {
212
212
  const jobId1 = await queue1.dispatch({ queue: 1 });
213
213
  const jobId2 = await queue2.dispatch({ queue: 2 });
214
214
 
215
- 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;
216
216
  await queue1.listen(handlerPath);
217
217
  await queue2.listen(handlerPath);
218
218
  // First wait for these jobs to be dequeued and sent to workers
@@ -245,9 +245,9 @@ describe('Queue E2E', () => {
245
245
  const q = client.queue('fail-opt');
246
246
  await q.dispatch({ task: 'will-fail' });
247
247
 
248
- 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)
249
249
  .pathname;
250
- 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;
251
251
 
252
252
  // listen with failHandler option — this covers queue.js lines 60-63
253
253
  await q.listen(mainHandler, { maxRetries: 0, failHandler });
@@ -270,7 +270,7 @@ describe('Queue E2E', () => {
270
270
  const q = client.queue(QUEUE_NAME);
271
271
  const jobId = await q.dispatch({ task: 'will-fail' });
272
272
 
273
- 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)
274
274
  .pathname;
275
275
  await q.listen(handlerPath, { maxRetries: 2, minBackoff: 1000, maxBackoff: 10000 });
276
276
 
@@ -295,7 +295,7 @@ describe('Queue E2E', () => {
295
295
  // Manually set stall_count to exceed maxStalls (default 3)
296
296
  await redis.hSet(`{stall-test}:waiting_job:${jobId}`, 'stall_count', '5');
297
297
 
298
- 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;
299
299
  await q.listen(handlerPath, { maxStalls: 3 });
300
300
  q.failKey = '{stall-test}-fail';
301
301
 
@@ -332,7 +332,7 @@ describe('Queue E2E', () => {
332
332
 
333
333
  await redis.hSet(`{stall-nofail}:waiting_job:${jobId}`, 'stall_count', '5');
334
334
 
335
- 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;
336
336
  await q.listen(handlerPath, { maxStalls: 3 });
337
337
  // No failKey set — job should just be finished
338
338
 
@@ -359,7 +359,7 @@ describe('Queue E2E', () => {
359
359
  const q = client.queue('bad-handler');
360
360
  await q.dispatch({ task: 'test' });
361
361
 
362
- 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)
363
363
  .pathname;
364
364
  await q.listen(handlerPath, { maxRetries: 0 });
365
365
  q.failKey = '{bad-handler}-fail';
@@ -397,7 +397,7 @@ describe('Queue E2E', () => {
397
397
  const q = client.queue('fail-test');
398
398
  const jobId = await q.dispatch({ task: 'will-fail' });
399
399
 
400
- 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)
401
401
  .pathname;
402
402
 
403
403
  // Listen without failHandler so no fail queue listener races us