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.
- package/.github/workflows/check.yml +3 -0
- package/.github/workflows/publish.yml +3 -0
- package/CLAUDE.md +5 -4
- package/Readme.md +9 -4
- package/biome.json +5 -1
- package/dist/client.d.ts +33 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +199 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/{src → dist}/constants.js +2 -10
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/{src → dist}/errors.js +1 -13
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +19 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +67 -0
- package/dist/manager.js.map +1 -0
- package/dist/pool.d.ts +29 -0
- package/dist/pool.d.ts.map +1 -0
- package/{src → dist}/pool.js +23 -82
- package/dist/pool.js.map +1 -0
- package/dist/queasy.lua +390 -0
- package/dist/queue.d.ts +22 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +81 -0
- package/dist/queue.js.map +1 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +24 -0
- package/dist/utils.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +42 -0
- package/dist/worker.js.map +1 -0
- package/docker-compose.yml +0 -2
- package/fuzztest/Readme.md +185 -0
- package/fuzztest/fuzz.ts +356 -0
- package/fuzztest/handlers/cascade-a.ts +90 -0
- package/fuzztest/handlers/cascade-b.ts +71 -0
- package/fuzztest/handlers/fail-handler.ts +47 -0
- package/fuzztest/handlers/periodic.ts +89 -0
- package/fuzztest/process.ts +100 -0
- package/fuzztest/shared/chaos.ts +29 -0
- package/fuzztest/shared/stream.ts +40 -0
- package/package.json +8 -7
- package/plans/redis-options.md +279 -0
- package/src/client.ts +246 -0
- package/src/constants.ts +33 -0
- package/src/errors.ts +13 -0
- package/src/index.ts +2 -0
- package/src/manager.ts +78 -0
- package/src/pool.ts +129 -0
- package/src/queasy.lua +2 -3
- package/src/queue.ts +108 -0
- package/src/types.ts +16 -0
- package/src/{utils.js → utils.ts} +3 -20
- package/src/{worker.js → worker.ts} +5 -12
- package/test/{client.test.js → client.test.ts} +6 -7
- package/test/{errors.test.js → errors.test.ts} +1 -1
- package/test/fixtures/always-fail-handler.ts +5 -0
- package/test/fixtures/data-logger-handler.ts +11 -0
- package/test/fixtures/failure-handler.ts +6 -0
- package/test/fixtures/permanent-error-handler.ts +6 -0
- package/test/fixtures/slow-handler.ts +6 -0
- package/test/fixtures/success-handler.js +0 -5
- package/test/fixtures/success-handler.ts +6 -0
- package/test/fixtures/with-failure-handler.ts +5 -0
- package/test/{guards.test.js → guards.test.ts} +21 -34
- package/test/{manager.test.js → manager.test.ts} +26 -34
- package/test/{pool.test.js → pool.test.ts} +14 -16
- package/test/{queue.test.js → queue.test.ts} +21 -21
- package/test/{redis-functions.test.js → redis-functions.test.ts} +14 -20
- package/test/{utils.test.js → utils.test.ts} +1 -1
- package/tsconfig.json +20 -0
- package/jsconfig.json +0 -17
- package/src/client.js +0 -258
- package/src/index.js +0 -2
- package/src/manager.js +0 -94
- package/src/queue.js +0 -154
- package/test/fixtures/always-fail-handler.js +0 -8
- package/test/fixtures/data-logger-handler.js +0 -19
- package/test/fixtures/failure-handler.js +0 -9
- package/test/fixtures/permanent-error-handler.js +0 -10
- package/test/fixtures/slow-handler.js +0 -9
- package/test/fixtures/with-failure-handler.js +0 -8
- /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.
|
|
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
|
-
|
|
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 } =
|
|
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 {
|
|
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
|
|
9
|
-
|
|
10
|
-
let
|
|
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(
|
|
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.
|
|
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,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
|
+
}
|
|
@@ -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 {
|
|
4
|
+
import type { RedisClientType } from 'redis';
|
|
5
|
+
import { Client } from '../src/index.ts';
|
|
5
6
|
|
|
6
7
|
describe('Client disconnect guard', () => {
|
|
7
|
-
|
|
8
|
-
let redis;
|
|
9
|
-
/** @type {import('../src/client.js').Client} */
|
|
10
|
-
let client;
|
|
8
|
+
let client: Client;
|
|
11
9
|
|
|
12
|
-
beforeEach(
|
|
13
|
-
redis = createClient();
|
|
14
|
-
await redis.connect();
|
|
10
|
+
beforeEach(() => {
|
|
15
11
|
return new Promise((res) => {
|
|
16
|
-
client = new Client(
|
|
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
|
-
|
|
33
|
-
let redis;
|
|
34
|
-
/** @type {import('../src/client.js').Client} */
|
|
35
|
-
let client;
|
|
27
|
+
let client: Client;
|
|
36
28
|
|
|
37
|
-
beforeEach(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
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
|
|
62
|
+
await nonProcessingClient.close();
|
|
76
63
|
});
|
|
77
64
|
});
|
|
78
65
|
|
|
79
66
|
describe('Client bump lost lock', () => {
|
|
80
|
-
|
|
81
|
-
let
|
|
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
|
-
|
|
94
|
-
|
|
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.
|
|
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.
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
155
|
-
let
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
104
|
-
let
|
|
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
|
-
|
|
115
|
-
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
10
|
-
let
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
248
|
+
const mainHandler = new URL('./fixtures/with-failure-handler.ts', import.meta.url)
|
|
249
249
|
.pathname;
|
|
250
|
-
const failHandler = new URL('./fixtures/success-handler.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|