@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.
- package/README.md +62 -42
- package/bull-queue.html +300 -91
- package/bull-queue.js +686 -143
- package/docs/ARCHITECTURE.md +63 -17
- package/docs/CHANGE_WORKFLOW.md +10 -11
- package/docs/COMMANDS.md +72 -11
- package/docs/CONNECTIONS.md +74 -7
- package/docs/MIGRATION.md +56 -22
- package/docs/NODE_GUIDE.md +52 -14
- package/docs/REFERENCE_MAP.md +29 -10
- package/docs/RELEASE.md +18 -5
- package/docs/RULES.md +33 -0
- package/docs/TELEMETRY.md +55 -0
- package/docs/TESTING.md +30 -16
- package/docs/TROUBLESHOOTING.md +44 -5
- package/examples/README.md +40 -12
- package/examples/bullmq_features.json +218 -16
- package/examples/example_flow.json +11 -11
- package/examples/postgres_backend.json +149 -0
- package/examples/repeatable_jobs.json +51 -19
- package/examples/scheduled_notifications.json +216 -0
- package/lib/acknowledgements.js +19 -1
- package/lib/commands.js +74 -35
- package/lib/connections.js +271 -48
- package/lib/scheduler.js +5 -74
- package/lib/serialization.js +1 -4
- package/package.json +25 -9
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
"type": "tab",
|
|
5
5
|
"label": "BullMQ repeatable jobs",
|
|
6
6
|
"disabled": false,
|
|
7
|
-
"info": "Dedicated
|
|
7
|
+
"info": "Dedicated examples for the native BullMQ v6 Job Scheduler commands."
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"id": "queue-repeatable-jobs",
|
|
11
|
-
"type": "
|
|
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": "
|
|
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": "
|
|
50
|
-
"func": "msg.cmd = \"
|
|
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": "
|
|
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": "
|
|
82
|
-
"func": "msg.cmd = \"
|
|
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": "
|
|
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": "
|
|
114
|
-
"func": "msg.cmd = \"
|
|
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": "
|
|
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": "
|
|
146
|
-
"func": "msg.cmd = \"
|
|
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": "
|
|
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": "
|
|
178
|
-
"func": "msg.cmd = \"
|
|
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": "
|
|
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": "
|
|
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
|
+
]
|
package/lib/acknowledgements.js
CHANGED
|
@@ -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 ||
|
|
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
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
255
|
+
return queue.addJobLog(msg.jobId, msg.logRow, msg.keepLogs);
|
|
219
256
|
case "getJobLogs":
|
|
220
257
|
return queue.getJobLogs(
|
|
221
|
-
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
|
|
268
|
+
throw new Error(`Unsupported bullmq cmd: ${cmd}`);
|
|
230
269
|
}
|
|
231
270
|
}
|
|
232
271
|
|