@pauldeng/node-red-contrib-bullmq 1.0.2 → 2.0.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.
@@ -4,11 +4,11 @@
4
4
  "type": "tab",
5
5
  "label": "BullMQ repeatable jobs",
6
6
  "disabled": false,
7
- "info": "Dedicated repeatable job command examples for BullMQ Job Scheduler compatibility commands."
7
+ "info": "Dedicated examples for the native BullMQ v6 Job Scheduler commands."
8
8
  },
9
9
  {
10
10
  "id": "queue-repeatable-jobs",
11
- "type": "bull-queue-server",
11
+ "type": "bullmq-queue-server",
12
12
  "name": "basecasts",
13
13
  "deployment": "single",
14
14
  "address": "localhost",
@@ -29,7 +29,7 @@
29
29
  "id": "inject-repeat-add",
30
30
  "type": "inject",
31
31
  "z": "flow-repeatable-jobs",
32
- "name": "repeat: add basecasts job",
32
+ "name": "scheduler: upsert basecasts job",
33
33
  "props": [{ "p": "payload" }],
34
34
  "repeat": "",
35
35
  "crontab": "",
@@ -46,8 +46,8 @@
46
46
  "id": "fn-repeat-add",
47
47
  "type": "function",
48
48
  "z": "flow-repeatable-jobs",
49
- "name": "add repeat cron",
50
- "func": "msg.cmd = \"add\";\nmsg.jobopts = {\"jobId\": msg.payload, \"repeat\": {\"cron\": \"30 9,19,29,39,49,59 * * * *\"}};\nreturn msg;",
49
+ "name": "upsertJobScheduler",
50
+ "func": "msg.cmd = \"upsertJobScheduler\";\nmsg.schedulerId = msg.payload;\nmsg.repeat = { pattern: \"30 9,19,29,39,49,59 * * * *\", tz: \"UTC\" };\nmsg.template = { name: \"default\", data: { payload: msg.payload } };\nreturn msg;",
51
51
  "outputs": 1,
52
52
  "noerr": 0,
53
53
  "initialize": "",
@@ -61,7 +61,7 @@
61
61
  "id": "inject-repeat-get-all",
62
62
  "type": "inject",
63
63
  "z": "flow-repeatable-jobs",
64
- "name": "repeat: getRepeatableJobs",
64
+ "name": "scheduler: getJobSchedulers",
65
65
  "props": [{ "p": "payload" }],
66
66
  "repeat": "",
67
67
  "crontab": "",
@@ -78,8 +78,8 @@
78
78
  "id": "fn-repeat-get-all",
79
79
  "type": "function",
80
80
  "z": "flow-repeatable-jobs",
81
- "name": "getRepeatableJobs",
82
- "func": "msg.cmd = \"getRepeatableJobs\";\nreturn msg;",
81
+ "name": "getJobSchedulers",
82
+ "func": "msg.cmd = \"getJobSchedulers\";\nreturn msg;",
83
83
  "outputs": 1,
84
84
  "noerr": 0,
85
85
  "initialize": "",
@@ -93,7 +93,7 @@
93
93
  "id": "inject-repeat-count",
94
94
  "type": "inject",
95
95
  "z": "flow-repeatable-jobs",
96
- "name": "repeat: count",
96
+ "name": "scheduler: getJobSchedulersCount",
97
97
  "props": [{ "p": "payload" }],
98
98
  "repeat": "",
99
99
  "crontab": "",
@@ -110,8 +110,8 @@
110
110
  "id": "fn-repeat-count",
111
111
  "type": "function",
112
112
  "z": "flow-repeatable-jobs",
113
- "name": "count",
114
- "func": "msg.cmd = \"count\";\nreturn msg;",
113
+ "name": "getJobSchedulersCount",
114
+ "func": "msg.cmd = \"getJobSchedulersCount\";\nreturn msg;",
115
115
  "outputs": 1,
116
116
  "noerr": 0,
117
117
  "initialize": "",
@@ -125,7 +125,7 @@
125
125
  "id": "inject-repeat-get-by-key",
126
126
  "type": "inject",
127
127
  "z": "flow-repeatable-jobs",
128
- "name": "repeat: getRepeatableJobByKey",
128
+ "name": "scheduler: getJobScheduler",
129
129
  "props": [{ "p": "payload" }],
130
130
  "repeat": "",
131
131
  "crontab": "",
@@ -142,8 +142,8 @@
142
142
  "id": "fn-repeat-get-by-key",
143
143
  "type": "function",
144
144
  "z": "flow-repeatable-jobs",
145
- "name": "getRepeatableJobByKey",
146
- "func": "msg.cmd = \"getRepeatableJobByKey\";\nmsg.jobid = msg.payload;\nreturn msg;",
145
+ "name": "getJobScheduler",
146
+ "func": "msg.cmd = \"getJobScheduler\";\nmsg.schedulerId = msg.payload;\nreturn msg;",
147
147
  "outputs": 1,
148
148
  "noerr": 0,
149
149
  "initialize": "",
@@ -157,7 +157,7 @@
157
157
  "id": "inject-repeat-remove-by-key",
158
158
  "type": "inject",
159
159
  "z": "flow-repeatable-jobs",
160
- "name": "repeat: removeRepeatableByKey",
160
+ "name": "scheduler: removeJobScheduler",
161
161
  "props": [{ "p": "payload" }],
162
162
  "repeat": "",
163
163
  "crontab": "",
@@ -174,8 +174,8 @@
174
174
  "id": "fn-repeat-remove-by-key",
175
175
  "type": "function",
176
176
  "z": "flow-repeatable-jobs",
177
- "name": "removeRepeatableByKey",
178
- "func": "msg.cmd = \"removeRepeatableByKey\";\nmsg.jobid = msg.payload;\nreturn msg;",
177
+ "name": "removeJobScheduler",
178
+ "func": "msg.cmd = \"removeJobScheduler\";\nmsg.schedulerId = msg.payload;\nreturn msg;",
179
179
  "outputs": 1,
180
180
  "noerr": 0,
181
181
  "initialize": "",
@@ -189,7 +189,7 @@
189
189
  "id": "inject-repeat-stop-all",
190
190
  "type": "inject",
191
191
  "z": "flow-repeatable-jobs",
192
- "name": "repeat: stopAndRemoveAllJobs",
192
+ "name": "scheduler: stopAndRemoveAllJobs",
193
193
  "props": [{ "p": "payload" }],
194
194
  "repeat": "",
195
195
  "crontab": "",
@@ -217,9 +217,41 @@
217
217
  "y": 500,
218
218
  "wires": [["cmd-repeatable-jobs"]]
219
219
  },
