@tsed/bullmq 8.0.1 → 8.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- package/src/BullMQModule.spec.ts +477 -0
- package/src/BullMQModule.ts +151 -0
- package/src/config/config.ts +59 -0
- package/src/constants/BullMQTypes.ts +7 -0
- package/src/constants/constants.ts +1 -0
- package/src/contracts/JobMethods.ts +7 -0
- package/src/contracts/JobStore.ts +7 -0
- package/src/contracts/index.ts +5 -0
- package/src/decorators/InjectQueue.ts +7 -0
- package/src/decorators/InjectWorker.ts +7 -0
- package/src/decorators/JobController.spec.ts +94 -0
- package/src/decorators/JobController.ts +33 -0
- package/src/decorators/index.ts +6 -0
- package/src/dispatchers/JobDispatcher.spec.ts +169 -0
- package/src/dispatchers/JobDispatcher.ts +78 -0
- package/src/dispatchers/JobDispatcherOptions.ts +4 -0
- package/src/dispatchers/index.ts +5 -0
- package/src/index.ts +24 -0
- package/src/utils/createQueueProvider.ts +16 -0
- package/src/utils/createWorkerProvider.ts +18 -0
- package/src/utils/getJobToken.ts +7 -0
- package/src/utils/getQueueToken.ts +3 -0
- package/src/utils/getWorkerToken.ts +3 -0
- package/src/utils/mapQueueOptions.ts +14 -0
- package/src/utils/mapWorkerOptions.ts +14 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@tsed/bullmq",
|
|
3
3
|
"description": "BullMQ integration for Ts.ED",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.0.
|
|
5
|
+
"version": "8.0.3",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"Ts.ED",
|
|
8
8
|
"BullMQ"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"typings": "./lib/types/index.d.ts",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
|
+
"@tsed/source": "./src/index.ts",
|
|
16
17
|
"types": "./lib/types/index.d.ts",
|
|
17
18
|
"import": "./lib/esm/index.js",
|
|
18
19
|
"default": "./lib/esm/index.js"
|
|
@@ -29,12 +30,12 @@
|
|
|
29
30
|
"tslib": "2.7.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
|
-
"@tsed/barrels": "8.0.
|
|
33
|
-
"@tsed/core": "8.0.
|
|
34
|
-
"@tsed/di": "8.0.
|
|
35
|
-
"@tsed/platform-http": "8.0.
|
|
36
|
-
"@tsed/schema": "8.0.
|
|
37
|
-
"@tsed/typescript": "8.0.
|
|
33
|
+
"@tsed/barrels": "8.0.3",
|
|
34
|
+
"@tsed/core": "8.0.3",
|
|
35
|
+
"@tsed/di": "8.0.3",
|
|
36
|
+
"@tsed/platform-http": "8.0.3",
|
|
37
|
+
"@tsed/schema": "8.0.3",
|
|
38
|
+
"@tsed/typescript": "8.0.3",
|
|
38
39
|
"bullmq": "^4.12.3 || ^5.1.1",
|
|
39
40
|
"eslint": "9.12.0",
|
|
40
41
|
"ts-mockito": "^2.6.1",
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import "./BullMQModule.js";
|
|
2
|
+
|
|
3
|
+
import {catchAsyncError} from "@tsed/core";
|
|
4
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
5
|
+
import {Queue, Worker} from "bullmq";
|
|
6
|
+
import {anything, instance, mock, verify, when} from "ts-mockito";
|
|
7
|
+
import {beforeEach} from "vitest";
|
|
8
|
+
|
|
9
|
+
import {BullMQModule} from "./BullMQModule.js";
|
|
10
|
+
import {type BullMQConfig} from "./config/config.js";
|
|
11
|
+
import {JobMethods} from "./contracts/index.js";
|
|
12
|
+
import {FallbackJobController, JobController} from "./decorators/index.js";
|
|
13
|
+
import {InjectQueue} from "./decorators/InjectQueue.js";
|
|
14
|
+
import {InjectWorker} from "./decorators/InjectWorker.js";
|
|
15
|
+
import {JobDispatcher} from "./dispatchers/index.js";
|
|
16
|
+
|
|
17
|
+
const queueConstructorSpy = vi.fn();
|
|
18
|
+
const workerConstructorSpy = vi.fn();
|
|
19
|
+
|
|
20
|
+
vi.mock("bullmq", () => {
|
|
21
|
+
return {
|
|
22
|
+
Queue: class {
|
|
23
|
+
constructor(...args: any[]) {
|
|
24
|
+
queueConstructorSpy(...args);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
close() {}
|
|
28
|
+
},
|
|
29
|
+
Worker: class {
|
|
30
|
+
constructor(...args: any[]) {
|
|
31
|
+
workerConstructorSpy(...args);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
close() {}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
@JobController("cron", "default", {
|
|
40
|
+
repeat: {
|
|
41
|
+
pattern: "* * * * *"
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
class CustomCronJob implements JobMethods {
|
|
45
|
+
@InjectQueue("default")
|
|
46
|
+
queue: Queue;
|
|
47
|
+
|
|
48
|
+
@InjectWorker("default")
|
|
49
|
+
worker: Worker;
|
|
50
|
+
|
|
51
|
+
handle() {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@JobController("regular", "default")
|
|
55
|
+
class RegularJob {
|
|
56
|
+
handle() {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("BullMQModule", () => {
|
|
60
|
+
let dispatcher: JobDispatcher;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
dispatcher = mock(JobDispatcher);
|
|
64
|
+
when(dispatcher.dispatch(CustomCronJob)).thenResolve();
|
|
65
|
+
});
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
queueConstructorSpy.mockClear();
|
|
68
|
+
workerConstructorSpy.mockClear();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(PlatformTest.reset);
|
|
72
|
+
|
|
73
|
+
describe("configuration", () => {
|
|
74
|
+
describe("merges config correctly", () => {
|
|
75
|
+
beforeEach(() =>
|
|
76
|
+
PlatformTest.create({
|
|
77
|
+
bullmq: {
|
|
78
|
+
queues: ["default", "special"],
|
|
79
|
+
connection: {
|
|
80
|
+
connectionName: "defaultConnectionName"
|
|
81
|
+
},
|
|
82
|
+
defaultQueueOptions: {
|
|
83
|
+
defaultJobOptions: {
|
|
84
|
+
delay: 100
|
|
85
|
+
},
|
|
86
|
+
blockingConnection: true
|
|
87
|
+
},
|
|
88
|
+
queueOptions: {
|
|
89
|
+
special: {
|
|
90
|
+
connection: {
|
|
91
|
+
connectionName: "specialConnectionName"
|
|
92
|
+
},
|
|
93
|
+
defaultJobOptions: {
|
|
94
|
+
attempts: 9
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
defaultWorkerOptions: {
|
|
99
|
+
connection: {
|
|
100
|
+
connectTimeout: 123
|
|
101
|
+
},
|
|
102
|
+
concurrency: 50
|
|
103
|
+
},
|
|
104
|
+
workerOptions: {
|
|
105
|
+
special: {
|
|
106
|
+
concurrency: 1,
|
|
107
|
+
lockDuration: 2
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
imports: [
|
|
112
|
+
{
|
|
113
|
+
token: JobDispatcher,
|
|
114
|
+
use: instance(dispatcher)
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
it("queue", () => {
|
|
121
|
+
expect(queueConstructorSpy).toHaveBeenCalledTimes(2);
|
|
122
|
+
|
|
123
|
+
expect(queueConstructorSpy).toHaveBeenNthCalledWith(1, "default", {
|
|
124
|
+
connection: {
|
|
125
|
+
connectionName: "defaultConnectionName"
|
|
126
|
+
},
|
|
127
|
+
defaultJobOptions: {
|
|
128
|
+
delay: 100
|
|
129
|
+
},
|
|
130
|
+
blockingConnection: true
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(queueConstructorSpy).toHaveBeenNthCalledWith(2, "special", {
|
|
134
|
+
connection: {
|
|
135
|
+
connectionName: "specialConnectionName"
|
|
136
|
+
},
|
|
137
|
+
defaultJobOptions: {
|
|
138
|
+
attempts: 9,
|
|
139
|
+
delay: 100
|
|
140
|
+
},
|
|
141
|
+
blockingConnection: true
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("worker", () => {
|
|
146
|
+
expect(workerConstructorSpy).toHaveBeenCalledTimes(2);
|
|
147
|
+
|
|
148
|
+
expect(workerConstructorSpy).toHaveBeenNthCalledWith(1, "default", expect.any(Function), {
|
|
149
|
+
connection: {
|
|
150
|
+
connectTimeout: 123
|
|
151
|
+
},
|
|
152
|
+
concurrency: 50
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(workerConstructorSpy).toHaveBeenNthCalledWith(2, "special", expect.any(Function), {
|
|
156
|
+
connection: {
|
|
157
|
+
connectTimeout: 123
|
|
158
|
+
},
|
|
159
|
+
concurrency: 1,
|
|
160
|
+
lockDuration: 2
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe("discover queues from decorators", () => {
|
|
165
|
+
beforeEach(() =>
|
|
166
|
+
PlatformTest.create({
|
|
167
|
+
bullmq: {
|
|
168
|
+
queues: ["special"],
|
|
169
|
+
connection: {
|
|
170
|
+
connectionName: "defaultConnectionName"
|
|
171
|
+
},
|
|
172
|
+
defaultQueueOptions: {
|
|
173
|
+
defaultJobOptions: {
|
|
174
|
+
delay: 100
|
|
175
|
+
},
|
|
176
|
+
blockingConnection: true
|
|
177
|
+
},
|
|
178
|
+
queueOptions: {
|
|
179
|
+
special: {
|
|
180
|
+
connection: {
|
|
181
|
+
connectionName: "specialConnectionName"
|
|
182
|
+
},
|
|
183
|
+
defaultJobOptions: {
|
|
184
|
+
attempts: 9
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
defaultWorkerOptions: {
|
|
189
|
+
connection: {
|
|
190
|
+
connectTimeout: 123
|
|
191
|
+
},
|
|
192
|
+
concurrency: 50
|
|
193
|
+
},
|
|
194
|
+
workerOptions: {
|
|
195
|
+
special: {
|
|
196
|
+
concurrency: 1,
|
|
197
|
+
lockDuration: 2
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
imports: [
|
|
202
|
+
{
|
|
203
|
+
token: JobDispatcher,
|
|
204
|
+
use: instance(dispatcher)
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
it("queue", () => {
|
|
211
|
+
expect(queueConstructorSpy).toHaveBeenCalledTimes(2);
|
|
212
|
+
|
|
213
|
+
expect(queueConstructorSpy).toHaveBeenNthCalledWith(1, "default", {
|
|
214
|
+
connection: {
|
|
215
|
+
connectionName: "defaultConnectionName"
|
|
216
|
+
},
|
|
217
|
+
defaultJobOptions: {
|
|
218
|
+
delay: 100
|
|
219
|
+
},
|
|
220
|
+
blockingConnection: true
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(queueConstructorSpy).toHaveBeenNthCalledWith(2, "special", {
|
|
224
|
+
connection: {
|
|
225
|
+
connectionName: "specialConnectionName"
|
|
226
|
+
},
|
|
227
|
+
defaultJobOptions: {
|
|
228
|
+
attempts: 9,
|
|
229
|
+
delay: 100
|
|
230
|
+
},
|
|
231
|
+
blockingConnection: true
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("worker", () => {
|
|
236
|
+
expect(workerConstructorSpy).toHaveBeenCalledTimes(2);
|
|
237
|
+
|
|
238
|
+
expect(workerConstructorSpy).toHaveBeenNthCalledWith(1, "default", expect.any(Function), {
|
|
239
|
+
connection: {
|
|
240
|
+
connectTimeout: 123
|
|
241
|
+
},
|
|
242
|
+
concurrency: 50
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(workerConstructorSpy).toHaveBeenNthCalledWith(2, "special", expect.any(Function), {
|
|
246
|
+
connection: {
|
|
247
|
+
connectTimeout: 123
|
|
248
|
+
},
|
|
249
|
+
concurrency: 1,
|
|
250
|
+
lockDuration: 2
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe("disableWorker", () => {
|
|
255
|
+
const config = {
|
|
256
|
+
queues: ["default", "foo", "bar"],
|
|
257
|
+
connection: {},
|
|
258
|
+
disableWorker: true
|
|
259
|
+
} as BullMQConfig;
|
|
260
|
+
|
|
261
|
+
beforeEach(() =>
|
|
262
|
+
PlatformTest.create({
|
|
263
|
+
bullmq: config,
|
|
264
|
+
imports: [
|
|
265
|
+
{
|
|
266
|
+
token: JobDispatcher,
|
|
267
|
+
use: instance(dispatcher)
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
it("should not create any workers", () => {
|
|
274
|
+
expect(workerConstructorSpy).toHaveBeenCalledTimes(0);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
describe("without", () => {
|
|
278
|
+
beforeEach(() =>
|
|
279
|
+
PlatformTest.create({
|
|
280
|
+
imports: [
|
|
281
|
+
{
|
|
282
|
+
token: JobDispatcher,
|
|
283
|
+
use: instance(dispatcher)
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
it("skips initialization", async () => {
|
|
289
|
+
expect(queueConstructorSpy).not.toHaveBeenCalled();
|
|
290
|
+
verify(dispatcher.dispatch(anything())).never();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("functionality", () => {
|
|
296
|
+
const config = {
|
|
297
|
+
queues: ["default", "foo", "bar"],
|
|
298
|
+
connection: {},
|
|
299
|
+
workerQueues: ["default", "foo"]
|
|
300
|
+
} as BullMQConfig;
|
|
301
|
+
|
|
302
|
+
beforeEach(() =>
|
|
303
|
+
PlatformTest.create({
|
|
304
|
+
bullmq: config,
|
|
305
|
+
imports: [
|
|
306
|
+
{
|
|
307
|
+
token: JobDispatcher,
|
|
308
|
+
use: instance(dispatcher)
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
})
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
describe("cronjobs", () => {
|
|
315
|
+
it("should dispatch cron jobs automatically", () => {
|
|
316
|
+
verify(dispatcher.dispatch(CustomCronJob)).once();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("queues", () => {
|
|
321
|
+
it("should get default", () => {
|
|
322
|
+
const instance = PlatformTest.get<Queue>("bullmq.queue.default");
|
|
323
|
+
|
|
324
|
+
expect(instance).toBeInstanceOf(Queue);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it.each(config.queues!)("should register queue(%s)", (queue) => {
|
|
328
|
+
const instance = PlatformTest.get<Queue>(`bullmq.queue.${queue}`);
|
|
329
|
+
|
|
330
|
+
expect(instance).toBeInstanceOf(Queue);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("workers", () => {
|
|
335
|
+
it("should get default", () => {
|
|
336
|
+
const instance = PlatformTest.get<Worker>("bullmq.worker.default");
|
|
337
|
+
|
|
338
|
+
expect(instance).toBeInstanceOf(Worker);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it.each(config.workerQueues!)("should register worker(%s)", (queue) => {
|
|
342
|
+
const instance = PlatformTest.get<Worker>(`bullmq.worker.${queue}`);
|
|
343
|
+
|
|
344
|
+
expect(instance).toBeInstanceOf(Worker);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("should not register unspecified worker queue", () => {
|
|
348
|
+
expect(PlatformTest.get("bullmq.worker.bar")).toBeUndefined();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("should run worker and execute processor", async () => {
|
|
352
|
+
const bullMQModule = PlatformTest.get<BullMQModule>(BullMQModule);
|
|
353
|
+
const worker = PlatformTest.get<JobMethods>("bullmq.job.default.regular");
|
|
354
|
+
const job = {
|
|
355
|
+
name: "regular",
|
|
356
|
+
queueName: "default",
|
|
357
|
+
data: {test: "test"}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
vi.spyOn(worker, "handle").mockResolvedValueOnce(undefined as never);
|
|
361
|
+
|
|
362
|
+
await (bullMQModule as any).onProcess(job);
|
|
363
|
+
|
|
364
|
+
expect(worker.handle).toHaveBeenCalledWith({test: "test"}, job);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("should log warning when the worker doesn't exists", async () => {
|
|
368
|
+
const bullMQModule = PlatformTest.get<BullMQModule>(BullMQModule);
|
|
369
|
+
|
|
370
|
+
vi.spyOn(PlatformTest.injector.logger, "warn");
|
|
371
|
+
|
|
372
|
+
const job = {
|
|
373
|
+
name: "regular",
|
|
374
|
+
queueName: "toto",
|
|
375
|
+
data: {test: "test"}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
await (bullMQModule as any).onProcess(job);
|
|
379
|
+
|
|
380
|
+
expect(PlatformTest.injector.logger.warn).toHaveBeenCalledWith({
|
|
381
|
+
event: "BULLMQ_JOB_NOT_FOUND",
|
|
382
|
+
message: "Job regular toto not found"
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("should run worker, execute processor and handle error", async () => {
|
|
387
|
+
const bullMQModule = PlatformTest.get<BullMQModule>(BullMQModule);
|
|
388
|
+
const worker = PlatformTest.get<JobMethods>("bullmq.job.default.regular");
|
|
389
|
+
const job = {
|
|
390
|
+
name: "regular",
|
|
391
|
+
queueName: "default",
|
|
392
|
+
data: {test: "test"},
|
|
393
|
+
attemptsMade: 1
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
vi.spyOn(PlatformTest.injector.logger, "error");
|
|
397
|
+
|
|
398
|
+
vi.spyOn(worker, "handle").mockRejectedValue(new Error("error") as never);
|
|
399
|
+
|
|
400
|
+
const error = await catchAsyncError(() => (bullMQModule as any).onProcess(job));
|
|
401
|
+
|
|
402
|
+
expect(worker.handle).toHaveBeenCalledWith({test: "test"}, job);
|
|
403
|
+
expect(PlatformTest.injector.logger.error).toHaveBeenCalledWith({
|
|
404
|
+
attempt: 1,
|
|
405
|
+
name: "regular",
|
|
406
|
+
queue: "default",
|
|
407
|
+
logType: "bullmq",
|
|
408
|
+
duration: expect.any(Number),
|
|
409
|
+
event: "BULLMQ_JOB_ERROR",
|
|
410
|
+
message: "error",
|
|
411
|
+
reqId: expect.any(String),
|
|
412
|
+
stack: expect.any(String),
|
|
413
|
+
time: expect.any(Object)
|
|
414
|
+
});
|
|
415
|
+
expect(error?.message).toEqual("error");
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
describe("with fallback controller", () => {
|
|
420
|
+
beforeEach(async () => {
|
|
421
|
+
@FallbackJobController("foo")
|
|
422
|
+
class FooFallbackController {
|
|
423
|
+
handle() {}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
@FallbackJobController()
|
|
427
|
+
class FallbackController {
|
|
428
|
+
handle() {}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
await PlatformTest.create({
|
|
432
|
+
bullmq: {
|
|
433
|
+
queues: ["default", "foo"],
|
|
434
|
+
connection: {}
|
|
435
|
+
},
|
|
436
|
+
imports: [
|
|
437
|
+
{
|
|
438
|
+
token: JobDispatcher,
|
|
439
|
+
use: instance(dispatcher)
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("should run queue specific fallback job controller", async () => {
|
|
446
|
+
const bullMQModule = PlatformTest.get<BullMQModule>(BullMQModule);
|
|
447
|
+
const worker = PlatformTest.get<JobMethods>("bullmq.fallback-job.foo");
|
|
448
|
+
const job = {
|
|
449
|
+
name: "unknown-name",
|
|
450
|
+
queueName: "foo",
|
|
451
|
+
data: {test: "test"}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
vi.spyOn(worker, "handle").mockResolvedValueOnce(undefined as never);
|
|
455
|
+
|
|
456
|
+
await (bullMQModule as any).onProcess(job);
|
|
457
|
+
|
|
458
|
+
expect(worker.handle).toHaveBeenCalledWith({test: "test"}, job);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should run overall fallback job controller", async () => {
|
|
462
|
+
const bullMQModule = PlatformTest.get<BullMQModule>(BullMQModule);
|
|
463
|
+
const worker = PlatformTest.get<JobMethods>("bullmq.fallback-job");
|
|
464
|
+
const job = {
|
|
465
|
+
name: "unknown-name",
|
|
466
|
+
queueName: "default",
|
|
467
|
+
data: {test: "123"}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
vi.spyOn(worker, "handle").mockResolvedValueOnce(undefined as never);
|
|
471
|
+
|
|
472
|
+
await (bullMQModule as any).onProcess(job);
|
|
473
|
+
|
|
474
|
+
expect(worker.handle).toHaveBeenCalledWith({test: "123"}, job);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constant,
|
|
3
|
+
DIContext,
|
|
4
|
+
inject,
|
|
5
|
+
injectable,
|
|
6
|
+
injectMany,
|
|
7
|
+
injector,
|
|
8
|
+
logger,
|
|
9
|
+
OnDestroy,
|
|
10
|
+
type OnInit,
|
|
11
|
+
ProviderType,
|
|
12
|
+
runInContext
|
|
13
|
+
} from "@tsed/di";
|
|
14
|
+
import {getComputedType} from "@tsed/schema";
|
|
15
|
+
import {Job, Queue, Worker} from "bullmq";
|
|
16
|
+
import {v4} from "uuid";
|
|
17
|
+
|
|
18
|
+
import {BullMQConfig} from "./config/config.js";
|
|
19
|
+
import {BullMQTypes} from "./constants/BullMQTypes.js";
|
|
20
|
+
import {BULLMQ} from "./constants/constants.js";
|
|
21
|
+
import {JobMethods, JobStore} from "./contracts/index.js";
|
|
22
|
+
import {JobDispatcher} from "./dispatchers/index.js";
|
|
23
|
+
import {createQueueProvider} from "./utils/createQueueProvider.js";
|
|
24
|
+
import {createWorkerProvider} from "./utils/createWorkerProvider.js";
|
|
25
|
+
import {getFallbackJobToken, getJobToken} from "./utils/getJobToken.js";
|
|
26
|
+
import {mapQueueOptions} from "./utils/mapQueueOptions.js";
|
|
27
|
+
import {mapWorkerOptions} from "./utils/mapWorkerOptions.js";
|
|
28
|
+
|
|
29
|
+
export class BullMQModule implements OnInit, OnDestroy {
|
|
30
|
+
private readonly dispatcher = inject(JobDispatcher);
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
// build providers allow @Inject(queue) usage in JobController instance
|
|
34
|
+
if (this.isEnabled()) {
|
|
35
|
+
const queues = [...this.getUniqQueueNames()];
|
|
36
|
+
|
|
37
|
+
this.buildQueues(queues);
|
|
38
|
+
|
|
39
|
+
if (!this.isWorkerEnabled()) {
|
|
40
|
+
const workers = this.config.workerQueues?.length ? this.config.workerQueues : queues;
|
|
41
|
+
|
|
42
|
+
this.buildWorkers(workers);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get config() {
|
|
48
|
+
return constant<BullMQConfig>("bullmq")!;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
$onInit() {
|
|
52
|
+
if (this.isEnabled()) {
|
|
53
|
+
injectMany<JobMethods>(BullMQTypes.CRON).map((job) => this.dispatcher.dispatch(getComputedType(job)));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async $onDestroy() {
|
|
58
|
+
if (!this.isEnabled()) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await Promise.all(injectMany<Queue>(BullMQTypes.QUEUE).map((queue) => queue.close()));
|
|
63
|
+
await Promise.all(injectMany<Worker>(BullMQTypes.WORKER).map((worker) => worker.close()));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
isEnabled() {
|
|
67
|
+
return !!this.config;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isWorkerEnabled() {
|
|
71
|
+
return this.config.disableWorker;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private buildQueues(queues: string[]) {
|
|
75
|
+
queues.forEach((queue) => {
|
|
76
|
+
const opts = mapQueueOptions(queue, this.config);
|
|
77
|
+
createQueueProvider(queue, opts);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private buildWorkers(workers: string[]) {
|
|
82
|
+
workers.forEach((worker) => {
|
|
83
|
+
const opts = mapWorkerOptions(worker, this.config);
|
|
84
|
+
createWorkerProvider(worker, this.onProcess, opts);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Auto discover queue names from provider and merge it with queue names from global configuration.
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
private getUniqQueueNames() {
|
|
93
|
+
return new Set(
|
|
94
|
+
injector()
|
|
95
|
+
.getProviders([BullMQTypes.JOB, BullMQTypes.CRON, BullMQTypes.FALLBACK_JOB])
|
|
96
|
+
.map((provider) => provider.store.get<JobStore>(BULLMQ)?.queue)
|
|
97
|
+
.concat(this.config.queues!)
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private getJob(name: string, queueName: string) {
|
|
103
|
+
return inject<JobMethods>(getJobToken(queueName, name)) || inject(getFallbackJobToken(queueName)) || inject(getFallbackJobToken());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private onProcess = async (job: Job) => {
|
|
107
|
+
const jobService = this.getJob(job.name, job.queueName);
|
|
108
|
+
|
|
109
|
+
if (!jobService) {
|
|
110
|
+
logger().warn({
|
|
111
|
+
event: "BULLMQ_JOB_NOT_FOUND",
|
|
112
|
+
message: `Job ${job.name} ${job.queueName} not found`
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const $ctx = new DIContext({
|
|
118
|
+
id: job.id || v4().split("-").join(""),
|
|
119
|
+
additionalProps: {
|
|
120
|
+
logType: "bullmq",
|
|
121
|
+
name: job.name,
|
|
122
|
+
queue: job.queueName,
|
|
123
|
+
attempt: job.attemptsMade
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
$ctx.set("BULLMQ_JOB", job);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
return await runInContext($ctx, () => {
|
|
131
|
+
$ctx.logger.info("Processing job");
|
|
132
|
+
try {
|
|
133
|
+
return jobService.handle(job.data, job);
|
|
134
|
+
} finally {
|
|
135
|
+
$ctx.logger.info("Finished processing job");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
} catch (er) {
|
|
139
|
+
$ctx.logger.error({
|
|
140
|
+
event: "BULLMQ_JOB_ERROR",
|
|
141
|
+
message: er.message,
|
|
142
|
+
stack: er.stack
|
|
143
|
+
});
|
|
144
|
+
throw er;
|
|
145
|
+
} finally {
|
|
146
|
+
await $ctx.destroy();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
injectable(BullMQModule).type(ProviderType.MODULE);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {ConnectionOptions, DefaultJobOptions, QueueOptions, WorkerOptions} from "bullmq";
|
|
2
|
+
|
|
3
|
+
export type BullMQConfig = {
|
|
4
|
+
/**
|
|
5
|
+
* Specify queue name's to create
|
|
6
|
+
*/
|
|
7
|
+
queues?: string[];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default connection to use for queue's and worker's
|
|
11
|
+
*/
|
|
12
|
+
connection: ConnectionOptions;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default queue options which are applied to every queue
|
|
16
|
+
*
|
|
17
|
+
* Can be extended/overridden by `queueOptions`
|
|
18
|
+
*/
|
|
19
|
+
defaultQueueOptions?: Partial<QueueOptions>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Specify additional queue options by queue name
|
|
23
|
+
*/
|
|
24
|
+
queueOptions?: Record<string, Partial<QueueOptions>>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Disable the creation of any worker.
|
|
28
|
+
*
|
|
29
|
+
* All other worker configuration will be ignored
|
|
30
|
+
*/
|
|
31
|
+
disableWorker?: boolean;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Specify for which queues to start a worker for.
|
|
35
|
+
*
|
|
36
|
+
* Defaultly for every queue added in the `queues` parameter
|
|
37
|
+
*/
|
|
38
|
+
workerQueues?: string[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default worker options which are applied to every worker
|
|
42
|
+
*
|
|
43
|
+
* Can be extended/overridden by `workerOptions`
|
|
44
|
+
*/
|
|
45
|
+
defaultWorkerOptions?: Partial<WorkerOptions>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Specify additional worker options by queue name
|
|
49
|
+
*/
|
|
50
|
+
workerOptions?: Record<string, Partial<WorkerOptions>>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
declare global {
|
|
54
|
+
namespace TsED {
|
|
55
|
+
interface Configuration {
|
|
56
|
+
bullmq?: BullMQConfig;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const BULLMQ = "bullmq";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {Store} from "@tsed/core";
|
|
2
|
+
import {GlobalProviders} from "@tsed/di";
|
|
3
|
+
|
|
4
|
+
import {FallbackJobController, JobController} from "./JobController.js";
|
|
5
|
+
|
|
6
|
+
describe("JobController", () => {
|
|
7
|
+
afterEach(() => GlobalProviders.clear());
|
|
8
|
+
|
|
9
|
+
it("should set the proper store data", () => {
|
|
10
|
+
@JobController("testjob")
|
|
11
|
+
class TestJob {}
|
|
12
|
+
|
|
13
|
+
const store = Store.from(TestJob).get("bullmq");
|
|
14
|
+
|
|
15
|
+
expect(store.name).toBe("testjob");
|
|
16
|
+
expect(store.queue).toBe("default");
|
|
17
|
+
expect(store.opts).toStrictEqual({});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should allow settings a custom queue", () => {
|
|
21
|
+
@JobController("testjob", "custom")
|
|
22
|
+
class CustomQueueJob {}
|
|
23
|
+
|
|
24
|
+
const store = Store.from(CustomQueueJob).get("bullmq");
|
|
25
|
+
|
|
26
|
+
expect(store.queue).toBe("custom");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should allow custom options to be set", () => {
|
|
30
|
+
@JobController("testjob", "default", {
|
|
31
|
+
backoff: 69,
|
|
32
|
+
attempts: 42
|
|
33
|
+
})
|
|
34
|
+
class CustomOptionsJob {}
|
|
35
|
+
|
|
36
|
+
const store = Store.from(CustomOptionsJob).get("bullmq");
|
|
37
|
+
|
|
38
|
+
expect(store.opts).toStrictEqual({
|
|
39
|
+
backoff: 69,
|
|
40
|
+
attempts: 42
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should set type for cron jobs", () => {
|
|
45
|
+
@JobController("testjob", "default", {
|
|
46
|
+
repeat: {count: 42}
|
|
47
|
+
})
|
|
48
|
+
class CustomCronJob {}
|
|
49
|
+
|
|
50
|
+
const store = Store.from(CustomCronJob).get("bullmq");
|
|
51
|
+
|
|
52
|
+
expect(store).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("FallbackJobController", () => {
|
|
57
|
+
afterEach(() => GlobalProviders.clear());
|
|
58
|
+
|
|
59
|
+
describe("with queue", () => {
|
|
60
|
+
it("should set the proper store data", () => {
|
|
61
|
+
@FallbackJobController("default")
|
|
62
|
+
class FallbackJob {}
|
|
63
|
+
|
|
64
|
+
const store = Store.from(FallbackJob).get("bullmq");
|
|
65
|
+
expect(store.queue).toBe("default");
|
|
66
|
+
|
|
67
|
+
const fallbackControllerForQueue = GlobalProviders.get("bullmq.fallback-job.default");
|
|
68
|
+
expect(fallbackControllerForQueue).not.toBeUndefined();
|
|
69
|
+
expect(fallbackControllerForQueue?.type).toEqual("bullmq:fallback-job");
|
|
70
|
+
expect(fallbackControllerForQueue?.useClass).toEqual(FallbackJob);
|
|
71
|
+
|
|
72
|
+
const fallbackController = GlobalProviders.get("bullmq.fallback-job");
|
|
73
|
+
expect(fallbackController).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("without queue", () => {
|
|
78
|
+
it("should set the proper store data", () => {
|
|
79
|
+
@FallbackJobController()
|
|
80
|
+
class FallbackJob {}
|
|
81
|
+
|
|
82
|
+
const store = Store.from(FallbackJob).get("bullmq");
|
|
83
|
+
expect(store.queue).toBeUndefined();
|
|
84
|
+
|
|
85
|
+
const fallbackController = GlobalProviders.get("bullmq.fallback-job");
|
|
86
|
+
expect(fallbackController).not.toBeUndefined();
|
|
87
|
+
expect(fallbackController?.type).toEqual("bullmq:fallback-job");
|
|
88
|
+
expect(fallbackController?.useClass).toEqual(FallbackJob);
|
|
89
|
+
|
|
90
|
+
const fallbackControllerForQueue = GlobalProviders.get("bullmq.fallback-job.default");
|
|
91
|
+
expect(fallbackControllerForQueue).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {StoreMerge, useDecorators} from "@tsed/core";
|
|
2
|
+
import {Injectable} from "@tsed/di";
|
|
3
|
+
import {JobsOptions} from "bullmq";
|
|
4
|
+
|
|
5
|
+
import {BullMQTypes} from "../constants/BullMQTypes.js";
|
|
6
|
+
import {BULLMQ} from "../constants/constants.js";
|
|
7
|
+
import {getFallbackJobToken, getJobToken} from "../utils/getJobToken.js";
|
|
8
|
+
|
|
9
|
+
export function JobController(name: string, queue: string = "default", opts: JobsOptions = {}) {
|
|
10
|
+
return useDecorators(
|
|
11
|
+
StoreMerge(BULLMQ, {
|
|
12
|
+
name,
|
|
13
|
+
queue,
|
|
14
|
+
opts
|
|
15
|
+
}),
|
|
16
|
+
Injectable({
|
|
17
|
+
token: getJobToken(queue, name),
|
|
18
|
+
type: opts.repeat ? BullMQTypes.CRON : BullMQTypes.JOB
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function FallbackJobController(queue?: string) {
|
|
24
|
+
return useDecorators(
|
|
25
|
+
StoreMerge(BULLMQ, {
|
|
26
|
+
queue
|
|
27
|
+
}),
|
|
28
|
+
Injectable({
|
|
29
|
+
token: getFallbackJobToken(queue),
|
|
30
|
+
type: BullMQTypes.FALLBACK_JOB
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {catchAsyncError} from "@tsed/core";
|
|
2
|
+
import {DITest, inject, injectable, injector} from "@tsed/di";
|
|
3
|
+
import {beforeEach} from "vitest";
|
|
4
|
+
|
|
5
|
+
import {JobMethods} from "../contracts/index.js";
|
|
6
|
+
import {JobController} from "../decorators/index.js";
|
|
7
|
+
import {JobDispatcher} from "./JobDispatcher.js";
|
|
8
|
+
|
|
9
|
+
@JobController("example-job-with-custom-id-from-job-methods")
|
|
10
|
+
class ExampleJobWithCustomJobIdFromJobMethods implements JobMethods {
|
|
11
|
+
handle(payload: string) {}
|
|
12
|
+
|
|
13
|
+
jobId(payload: string): string {
|
|
14
|
+
return payload.toUpperCase();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@JobController("example-job", "default", {
|
|
19
|
+
backoff: 69
|
|
20
|
+
})
|
|
21
|
+
class ExampleTestJob implements JobMethods {
|
|
22
|
+
handle(payload: {msg: string}) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@JobController("queue-not-configured", "not-configured")
|
|
26
|
+
class NotConfiguredQueueTestJob implements JobMethods {
|
|
27
|
+
handle() {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getFixture() {
|
|
31
|
+
const dispatcher = inject(JobDispatcher);
|
|
32
|
+
const queue = {
|
|
33
|
+
name: "default",
|
|
34
|
+
add: vi.fn()
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const specialQueue = {
|
|
38
|
+
name: "special",
|
|
39
|
+
add: vi.fn()
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
injectable("bullmq.queue.default").value(queue);
|
|
43
|
+
injectable("bullmq.queue.special").value(specialQueue);
|
|
44
|
+
injectable("bullmq.job.default.example-job").value(new ExampleTestJob());
|
|
45
|
+
injectable("bullmq.job.default.example-job-with-custom-id-from-job-methods").value(new ExampleJobWithCustomJobIdFromJobMethods());
|
|
46
|
+
|
|
47
|
+
vi.spyOn(injector(), "resolve");
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
dispatcher,
|
|
51
|
+
queue,
|
|
52
|
+
specialQueue,
|
|
53
|
+
job: inject<ExampleJobWithCustomJobIdFromJobMethods>("bullmq.job.default.example-job-with-custom-id-from-job-methods")
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("JobDispatcher", () => {
|
|
58
|
+
beforeEach(() => DITest.create());
|
|
59
|
+
afterEach(() => DITest.reset());
|
|
60
|
+
|
|
61
|
+
it("should throw an exception when a queue is not configured", async () => {
|
|
62
|
+
const {dispatcher} = getFixture();
|
|
63
|
+
|
|
64
|
+
const error = await catchAsyncError(() => dispatcher.dispatch(NotConfiguredQueueTestJob));
|
|
65
|
+
|
|
66
|
+
await expect(error).toEqual(new Error("Queue(not-configured) not defined"));
|
|
67
|
+
|
|
68
|
+
expect(injector().resolve).toHaveBeenCalledWith("bullmq.queue.not-configured", expect.anything());
|
|
69
|
+
});
|
|
70
|
+
it("should dispatch job as type", async () => {
|
|
71
|
+
const {dispatcher, queue} = getFixture();
|
|
72
|
+
|
|
73
|
+
await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"});
|
|
74
|
+
|
|
75
|
+
expect(queue.add).toHaveBeenCalledOnce();
|
|
76
|
+
expect(queue.add).toHaveBeenCalledWith(
|
|
77
|
+
"example-job",
|
|
78
|
+
expect.objectContaining({msg: "hello test"}),
|
|
79
|
+
expect.objectContaining({backoff: 69})
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
it("should dispatch job as options", async () => {
|
|
83
|
+
const {dispatcher, specialQueue} = getFixture();
|
|
84
|
+
|
|
85
|
+
await dispatcher.dispatch(
|
|
86
|
+
{
|
|
87
|
+
queue: "special",
|
|
88
|
+
name: "some-name"
|
|
89
|
+
},
|
|
90
|
+
{msg: "hello test"}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(specialQueue.add).toHaveBeenCalledOnce();
|
|
94
|
+
expect(specialQueue.add).toHaveBeenCalledWith("some-name", expect.objectContaining({msg: "hello test"}), expect.anything());
|
|
95
|
+
});
|
|
96
|
+
it("should dispatch job as string", async () => {
|
|
97
|
+
const {dispatcher, queue} = getFixture();
|
|
98
|
+
|
|
99
|
+
await dispatcher.dispatch("some-name", {msg: "hello test"});
|
|
100
|
+
|
|
101
|
+
expect(queue.add).toHaveBeenCalledOnce();
|
|
102
|
+
expect(queue.add).toHaveBeenCalledWith("some-name", expect.objectContaining({msg: "hello test"}), expect.anything());
|
|
103
|
+
});
|
|
104
|
+
it("should overwrite job options defined by the job", async () => {
|
|
105
|
+
const {dispatcher, queue} = getFixture();
|
|
106
|
+
|
|
107
|
+
await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"}, {backoff: 42, jobId: "ffeeaa"});
|
|
108
|
+
|
|
109
|
+
expect(queue.add).toHaveBeenCalledOnce();
|
|
110
|
+
expect(queue.add).toHaveBeenCalledWith(
|
|
111
|
+
"example-job",
|
|
112
|
+
expect.objectContaining({msg: "hello test"}),
|
|
113
|
+
expect.objectContaining({backoff: 42, jobId: "ffeeaa"})
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
it("should keep existing options and add new ones", async () => {
|
|
117
|
+
const {dispatcher, queue} = getFixture();
|
|
118
|
+
|
|
119
|
+
await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"}, {jobId: "ffeeaa"});
|
|
120
|
+
|
|
121
|
+
expect(queue.add).toHaveBeenCalledOnce();
|
|
122
|
+
expect(queue.add).toHaveBeenCalledWith(
|
|
123
|
+
"example-job",
|
|
124
|
+
expect.objectContaining({msg: "hello test"}),
|
|
125
|
+
expect.objectContaining({backoff: 69, jobId: "ffeeaa"})
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
describe("custom jobId", () => {
|
|
129
|
+
it("should allow setting the job id from within the job", async () => {
|
|
130
|
+
const {dispatcher, queue} = getFixture();
|
|
131
|
+
|
|
132
|
+
await dispatcher.dispatch(ExampleJobWithCustomJobIdFromJobMethods, "hello world");
|
|
133
|
+
|
|
134
|
+
expect(queue.add).toHaveBeenCalledOnce();
|
|
135
|
+
expect(queue.add).toHaveBeenCalledWith("example-job-with-custom-id-from-job-methods", "hello world", expect.anything());
|
|
136
|
+
|
|
137
|
+
const [, , opts] = queue.add.mock.calls.at(-1)!;
|
|
138
|
+
|
|
139
|
+
expect(opts).toMatchObject({
|
|
140
|
+
jobId: "HELLO WORLD"
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should pass the payload to the jobId method", async () => {
|
|
145
|
+
const {dispatcher, job} = getFixture();
|
|
146
|
+
|
|
147
|
+
vi.spyOn(job, "jobId");
|
|
148
|
+
|
|
149
|
+
await dispatcher.dispatch(ExampleJobWithCustomJobIdFromJobMethods, "hello world");
|
|
150
|
+
|
|
151
|
+
expect(job.jobId).toHaveBeenCalledOnce();
|
|
152
|
+
expect(job.jobId).toHaveBeenCalledWith("hello world");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should choose the jobId provided to the dispatcher even when the method is implemented", async () => {
|
|
156
|
+
const {dispatcher, queue} = getFixture();
|
|
157
|
+
|
|
158
|
+
await dispatcher.dispatch(ExampleJobWithCustomJobIdFromJobMethods, "hello world", {
|
|
159
|
+
jobId: "I don't think so"
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const [, , opts] = queue.add.mock.calls.at(-1)!;
|
|
163
|
+
|
|
164
|
+
expect(opts).toMatchObject({
|
|
165
|
+
jobId: "I don't think so"
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {isClass, Store, Type} from "@tsed/core";
|
|
2
|
+
import {inject, injectable} from "@tsed/di";
|
|
3
|
+
import {Job as BullMQJob, JobsOptions, Queue} from "bullmq";
|
|
4
|
+
|
|
5
|
+
import {BULLMQ} from "../constants/constants.js";
|
|
6
|
+
import {JobMethods, type JobStore} from "../contracts/index.js";
|
|
7
|
+
import {getJobToken} from "../utils/getJobToken.js";
|
|
8
|
+
import {getQueueToken} from "../utils/getQueueToken.js";
|
|
9
|
+
import type {JobDispatcherOptions} from "./JobDispatcherOptions.js";
|
|
10
|
+
|
|
11
|
+
export class JobDispatcher {
|
|
12
|
+
public async dispatch<T extends JobMethods>(
|
|
13
|
+
job: Type<T>,
|
|
14
|
+
payload?: Parameters<T["handle"]>[0],
|
|
15
|
+
options?: JobsOptions
|
|
16
|
+
): Promise<BullMQJob>;
|
|
17
|
+
public async dispatch<P = unknown>(job: JobDispatcherOptions, payload?: P, options?: JobsOptions): Promise<BullMQJob>;
|
|
18
|
+
public async dispatch<P = unknown>(job: string, payload?: P, options?: JobsOptions): Promise<BullMQJob>;
|
|
19
|
+
public async dispatch(job: Type | JobDispatcherOptions | string, payload: unknown, options: JobsOptions = {}): Promise<BullMQJob> {
|
|
20
|
+
const {queueName, jobName, defaultJobOptions} = await this.resolveDispatchArgs(job, payload);
|
|
21
|
+
|
|
22
|
+
const queue = inject<Queue>(getQueueToken(queueName));
|
|
23
|
+
|
|
24
|
+
if (!queue) {
|
|
25
|
+
throw new Error(`Queue(${queueName}) not defined`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return queue.add(jobName, payload, {
|
|
29
|
+
...defaultJobOptions,
|
|
30
|
+
...options
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async resolveDispatchArgs(job: Type | JobDispatcherOptions | string, payload: unknown) {
|
|
35
|
+
let queueName: string;
|
|
36
|
+
let jobName: string;
|
|
37
|
+
let defaultJobOptions: JobsOptions | undefined;
|
|
38
|
+
|
|
39
|
+
if (typeof job === "function") {
|
|
40
|
+
// job is passed as a Type
|
|
41
|
+
const store = Store.from(job).get<JobStore>(BULLMQ);
|
|
42
|
+
queueName = store.queue;
|
|
43
|
+
jobName = store.name;
|
|
44
|
+
|
|
45
|
+
defaultJobOptions = await this.retrieveJobOptionsFromClassBasedJob(store, payload);
|
|
46
|
+
} else if (typeof job === "object") {
|
|
47
|
+
// job is passed as JobDispatcherOptions
|
|
48
|
+
queueName = job.queue;
|
|
49
|
+
jobName = job.name;
|
|
50
|
+
} else {
|
|
51
|
+
// job is passed as a string
|
|
52
|
+
queueName = "default";
|
|
53
|
+
jobName = job;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
queueName,
|
|
58
|
+
jobName,
|
|
59
|
+
defaultJobOptions
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async retrieveJobOptionsFromClassBasedJob(store: JobStore, payload: unknown): Promise<JobsOptions> {
|
|
64
|
+
const job = inject<JobMethods>(getJobToken(store.queue, store.name));
|
|
65
|
+
const jobId = await job.jobId?.(payload);
|
|
66
|
+
|
|
67
|
+
if (jobId === undefined) {
|
|
68
|
+
return store.opts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...store.opts,
|
|
73
|
+
jobId
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
injectable(JobDispatcher);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Automatically generated by @tsed/barrels.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./BullMQModule.js";
|
|
5
|
+
export * from "./config/config.js";
|
|
6
|
+
export * from "./constants/BullMQTypes.js";
|
|
7
|
+
export * from "./constants/constants.js";
|
|
8
|
+
export * from "./contracts/index.js";
|
|
9
|
+
export * from "./contracts/JobMethods.js";
|
|
10
|
+
export * from "./contracts/JobStore.js";
|
|
11
|
+
export * from "./decorators/index.js";
|
|
12
|
+
export * from "./decorators/InjectQueue.js";
|
|
13
|
+
export * from "./decorators/InjectWorker.js";
|
|
14
|
+
export * from "./decorators/JobController.js";
|
|
15
|
+
export * from "./dispatchers/index.js";
|
|
16
|
+
export * from "./dispatchers/JobDispatcher.js";
|
|
17
|
+
export * from "./dispatchers/JobDispatcherOptions.js";
|
|
18
|
+
export * from "./utils/createQueueProvider.js";
|
|
19
|
+
export * from "./utils/createWorkerProvider.js";
|
|
20
|
+
export * from "./utils/getJobToken.js";
|
|
21
|
+
export * from "./utils/getQueueToken.js";
|
|
22
|
+
export * from "./utils/getWorkerToken.js";
|
|
23
|
+
export * from "./utils/mapQueueOptions.js";
|
|
24
|
+
export * from "./utils/mapWorkerOptions.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {inject, injectable} from "@tsed/di";
|
|
2
|
+
import {Queue, QueueOptions} from "bullmq";
|
|
3
|
+
|
|
4
|
+
import {getQueueToken} from "./getQueueToken.js";
|
|
5
|
+
|
|
6
|
+
export function createQueueProvider(queue: string, opts: QueueOptions) {
|
|
7
|
+
const token = getQueueToken(queue);
|
|
8
|
+
|
|
9
|
+
injectable(token)
|
|
10
|
+
.factory(() => new Queue(queue, opts))
|
|
11
|
+
.hooks({
|
|
12
|
+
$onDestroy: (queue: Queue) => queue.close()
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return inject<Queue>(token);
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {inject, injectable} from "@tsed/di";
|
|
2
|
+
import {Job, Worker, WorkerOptions} from "bullmq";
|
|
3
|
+
|
|
4
|
+
import {BullMQTypes} from "../constants/BullMQTypes.js";
|
|
5
|
+
import {getWorkerToken} from "./getWorkerToken.js";
|
|
6
|
+
|
|
7
|
+
export function createWorkerProvider(worker: string, process: (job: Job) => any, opts: WorkerOptions) {
|
|
8
|
+
const token = getWorkerToken(worker);
|
|
9
|
+
|
|
10
|
+
injectable(token)
|
|
11
|
+
.type(BullMQTypes.WORKER)
|
|
12
|
+
.value(new Worker(worker, process, opts))
|
|
13
|
+
.hooks({
|
|
14
|
+
$onDestroy: (worker: Worker) => worker.close()
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return inject<Worker>(token);
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {deepMerge} from "@tsed/core";
|
|
2
|
+
import type {QueueOptions} from "bullmq";
|
|
3
|
+
|
|
4
|
+
import {BullMQConfig} from "../config/config.js";
|
|
5
|
+
|
|
6
|
+
export function mapQueueOptions(queue: string, bullMQConfig: BullMQConfig): QueueOptions {
|
|
7
|
+
return deepMerge<QueueOptions, QueueOptions>(
|
|
8
|
+
{
|
|
9
|
+
connection: bullMQConfig.connection,
|
|
10
|
+
...bullMQConfig.defaultQueueOptions
|
|
11
|
+
},
|
|
12
|
+
bullMQConfig.queueOptions?.[queue]
|
|
13
|
+
)!;
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {deepMerge} from "@tsed/core";
|
|
2
|
+
import type {WorkerOptions} from "bullmq";
|
|
3
|
+
|
|
4
|
+
import {BullMQConfig} from "../config/config.js";
|
|
5
|
+
|
|
6
|
+
export function mapWorkerOptions(queue: string, bullMQConfig: BullMQConfig): WorkerOptions {
|
|
7
|
+
return deepMerge<WorkerOptions, WorkerOptions>(
|
|
8
|
+
{
|
|
9
|
+
connection: bullMQConfig.connection,
|
|
10
|
+
...bullMQConfig.defaultWorkerOptions
|
|
11
|
+
},
|
|
12
|
+
bullMQConfig.workerOptions?.[queue]
|
|
13
|
+
)!;
|
|
14
|
+
}
|