@tsed/bullmq 7.47.1 → 7.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +116 -11
  2. package/jest.config.js +1 -1
  3. package/lib/cjs/BullMQModule.js +24 -9
  4. package/lib/cjs/BullMQModule.js.map +1 -1
  5. package/lib/cjs/decorators/JobController.js +10 -1
  6. package/lib/cjs/decorators/JobController.js.map +1 -1
  7. package/lib/cjs/dispatchers/JobDispatcher.js +35 -8
  8. package/lib/cjs/dispatchers/JobDispatcher.js.map +1 -1
  9. package/lib/cjs/dispatchers/JobDispatcherOptions.js +3 -0
  10. package/lib/cjs/dispatchers/JobDispatcherOptions.js.map +1 -0
  11. package/lib/cjs/dispatchers/index.js +1 -0
  12. package/lib/cjs/dispatchers/index.js.map +1 -1
  13. package/lib/cjs/index.js +1 -0
  14. package/lib/cjs/index.js.map +1 -1
  15. package/lib/esm/BullMQModule.js +24 -9
  16. package/lib/esm/BullMQModule.js.map +1 -1
  17. package/lib/esm/decorators/JobController.js +8 -0
  18. package/lib/esm/decorators/JobController.js.map +1 -1
  19. package/lib/esm/dispatchers/JobDispatcher.js +35 -8
  20. package/lib/esm/dispatchers/JobDispatcher.js.map +1 -1
  21. package/lib/esm/dispatchers/JobDispatcherOptions.js +2 -0
  22. package/lib/esm/dispatchers/JobDispatcherOptions.js.map +1 -0
  23. package/lib/esm/dispatchers/index.js +1 -0
  24. package/lib/esm/dispatchers/index.js.map +1 -1
  25. package/lib/esm/index.js +1 -0
  26. package/lib/esm/index.js.map +1 -1
  27. package/lib/tsconfig.esm.tsbuildinfo +1 -1
  28. package/lib/tsconfig.tsbuildinfo +1 -1
  29. package/lib/types/config/config.d.ts +40 -1
  30. package/lib/types/contracts/JobMethods.d.ts +3 -2
  31. package/lib/types/decorators/JobController.d.ts +1 -0
  32. package/lib/types/dispatchers/JobDispatcher.d.ts +4 -0
  33. package/lib/types/dispatchers/JobDispatcherOptions.d.ts +4 -0
  34. package/lib/types/dispatchers/index.d.ts +1 -0
  35. package/lib/types/index.d.ts +1 -0
  36. package/package.json +5 -5
  37. package/src/BullMQModule.spec.ts +295 -118
  38. package/src/BullMQModule.ts +35 -10
  39. package/src/config/config.ts +48 -3
  40. package/src/contracts/JobMethods.ts +4 -2
  41. package/src/decorators/JobController.spec.ts +44 -1
  42. package/src/decorators/JobController.ts +12 -0
  43. package/src/dispatchers/JobDispatcher.spec.ts +48 -14
  44. package/src/dispatchers/JobDispatcher.ts +44 -8
  45. package/src/dispatchers/JobDispatcherOptions.ts +4 -0
  46. package/src/dispatchers/index.ts +1 -0
  47. package/src/index.ts +1 -0
@@ -1,7 +1,8 @@
1
1
  import {BeforeInit, DIContext, runInContext} from "@tsed/common";
2
2
  import {Constant, InjectorService, Module} from "@tsed/di";
3
+ import {deepMerge} from "@tsed/core";
3
4
  import {getComputedType} from "@tsed/schema";
4
- import {Job, Queue, Worker} from "bullmq";
5
+ import {Job, Queue, type QueueOptions, Worker, type WorkerOptions} from "bullmq";
5
6
  import {v4} from "uuid";
6
7
  import {BullMQConfig} from "./config/config";
7
8
  import {JobMethods} from "./contracts";
@@ -19,7 +20,9 @@ export class BullMQModule implements BeforeInit {
19
20
  return;
20
21
  }
21
22
 
22
- this.buildWorkers();
23
+ if (!this.bullmq.disableWorker) {
24
+ this.buildWorkers();
25
+ }
23
26
  this.buildQueues();
24
27
 
25
28
  this.injector.getMany<JobMethods>("bullmq:cron").map((job) => this.dispatcher.dispatch(getComputedType(job)));