220
+ {
221
+ "id": "inject-repeat-timezone",
222
+ "type": "inject",
223
+ "z": "flow-repeatable-jobs",
224
+ "name": "scheduler: upsert with timezone",
225
+ "props": [{ "p": "payload" }],
226
+ "repeat": "",
227
+ "crontab": "",
228
+ "once": false,
229
+ "onceDelay": 0.1,
230
+ "topic": "",
231
+ "payload": "gateway-timezone-demo",
232
+ "payloadType": "str",
233
+ "x": 200,
234
+ "y": 580,
235
+ "wires": [["fn-repeat-timezone"]]
236
+ },
237
+ {
238
+ "id": "fn-repeat-timezone",
239
+ "type": "function",
240
+ "z": "flow-repeatable-jobs",
241
+ "name": "upsert scheduler with tz",
242
+ "func": "msg.cmd = \"upsertJobScheduler\";\nmsg.schedulerId = msg.payload;\nmsg.repeat = { pattern: \"30 9,19,29,39,49,59 * * * *\", tz: \"Australia/Melbourne\" };\nmsg.template = { name: \"default\", data: { payload: msg.payload } };\nreturn msg;",
243
+ "outputs": 1,
244
+ "noerr": 0,
245
+ "initialize": "",
246
+ "finalize": "",
247
+ "libs": [],
248
+ "x": 470,
249
+ "y": 580,
250
+ "wires": [["cmd-repeatable-jobs"]]
251
+ },
220
252
  {
221
253
  "id": "cmd-repeatable-jobs",
222
- "type": "bull cmd",
254
+ "type": "bullmq cmd",
223
255
  "z": "flow-repeatable-jobs",
224
256
  "name": "run repeat command",
225
257
  "queue": "queue-repeatable-jobs",
@@ -0,0 +1,216 @@
1
+ [
2
+ {
3
+ "id": "flow-scheduled-notifications",
4
+ "type": "tab",
5
+ "label": "Scheduled notifications",
6
+ "disabled": false,
7
+ "info": ""
8
+ },
9
+ {
10
+ "id": "queue-scheduled-notifications",
11
+ "type": "bullmq-queue-server",
12
+ "name": "notifycasts",
13
+ "backend": "redis",
14
+ "deployment": "single",
15
+ "address": "localhost",
16
+ "port": "6379",
17
+ "db": "",
18
+ "database": "",
19
+ "schema": "",
20
+ "max": "",
21
+ "migrate": true,
22
+ "clusterNodes": "",
23
+ "sentinels": "",
24
+ "sentinelMasterName": "",
25
+ "username": "",
26
+ "sentinelUsername": "",
27
+ "tls": false,
28
+ "sentinelTls": false,
29
+ "tlsRejectUnauthorized": true,
30
+ "tlsServerName": "",
31
+ "prefix": ""
32
+ },
33
+ {
34
+ "id": "inject-notify-schedule",
35
+ "type": "inject",
36
+ "z": "flow-scheduled-notifications",
37
+ "name": "notify: schedule user series",
38
+ "props": [
39
+ {
40
+ "p": "payload"
41
+ }
42
+ ],
43
+ "repeat": "",
44
+ "crontab": "",
45
+ "once": false,
46
+ "onceDelay": 0.1,
47
+ "topic": "",
48
+ "payload": "",
49
+ "payloadType": "date",
50
+ "x": 190,
51
+ "y": 100,
52
+ "wires": [["fn-notify-schedule"]]
53
+ },
54
+ {
55
+ "id": "fn-notify-schedule",
56
+ "type": "function",
57
+ "z": "flow-scheduled-notifications",
58
+ "name": "addBulk from ISO-8601 schedule",
59
+ "func": "// One BullMQ job per scheduled message. BullMQ delays in milliseconds rather\n// than at an absolute time, so each explicit ISO-8601 instant is converted to\n// a delay when the job is enqueued.\n//\n// Send your own schedule as msg.payload in this shape to use real data; the\n// sample below runs when nothing is sent, and its dates are placeholders --\n// replace them, or any instant already in the past becomes a delay of 0 and\n// the notification is delivered immediately.\nconst userSchedules = Array.isArray(msg.payload)\n ? msg.payload\n : [\n {\n userId: \"user_A\",\n jobs: [\n { time: \"2026-10-01T08:00:00Z\", message: { type: \"welcome\", title: \"Welcome!\", content: \"Thanks for signing up.\", pointsAwarded: 100 } },\n { time: \"2026-10-03T12:00:00Z\", message: { type: \"survey\", title: \"Quick Feedback\", url: \"https://example.com\", rewardCode: \"THANKYOU\" } },\n { time: \"2026-10-07T15:30:00Z\", message: { type: \"promotion\", title: \"Flash Sale\", discountPercentage: 25 } },\n { time: \"2026-10-15T09:00:00Z\", message: { type: \"billing\", title: \"Invoice Ready\", invoiceId: \"INV-001\", amountDue: 49.99 } },\n { time: \"2026-10-31T20:00:00Z\", message: { type: \"summary\", title: \"Monthly Wrap-up\", activeDays: 14 } }\n ]\n },\n {\n userId: \"user_B\",\n jobs: [\n { time: \"2026-10-02T10:00:00Z\", message: { type: \"security\", title: \"New Login Detected\", device: \"Chrome / Windows\", location: \"New York\" } },\n { time: \"2026-10-12T14:00:00Z\", message: { type: \"trial_expiry\", title: \"Trial Ending Soon\", daysLeft: 3, upgradeUrl: \"https://example.com\" } },\n { time: \"2026-10-25T18:00:00Z\", message: { type: \"reengage\", title: \"We Miss You\", specialOffer: \"Free Shipping Code: SHIPFREE\" } }\n ]\n }\n ];\n\nconst now = Date.now();\n\n// addBulk enqueues the whole series in one round trip. A per-notification\n// jobId derived from the user, the type and the scheduled instant makes\n// re-sending the same schedule idempotent: BullMQ ignores an add for a jobId\n// it already holds, so a second inject cannot double-book a notification.\n//\n// The instant goes in as epoch milliseconds, and the parts are joined with\n// \"-\", because BullMQ rejects a custom job id containing \":\" -- which an\n// ISO-8601 timestamp is full of.\nmsg.cmd = \"addBulk\";\nmsg.payload = userSchedules.flatMap((user) =>\n user.jobs.map((job) => {\n const scheduledAt = new Date(job.time).getTime();\n return {\n name: job.message.type,\n data: { userId: user.userId, scheduledFor: job.time, message: job.message },\n opts: {\n delay: Math.max(0, scheduledAt - now),\n jobId: `${user.userId}-${job.message.type}-${scheduledAt}`,\n removeOnComplete: 1000,\n removeOnFail: 5000\n }\n };\n })\n);\nreturn msg;",
60
+ "outputs": 1,
61
+ "noerr": 0,
62
+ "initialize": "",
63
+ "finalize": "",
64
+ "libs": [],
65
+ "x": 470,
66
+ "y": 100,
67
+ "wires": [["cmd-scheduled-notifications"]]
68
+ },
69
+ {
70
+ "id": "inject-notify-cron",
71
+ "type": "inject",
72
+ "z": "flow-scheduled-notifications",
73
+ "name": "notify: cron daily digest",
74
+ "props": [
75
+ {
76
+ "p": "payload"
77
+ }
78
+ ],
79
+ "repeat": "",
80
+ "crontab": "",
81
+ "once": false,
82
+ "onceDelay": 0.1,
83
+ "topic": "",
84
+ "payload": "",
85
+ "payloadType": "date",
86
+ "x": 190,
87
+ "y": 180,
88
+ "wires": [["fn-notify-cron"]]
89
+ },
90
+ {
91
+ "id": "fn-notify-cron",
92
+ "type": "function",
93
+ "z": "flow-scheduled-notifications",
94
+ "name": "upsertJobScheduler with message",
95
+ "func": "// A cron-scheduled repeating notification. The pattern is a 6-field cron\n// expression (the leading field is seconds), and msg.repeat.tz fixes the zone\n// so the hour does not drift with the host's own timezone or daylight saving.\n//\n// The message travels in msg.template.data: the template is what every job\n// this scheduler generates is created from, so anything the worker needs must\n// be in there rather than in the scheduler id.\nmsg.cmd = \"upsertJobScheduler\";\nmsg.schedulerId = \"daily-digest\";\nmsg.repeat = { pattern: \"0 0 8 * * *\", tz: \"UTC\" };\nmsg.template = {\n name: \"digest\",\n data: {\n userId: \"user_A\",\n message: {\n type: \"digest\",\n title: \"Your daily digest\",\n content: \"Here is what happened while you were away.\"\n }\n }\n};\nreturn msg;",
96
+ "outputs": 1,
97
+ "noerr": 0,
98
+ "initialize": "",
99
+ "finalize": "",
100
+ "libs": [],
101
+ "x": 470,
102
+ "y": 180,
103
+ "wires": [["cmd-scheduled-notifications"]]
104
+ },
105
+ {
106
+ "id": "inject-notify-cron-remove",
107
+ "type": "inject",
108
+ "z": "flow-scheduled-notifications",
109
+ "name": "notify: remove cron digest",
110
+ "props": [
111
+ {
112
+ "p": "payload"
113
+ }
114
+ ],
115
+ "repeat": "",
116
+ "crontab": "",
117
+ "once": false,
118
+ "onceDelay": 0.1,
119
+ "topic": "",
120
+ "payload": "",
121
+ "payloadType": "date",
122
+ "x": 190,
123
+ "y": 260,
124
+ "wires": [["fn-notify-cron-remove"]]
125
+ },
126
+ {
127
+ "id": "fn-notify-cron-remove",
128
+ "type": "function",
129
+ "z": "flow-scheduled-notifications",
130
+ "name": "removeJobScheduler",
131
+ "func": "// Job Schedulers outlive a redeploy, so an example that creates one needs a\n// way to remove it again.\nmsg.cmd = \"removeJobScheduler\";\nmsg.schedulerId = \"daily-digest\";\nreturn msg;",
132
+ "outputs": 1,
133
+ "noerr": 0,
134
+ "initialize": "",
135
+ "finalize": "",
136
+ "libs": [],
137
+ "x": 470,
138
+ "y": 260,
139
+ "wires": [["cmd-scheduled-notifications"]]
140
+ },
141
+ {
142
+ "id": "cmd-scheduled-notifications",
143
+ "type": "bullmq cmd",
144
+ "z": "flow-scheduled-notifications",
145
+ "name": "schedule command",
146
+ "queue": "queue-scheduled-notifications",
147
+ "x": 760,
148
+ "y": 180,
149
+ "wires": [["debug-scheduled-notifications"]]
150
+ },
151
+ {
152
+ "id": "debug-scheduled-notifications",
153
+ "type": "debug",
154
+ "z": "flow-scheduled-notifications",
155
+ "name": "schedule output",
156
+ "active": true,
157
+ "tosidebar": true,
158
+ "console": false,
159
+ "tostatus": false,
160
+ "complete": "payload",
161
+ "targetType": "msg",
162
+ "statusVal": "",
163
+ "statusType": "auto",
164
+ "x": 990,
165
+ "y": 180,
166
+ "wires": []
167
+ },
168
+ {
169
+ "id": "run-scheduled-notifications",
170
+ "type": "bullmq run",
171
+ "z": "flow-scheduled-notifications",
172
+ "name": "notification worker",
173
+ "queue": "queue-scheduled-notifications",
174
+ "completionMode": "immediate",
175
+ "ackTimeout": 300000,
176
+ "concurrency": 1,
177
+ "maxStartedAttempts": 100,
178
+ "limiterMax": "",
179
+ "limiterDuration": "",
180
+ "x": 200,
181
+ "y": 400,
182
+ "wires": [["fn-notify-deliver"]]
183
+ },
184
+ {
185
+ "id": "fn-notify-deliver",
186
+ "type": "function",
187
+ "z": "flow-scheduled-notifications",
188
+ "name": "deliver notification",
189
+ "func": "// The worker is handed one message per notification, whether it came from the\n// delayed series or from the cron scheduler. msg.payload is the job's data, so\n// the notification itself is msg.payload.message.\nconst { userId, message } = msg.payload || {};\nmsg.topic = `${userId || \"unknown\"}/${(message && message.type) || \"unknown\"}`;\nmsg.payload = { userId, ...message };\nreturn msg;",
190
+ "outputs": 1,
191
+ "noerr": 0,
192
+ "initialize": "",
193
+ "finalize": "",
194
+ "libs": [],
195
+ "x": 470,
196
+ "y": 400,
197
+ "wires": [["debug-notify-delivered"]]
198
+ },
199
+ {
200
+ "id": "debug-notify-delivered",
201
+ "type": "debug",
202
+ "z": "flow-scheduled-notifications",
203
+ "name": "delivered notification",
204
+ "active": true,
205
+ "tosidebar": true,
206
+ "console": false,
207
+ "tostatus": true,
208
+ "complete": "payload",
209
+ "targetType": "msg",
210
+ "statusVal": "topic",
211
+ "statusType": "msg",
212
+ "x": 740,
213
+ "y": 400,
214
+ "wires": []
215
+ }
216
+ ]
@@ -10,8 +10,24 @@ class AcknowledgementEntry {
10
10
  this.queue = context.queue;
11
11
  this.queueName = context.queueName;
12
12
  this.runNodeId = context.runNodeId;
13
+ this.worker = context.worker;
14
+ // The job's lock token. Required by the moveTo* transitions and never
15
+ // serialized into a Node-RED message.
16
+ this.token = context.token;
13
17
  this.timeout = undefined;
14
18
 
19
+ // BullMQ's per-job AbortController: aborting it (worker.cancelJob /
20
+ // cancelAllJobs) does not settle this acknowledgement by itself, so a
21
+ // manual-mode job would stay active forever unless we fail it here.
22
+ this.signal = context.signal;
23
+ this.abortListener = undefined;
24
+ if (this.signal) {
25
+ this.abortListener = () => {
26
+ this.fail(new Error(String(this.signal.reason)));
27
+ };
28
+ this.signal.addEventListener("abort", this.abortListener);
29
+ }
30
+
15
31
  if (timeoutMs > 0) {
16
32
  this.timeout = setTimeout(() => {
17
33
  this.fail(
@@ -46,6 +62,9 @@ class AcknowledgementEntry {
46
62
  }
47
63
  this.settlement = settlement;
48
64
  clearTimeout(this.timeout);
65
+ if (this.abortListener) {
66
+ this.signal.removeEventListener("abort", this.abortListener);
67
+ }
49
68
  this.events.emit("settled", settlement);
50
69
  }
51
70
  }
@@ -105,7 +124,6 @@ class AcknowledgementRegistry {
105
124
  }
106
125
 
107
126
  module.exports = {
108
- AcknowledgementEntry,
109
127
  AcknowledgementRegistry,
110
128
  parseAckTimeoutMs,
111
129
  };
package/lib/commands.js CHANGED
@@ -1,20 +1,26 @@
1
1
  "use strict";
2
2
 
3
- const {
4
- getLegacySchedulerId,
5
- normalizeAddRequest,
6
- serializeScheduler,
7
- } = require("./scheduler");
3
+ const { getSchedulerId, serializeScheduler } = require("./scheduler");
8
4
  const { serializeJob } = require("./serialization");
9
5
 
10
6
  const CLEAN_STATES = ["completed", "failed", "delayed", "wait"];
7
+ const REMOVED_COMMANDS = {
8
+ count: "getJobSchedulersCount",
9
+ getRepeatableJobs: "getJobSchedulers",
10
+ getRepeatableJobByKey: "getJobScheduler",
11
+ removeRepeatableByKey: "removeJobScheduler",
12
+ };
13
+
14
+ function isPresent(value) {
15
+ return value !== undefined && value !== null && value !== "";
16
+ }
11
17
 
12
18
  function valueOrDefault(value, defaultValue) {
13
19
  return value === undefined || value === null ? defaultValue : value;
14
20
  }
15
21
 
16
22
  async function getRequiredJob(queue, msg) {
17
- const jobId = String(msg.jobId || msg.jobid || "").trim();
23
+ const jobId = String(msg.jobId || "").trim();
18
24
  if (!jobId) {
19
25
  throw new Error("msg.jobId is required");
20
26
  }
@@ -26,19 +32,16 @@ async function getRequiredJob(queue, msg) {
26
32
  }
27
33
 
28
34
  async function dispatchAdd(queue, msg) {
29
- const request = normalizeAddRequest(msg);
30
- if (request.kind === "scheduler") {
31
- return serializeJob(
32
- await queue.upsertJobScheduler(
33
- request.schedulerId,
34
- request.repeat,
35
- request.template,
36
- ),
35
+ const opts = msg.jobopts || {};
36
+ if (opts.repeat) {
37
+ throw new Error(
38
+ 'add does not support repeat options; use cmd "upsertJobScheduler" with msg.repeat.pattern',
37
39
  );
38
40
  }
39
- return serializeJob(
40
- await queue.add(request.name, request.data, request.opts),
41
- );
41
+ const data = Object.prototype.hasOwnProperty.call(msg, "jobData")
42
+ ? msg.jobData
43
+ : { payload: msg.payload };
44
+ return serializeJob(await queue.add(msg.jobName || "default", data, opts));
42
45
  }
43
46
 
44
47
  async function dispatchStopAndRemoveAllJobs(queue) {
@@ -63,15 +66,37 @@ async function dispatchStopAndRemoveAllJobs(queue) {
63
66
  }
64
67
 
65
68
  async function dispatchCommand(queue, msg = {}) {
66
- const cmd = msg.cmd || msg.command || "add";
69
+ if (!isPresent(msg.cmd) && isPresent(msg.command)) {
70
+ throw new Error("msg.command is not supported; use msg.cmd");
71
+ }
72
+ if (!isPresent(msg.jobId) && isPresent(msg.jobid)) {
73
+ throw new Error("msg.jobid is not supported; use msg.jobId");
74
+ }
75
+ const cmd = msg.cmd || "add";
76
+ if (Object.hasOwn(REMOVED_COMMANDS, cmd)) {
77
+ throw new Error(
78
+ `cmd "${cmd}" is not supported; use "${REMOVED_COMMANDS[cmd]}"`,
79
+ );
80
+ }
67
81
 
68
82
  switch (cmd) {
69
83
  case "add":
70
84
  return dispatchAdd(queue, msg);
71
- case "addBulk":
72
- return (await queue.addBulk(msg.payload || [])).map(serializeJob);
85
+ case "addBulk": {
86
+ const jobs = msg.payload || [];
87
+ if (Array.isArray(jobs)) {
88
+ for (const job of jobs) {
89
+ if (job && job.opts && job.opts.repeat) {
90
+ throw new Error(
91
+ 'addBulk does not support repeat options; use cmd "upsertJobScheduler" with msg.repeat.pattern',
92
+ );
93
+ }
94
+ }
95
+ }
96
+ return (await queue.addBulk(jobs)).map(serializeJob);
97
+ }
73
98
  case "getJob":
74
- return serializeJob(await queue.getJob(msg.jobId || msg.jobid));
99
+ return serializeJob(await queue.getJob(msg.jobId));
75
100
  case "getJobs":
76
101
  return (
77
102
  await queue.getJobs(
@@ -137,20 +162,34 @@ async function dispatchCommand(queue, msg = {}) {
137
162
  return queue.getDeduplicationJobId(msg.deduplicationId);
138
163
  case "removeDeduplicationKey":
139
164
  return queue.removeDeduplicationKey(msg.deduplicationId);
140
- case "upsertJobScheduler":
165
+ case "upsertJobScheduler": {
166
+ if (
167
+ !msg.repeat ||
168
+ typeof msg.repeat !== "object" ||
169
+ Array.isArray(msg.repeat)
170
+ ) {
171
+ throw new Error(
172
+ "upsertJobScheduler requires msg.repeat to be a BullMQ v6 repeat options object",
173
+ );
174
+ }
175
+ if ("cron" in msg.repeat || "utc" in msg.repeat) {
176
+ throw new Error(
177
+ "upsertJobScheduler requires BullMQ v6 repeat.pattern and repeat.tz fields",
178
+ );
179
+ }
141
180
  return serializeJob(
142
181
  await queue.upsertJobScheduler(
143
- getLegacySchedulerId(msg),
182
+ getSchedulerId(msg),
144
183
  msg.repeat,
145
184
  msg.template,
146
185
  ),
147
186
  );
187
+ }
148
188
  case "getJobScheduler":
149
189
  return serializeScheduler(
150
- await queue.getJobScheduler(getLegacySchedulerId(msg)),
190
+ await queue.getJobScheduler(getSchedulerId(msg)),
151
191
  );
152
192
  case "getJobSchedulers":
153
- case "getRepeatableJobs":
154
193
  return (
155
194
  await queue.getJobSchedulers(
156
195
  valueOrDefault(msg.start, 0),
@@ -159,15 +198,9 @@ async function dispatchCommand(queue, msg = {}) {
159
198
  )
160
199
  ).map(serializeScheduler);
161
200
  case "getJobSchedulersCount":
162
- case "count":
163
201
  return queue.getJobSchedulersCount();
164
202
  case "removeJobScheduler":
165
- case "removeRepeatableByKey":
166
- return queue.removeJobScheduler(getLegacySchedulerId(msg));
167
- case "getRepeatableJobByKey":
168
- return serializeScheduler(
169
- await queue.getJobScheduler(getLegacySchedulerId(msg)),
170
- );
203
+ return queue.removeJobScheduler(getSchedulerId(msg));
171
204
  case "getJobCounts":
172
205
  return queue.getJobCounts(...(msg.types || []));
173
206
  case "pause":
@@ -176,6 +209,10 @@ async function dispatchCommand(queue, msg = {}) {
176
209
  case "resume":
177
210
  await queue.resume();
178
211
  return true;
212
+ case "isPaused":
213
+ return queue.isPaused();
214
+ case "isMaxed":
215
+ return queue.isMaxed();
179
216
  case "drain":
180
217
  await queue.drain(valueOrDefault(msg.delayed, false));
181
218
  return true;
@@ -215,18 +252,20 @@ async function dispatchCommand(queue, msg = {}) {
215
252
  case "removeRateLimitKey":
216
253
  return queue.removeRateLimitKey();
217
254
  case "addJobLog":
218
- return queue.addJobLog(msg.jobId || msg.jobid, msg.logRow, msg.keepLogs);
255
+ return queue.addJobLog(msg.jobId, msg.logRow, msg.keepLogs);
219
256
  case "getJobLogs":
220
257
  return queue.getJobLogs(
221
- msg.jobId || msg.jobid,
258
+ msg.jobId,
222
259
  valueOrDefault(msg.start, 0),
223
260
  valueOrDefault(msg.end, -1),
224
261
  valueOrDefault(msg.asc, true),
225
262
  );
263
+ case "getVersion":
264
+ return queue.getVersion();
226
265
  case "exportPrometheusMetrics":
227
266
  return queue.exportPrometheusMetrics();
228
267
  default:
229
- throw new Error(`Unsupported bull cmd: ${cmd}`);
268
+ throw new Error(`Unsupported bullmq cmd: ${cmd}`);
230
269
  }
231
270
  }
232
271