@@ -27,13 +30,19 @@ export class BullMQModule implements BeforeInit {
27
30
 
28
31
  private buildQueues() {
29
32
  this.bullmq.queues.forEach((queue) => {
33
+ const ops = deepMerge<QueueOptions, QueueOptions>(
34
+ {
35
+ connection: this.bullmq.connection,
36
+ defaultJobOptions: this.bullmq.defaultJobOptions,
37
+ ...this.bullmq.defaultQueueOptions
38
+ },
39
+ this.bullmq.queueOptions?.[queue]
40
+ )!;
41
+
30
42
  this.injector
31
43
  .add(`bullmq.queue.${queue}`, {
32
44
  type: "bullmq:queue",
33
- useValue: new Queue(queue, {
34
- connection: this.bullmq.connection,
35
- defaultJobOptions: this.bullmq.defaultJobOptions
36
- }),
45
+ useValue: new Queue(queue, ops),
37
46
  hooks: {
38
47
  $onDestroy: (queue) => queue.close()
39
48
  }
@@ -44,12 +53,18 @@ export class BullMQModule implements BeforeInit {
44
53
 
45
54
  private buildWorkers() {
46
55
  (this.bullmq.workerQueues ?? this.bullmq.queues).forEach((queue) => {
56
+ const ops = deepMerge<WorkerOptions, WorkerOptions>(
57
+ {
58
+ connection: this.bullmq.connection,
59
+ ...this.bullmq.defaultWorkerOptions
60
+ },
61
+ this.bullmq.workerOptions?.[queue]
62
+ )!;
63
+
47
64
  this.injector
48
65
  .add(`bullmq.worker.${queue}`, {
49
66
  type: "bullmq:worker",
50
- useValue: new Worker(queue, this.onProcess.bind(this), {
51
- connection: this.bullmq.connection
52
- }),
67
+ useValue: new Worker(queue, this.onProcess.bind(this), ops),
53
68
  hooks: {
54
69
  $onDestroy: (worker) => worker.close()
55
70
  }
@@ -59,7 +74,17 @@ export class BullMQModule implements BeforeInit {
59
74
  }
60
75
 
61
76
  private getJob(name: string, queueName: string) {
62
- return this.injector.get(`bullmq.job.${queueName}.${name}`);
77
+ const job = this.injector.get<JobMethods>(`bullmq.job.${queueName}.${name}`);
78
+ if (job) {
79
+ return job;
80
+ }
81
+
82
+ const fallbackJobForQueue = this.injector.get(`bullmq.fallback-job.${queueName}`);
83
+ if (fallbackJobForQueue) {
84
+ return fallbackJobForQueue;
85
+ }
86
+
87
+ return this.injector.get("bullmq.fallback-job");
63
88
  }
64
89
 
65
90
  private async onProcess(job: Job) {
@@ -1,13 +1,58 @@
1
- import {type ConnectionOptions, type DefaultJobOptions} from "bullmq";
1
+ import type {ConnectionOptions, DefaultJobOptions, QueueOptions, WorkerOptions} from "bullmq";
2
2
 
3
3
  export type BullMQConfig = {
4
+ /**
5
+ * Specify queue name's to create
6
+ */
4
7
  queues: string[];
8
+
9
+ /**
10
+ * Default connection to use for queue's and worker's
11
+ */
5
12
  connection: ConnectionOptions;
13
+
14
+ /**
15
+ * @deprecated Use defaultQueueOptions instead. Will be removed in the next major release
16
+ */
6
17
  defaultJobOptions?: DefaultJobOptions;
18
+
19
+ /**
20
+ * Default queue options which are applied to every queue
21
+ *
22
+ * Can be extended/overridden by `queueOptions`
23
+ */
24
+ defaultQueueOptions?: QueueOptions;
25
+
26
+ /**
27
+ * Specify additional queue options by queue name
28
+ */
29
+ queueOptions?: Record<string, QueueOptions>;
30
+
31
+ /**
32
+ * Disable the creation of any worker.
33
+ *
34
+ * All other worker configuration will be ignored
35
+ */
7
36
  disableWorker?: boolean;
8
- // optionally specify for which queues to start a worker for
9
- // in case not all queues should be worked on
37
+
38
+ /**
39
+ * Specify for which queues to start a worker for.
40
+ *
41
+ * Defaultly for every queue added in the `queues` parameter
42
+ */
10
43
  workerQueues?: string[];
44
+
45
+ /**
46
+ * Default worker options which are applied to every worker
47
+ *
48
+ * Can be extended/overridden by `workerOptions`
49
+ */
50
+ defaultWorkerOptions?: WorkerOptions;
51
+
52
+ /**
53
+ * Specify additional worker options by queue name
54
+ */
55
+ workerOptions?: Record<string, WorkerOptions>;
11
56
  };
12
57
 
13
58
  declare global {
@@ -1,3 +1,5 @@
1
- export interface JobMethods {
2
- handle(payload: unknown): unknown;
1
+ import {Job} from "bullmq";
2
+
3
+ export interface JobMethods<DataType = unknown, ReturnType = unknown> {
4
+ handle(payload: DataType, job: Job<DataType, ReturnType>): ReturnType | Promise<ReturnType>;
3
5
  }
@@ -1,7 +1,10 @@
1
- import {JobController} from "./JobController";
1
+ import {FallbackJobController, JobController} from "./JobController";
2
2
  import {Store} from "@tsed/core";
3
+ import {GlobalProviders} from "@tsed/di";
3
4
 
4
5
  describe("JobController", () => {
6
+ afterEach(() => GlobalProviders.clear());
7
+
5
8
  it("should set the proper store data", () => {
6
9
  @JobController("testjob")
7
10
  class TestJob {}
@@ -48,3 +51,43 @@ describe("JobController", () => {
48
51
  expect(store);
49
52
  });
50
53
  });
54
+
55
+ describe("FallbackJobController", () => {
56
+ afterEach(() => GlobalProviders.clear());
57
+
58
+ describe("with queue", () => {
59
+ it("should set the proper store data", () => {
60
+ @FallbackJobController("default")
61
+ class FallbackJob {}
62
+
63
+ const store = Store.from(FallbackJob).get("bullmq");
64
+ expect(store.queue).toBe("default");
65
+
66
+ const fallbackControllerForQueue = GlobalProviders.get("bullmq.fallback-job.default");
67
+ expect(fallbackControllerForQueue).not.toBeUndefined();
68
+ expect(fallbackControllerForQueue.type).toEqual("bullmq:fallback-job");
69
+ expect(fallbackControllerForQueue.useClass).toEqual(FallbackJob);
70
+
71
+ const fallbackController = GlobalProviders.get("bullmq.fallback-job");
72
+ expect(fallbackController).toBeUndefined();
73
+ });
74
+ });
75
+
76
+ describe("without queue", () => {
77
+ it("should set the proper store data", () => {
78
+ @FallbackJobController()
79
+ class FallbackJob {}
80
+
81
+ const store = Store.from(FallbackJob).get("bullmq");
82
+ expect(store.queue).toBeUndefined();
83
+
84
+ const fallbackController = GlobalProviders.get("bullmq.fallback-job");
85
+ expect(fallbackController).not.toBeUndefined();
86
+ expect(fallbackController.type).toEqual("bullmq:fallback-job");
87
+ expect(fallbackController.useClass).toEqual(FallbackJob);
88
+
89
+ const fallbackControllerForQueue = GlobalProviders.get("bullmq.fallback-job.default");
90
+ expect(fallbackControllerForQueue).toBeUndefined();
91
+ });
92
+ });
93
+ });
@@ -15,3 +15,15 @@ export function JobController(name: string, queue: string = "default", opts: Job
15
15
  })
16
16
  );
17
17
  }
18
+
19
+ export function FallbackJobController(queue?: string) {
20
+ return useDecorators(
21
+ StoreMerge("bullmq", {
22
+ queue
23
+ }),
24
+ Injectable({
25
+ provide: "bullmq.fallback-job" + (queue ? `.${queue}` : ""),
26
+ type: "bullmq:fallback-job"
27
+ })
28
+ );
29
+ }
@@ -9,7 +9,7 @@ import {instance, mock, verify, when, objectContaining} from "ts-mockito";
9
9
  backoff: 69
10
10
  })
11
11
  class ExampleTestJob implements JobMethods {
12
- handle(payload: { msg: string }) {}
12
+ handle(payload: {msg: string}) {}
13
13
  }
14
14
 
15
15
  @JobController("queue-not-configured", "not-configured")
@@ -20,20 +20,20 @@ class NotConfiguredQueueTestJob implements JobMethods {
20
20
  describe("JobDispatcher", () => {
21
21
  it("should throw an exception when a queue is not configured", () => {
22
22
  const injector = mock(InjectorService);
23
- when(injector.getMany("bullmq:queue")).thenReturn([]);
23
+ when(injector.get("bullmq.queue.not-configured")).thenReturn();
24
24
 
25
25
  const dispatcher = new JobDispatcher(instance(injector));
26
26
 
27
27
  expect(dispatcher.dispatch(NotConfiguredQueueTestJob)).rejects.toThrow(new Error("Queue(not-configured) not defined"));
28
- verify(injector.getMany("bullmq:queue")).once();
28
+ verify(injector.get("bullmq.queue.not-configured")).once();
29
29
  });
30
30
 
31
- it("should dispatch job", async () => {
31
+ it("should dispatch job as type", async () => {
32
32
  const injector = mock(InjectorService);
33
33
  const queue = mock(Queue);
34
34
  when(queue.name).thenReturn("default");
35
35
 
36
- when(injector.getMany("bullmq:queue")).thenReturn([instance(queue)]);
36
+ when(injector.get("bullmq.queue.default")).thenReturn(instance(queue));
37
37
 
38
38
  const dispatcher = new JobDispatcher(instance(injector));
39
39
 
@@ -50,16 +50,50 @@ describe("JobDispatcher", () => {
50
50
  ).once();
51
51
  });
52
52
 
53
- it('should overwrite job options defined by the job', async () => {
53
+ it("should dispatch job as options", async () => {
54
+ const injector = mock(InjectorService);
55
+ const queue = mock(Queue);
56
+ when(queue.name).thenReturn("special");
57
+
58
+ when(injector.get("bullmq.queue.special")).thenReturn(instance(queue));
59
+
60
+ const dispatcher = new JobDispatcher(instance(injector));
61
+
62
+ await dispatcher.dispatch(
63
+ {
64
+ queue: "special",
65
+ name: "some-name"
66
+ },
67
+ {msg: "hello test"}
68
+ );
69
+
70
+ verify(queue.add("some-name", objectContaining({msg: "hello test"}), objectContaining({}))).once();
71
+ });
72
+
73
+ it("should dispatch job as string", async () => {
74
+ const injector = mock(InjectorService);
75
+ const queue = mock(Queue);
76
+ when(queue.name).thenReturn("default");
77
+
78
+ when(injector.get("bullmq.queue.default")).thenReturn(instance(queue));
79
+
80
+ const dispatcher = new JobDispatcher(instance(injector));
81
+
82
+ await dispatcher.dispatch("some-name", {msg: "hello test"});
83
+
84
+ verify(queue.add("some-name", objectContaining({msg: "hello test"}), objectContaining({}))).once();
85
+ });
86
+
87
+ it("should overwrite job options defined by the job", async () => {
54
88
  const injector = mock(InjectorService);
55
89
  const queue = mock(Queue);
56
90
  when(queue.name).thenReturn("default");
57
91
 
58
- when(injector.getMany("bullmq:queue")).thenReturn([instance(queue)]);
92
+ when(injector.get("bullmq.queue.default")).thenReturn(instance(queue));
59
93
 
60
94
  const dispatcher = new JobDispatcher(instance(injector));
61
95
 
62
- await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"}, {backoff: 42, jobId: 'ffeeaa'});
96
+ await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"}, {backoff: 42, jobId: "ffeeaa"});
63
97
 
64
98
  verify(
65
99
  queue.add(
@@ -67,22 +101,22 @@ describe("JobDispatcher", () => {
67
101
  objectContaining({msg: "hello test"}),
68
102
  objectContaining({
69
103
  backoff: 42,
70
- jobId: 'ffeeaa'
104
+ jobId: "ffeeaa"
71
105
  })
72
106
  )
73
107
  ).once();
74
- })
108
+ });
75
109
 
76
- it('should keep existing options and add new ones', async () => {
110
+ it("should keep existing options and add new ones", async () => {
77
111
  const injector = mock(InjectorService);
78
112
  const queue = mock(Queue);
79
113
  when(queue.name).thenReturn("default");
80
114
 
81
- when(injector.getMany("bullmq:queue")).thenReturn([instance(queue)]);
115
+ when(injector.get("bullmq.queue.default")).thenReturn(instance(queue));
82
116
 
83
117
  const dispatcher = new JobDispatcher(instance(injector));
84
118
 
85
- await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"}, {jobId: 'ffeeaa'});
119
+ await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"}, {jobId: "ffeeaa"});
86
120
 
87
121
  verify(
88
122
  queue.add(
@@ -90,7 +124,7 @@ describe("JobDispatcher", () => {
90
124
  objectContaining({msg: "hello test"}),
91
125
  objectContaining({
92
126
  backoff: 69,
93
- jobId: 'ffeeaa'
127
+ jobId: "ffeeaa"
94
128
  })
95
129
  )
96
130
  ).once();
@@ -1,25 +1,61 @@
1
+ /* eslint-disable require-await */
1
2
  import {InjectorService, Service} from "@tsed/di";
2
3
  import {JobMethods, type JobStore} from "../contracts";
3
4
  import {Store, Type} from "@tsed/core";
4
5
  import {Job as BullMQJob, JobsOptions, Queue} from "bullmq";
6
+ import type {JobDispatcherOptions} from "./JobDispatcherOptions";
5
7
 
6
8
  @Service()
7
9
  export class JobDispatcher {
8
10
  constructor(private readonly injector: InjectorService) {}
9
11
 
10
- // eslint-disable-next-line require-await
11
- public async dispatch<T extends JobMethods>(job: Type<T>, payload: Parameters<T["handle"]>[0] = {}, options: JobsOptions = {}): Promise<BullMQJob> {
12
- const store = Store.from(job).get<JobStore>("bullmq");
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<T = any>(job: JobDispatcherOptions, payload?: T, options?: JobsOptions): Promise<BullMQJob>;
18
+ public async dispatch<T = any>(job: string, payload?: T, options?: JobsOptions): Promise<BullMQJob>;
19
+ public async dispatch(job: Type | JobDispatcherOptions | string, payload: any, options: JobsOptions = {}): Promise<BullMQJob> {
20
+ const {queueName, jobName, defaultJobOptions} = this.resolveDispatchArgs(job);
13
21
 
14
- const queue = this.injector.getMany<Queue>("bullmq:queue").find((queue) => queue.name === store.queue);
22
+ const queue = this.injector.get<Queue>(`bullmq.queue.${queueName}`);
15
23
 
16
24
  if (!queue) {
17
- throw new Error(`Queue(${store.queue}) not defined`);
25
+ throw new Error(`Queue(${queueName}) not defined`);
18
26
  }
19
27
 
20
- return queue.add(store.name, payload, {
21
- ...store.opts,
22
- ...options,
28
+ return queue.add(jobName, payload, {
29
+ ...defaultJobOptions,
30
+ ...options
23
31
  });
24
32
  }
33
+
34
+ private resolveDispatchArgs(job: Type | JobDispatcherOptions | string) {
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
+ defaultJobOptions = store.opts;
45
+ } else if (typeof job === "object") {
46
+ // job is passed as JobDispatcherOptions
47
+ queueName = job.queue;
48
+ jobName = job.name;
49
+ } else {
50
+ // job is passed as a string
51
+ queueName = "default";
52
+ jobName = job;
53
+ }
54
+
55
+ return {
56
+ queueName,
57
+ jobName,
58
+ defaultJobOptions
59
+ };
60
+ }
25
61
  }
@@ -0,0 +1,4 @@
1
+ export interface JobDispatcherOptions {
2
+ queue: string;
3
+ name: string;
4
+ }
@@ -3,3 +3,4 @@
3
3
  */
4
4
 
5
5
  export * from "./JobDispatcher";
6
+ export * from "./JobDispatcherOptions";
package/src/index.ts CHANGED
@@ -8,3 +8,4 @@ export * from "./contracts/JobMethods";
8
8
  export * from "./contracts/JobStore";
9
9
  export * from "./decorators/JobController";
10
10
  export * from "./dispatchers/JobDispatcher";
11
+ export * from "./dispatchers/JobDispatcherOptions";