@pauldeng/node-red-contrib-bullmq 1.0.3 → 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 +61 -38
- package/bull-queue.html +241 -73
- package/bull-queue.js +623 -120
- package/docs/ARCHITECTURE.md +62 -18
- package/docs/CHANGE_WORKFLOW.md +10 -11
- package/docs/COMMANDS.md +72 -11
- package/docs/CONNECTIONS.md +62 -5
- package/docs/MIGRATION.md +56 -22
- package/docs/NODE_GUIDE.md +51 -16
- package/docs/REFERENCE_MAP.md +27 -10
- package/docs/RELEASE.md +17 -4
- 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 +212 -26
- package/lib/scheduler.js +5 -74
- package/lib/serialization.js +1 -4
- package/package.json +24 -8
package/lib/connections.js
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
const { isIP } = require("node:net");
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Shared by both backends; "localhost" is the right default either way.
|
|
6
|
+
const DEFAULT_HOST = "localhost";
|
|
6
7
|
const DEFAULT_REDIS_PORT = 6379;
|
|
7
8
|
const CLUSTER_PREFIX = "{bull}";
|
|
9
|
+
const DEFAULT_POSTGRES_PORT = 5432;
|
|
10
|
+
const DEFAULT_POSTGRES_POOL_MAX = 2;
|
|
11
|
+
// The only fail-fast lever on the PostgreSQL path (skipWaitingForReady is
|
|
12
|
+
// Redis-only) -- matches the Redis path's connectTimeout.
|
|
13
|
+
const POSTGRES_CONNECTION_TIMEOUT_MS = 10000;
|
|
14
|
+
const VALID_BACKENDS = ["redis", "postgres"];
|
|
8
15
|
|
|
9
16
|
function isPresent(value) {
|
|
10
17
|
return value !== undefined && value !== null && value !== "";
|
|
@@ -23,17 +30,44 @@ function toBoolean(value, defaultValue = false) {
|
|
|
23
30
|
return ["true", "1", "yes", "on"].includes(String(value).toLowerCase());
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
function toPort(value, defaultValue = DEFAULT_REDIS_PORT) {
|
|
33
|
+
function toPort(value, defaultValue = DEFAULT_REDIS_PORT, label = "Redis") {
|
|
27
34
|
if (!isPresent(value)) {
|
|
28
35
|
return defaultValue;
|
|
29
36
|
}
|
|
30
37
|
const port = Number(value);
|
|
31
38
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
32
|
-
throw new Error(`Invalid
|
|
39
|
+
throw new Error(`Invalid ${label} port: ${value}`);
|
|
33
40
|
}
|
|
34
41
|
return port;
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
// A saved 2.0.0 flow has no backend property at all, so absent/blank MUST
|
|
45
|
+
// mean "redis" -- that is the only way an existing flow keeps working
|
|
46
|
+
// untouched.
|
|
47
|
+
function normalizeBackend(config) {
|
|
48
|
+
if (!isPresent(config.backend)) {
|
|
49
|
+
return "redis";
|
|
50
|
+
}
|
|
51
|
+
const backend = String(config.backend).trim().toLowerCase();
|
|
52
|
+
if (!VALID_BACKENDS.includes(backend)) {
|
|
53
|
+
throw new Error(`Unsupported backend: ${config.backend}`);
|
|
54
|
+
}
|
|
55
|
+
return backend;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toPoolMax(value) {
|
|
59
|
+
if (!isPresent(value)) {
|
|
60
|
+
return DEFAULT_POSTGRES_POOL_MAX;
|
|
61
|
+
}
|
|
62
|
+
const max = Number(value);
|
|
63
|
+
if (!Number.isInteger(max) || max < 1) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"PostgreSQL pool max must be a positive whole number of connections",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return max;
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
function parseEndpoint(
|
|
38
72
|
endpoint,
|
|
39
73
|
defaultPort = DEFAULT_REDIS_PORT,
|
|
@@ -116,27 +150,87 @@ function parseEndpointList(
|
|
|
116
150
|
.map((item) => parseEndpoint(item, defaultPort, tls, tlsName));
|
|
117
151
|
}
|
|
118
152
|
|
|
119
|
-
function readSecret(
|
|
120
|
-
|
|
121
|
-
|
|
153
|
+
function readSecret(credentials, name) {
|
|
154
|
+
return credentials && isPresent(credentials[name])
|
|
155
|
+
? credentials[name]
|
|
156
|
+
: undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Blank means "keep every job", which is BullMQ's own default and grows
|
|
160
|
+
// without bound. A count keeps the newest N and is what production needs.
|
|
161
|
+
function toKeepCount(value, name) {
|
|
162
|
+
if (!isPresent(value)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const count = Number(value);
|
|
166
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`${name} must be a non-negative whole number of jobs to keep`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return count;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildDefaultJobOptions(config) {
|
|
175
|
+
const defaults = {};
|
|
176
|
+
const removeOnComplete = toKeepCount(
|
|
177
|
+
config.removeOnComplete,
|
|
178
|
+
"removeOnComplete",
|
|
179
|
+
);
|
|
180
|
+
const removeOnFail = toKeepCount(config.removeOnFail, "removeOnFail");
|
|
181
|
+
if (removeOnComplete !== undefined) {
|
|
182
|
+
defaults.removeOnComplete = removeOnComplete;
|
|
122
183
|
}
|
|
123
|
-
|
|
184
|
+
if (removeOnFail !== undefined) {
|
|
185
|
+
defaults.removeOnFail = removeOnFail;
|
|
186
|
+
}
|
|
187
|
+
return Object.keys(defaults).length > 0 ? defaults : undefined;
|
|
124
188
|
}
|
|
125
189
|
|
|
126
190
|
function normalizeQueueConfig(config = {}, credentials = {}) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
191
|
+
const removedAlias = [
|
|
192
|
+
"mode",
|
|
193
|
+
"redisMode",
|
|
194
|
+
"host",
|
|
195
|
+
"queueName",
|
|
196
|
+
"startupNodes",
|
|
197
|
+
"masterName",
|
|
198
|
+
"nameOfMaster",
|
|
199
|
+
].find((name) => isPresent(config[name]));
|
|
200
|
+
if (removedAlias) {
|
|
201
|
+
throw new Error(`Unsupported config field: ${removedAlias}`);
|
|
202
|
+
}
|
|
203
|
+
const plaintextSecret = [
|
|
204
|
+
"password",
|
|
205
|
+
"sentinelPassword",
|
|
206
|
+
"tlsCa",
|
|
207
|
+
"tlsCert",
|
|
208
|
+
"tlsKey",
|
|
209
|
+
].find((name) => isPresent(config[name]));
|
|
210
|
+
if (plaintextSecret) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`${plaintextSecret} must be stored in Node-RED credentials`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Validate only the fields belonging to the selected backend. Hidden
|
|
217
|
+
// editor rows keep their values, so a flow switched to postgres still
|
|
218
|
+
// carries deployment/clusterNodes/prefix in its saved JSON -- rejecting
|
|
219
|
+
// those would make switching backends impossible without hand-editing
|
|
220
|
+
// flows.
|
|
221
|
+
if (normalizeBackend(config) === "postgres") {
|
|
222
|
+
return normalizePostgresConfig(config, credentials);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const deployment = String(config.deployment || "single").toLowerCase();
|
|
132
226
|
|
|
133
227
|
if (!["single", "cluster", "sentinel"].includes(deployment)) {
|
|
134
|
-
throw new Error(`Unsupported Redis deployment mode: ${
|
|
228
|
+
throw new Error(`Unsupported Redis deployment mode: ${deployment}`);
|
|
135
229
|
}
|
|
136
230
|
|
|
137
|
-
const host = config.
|
|
231
|
+
const host = config.address || DEFAULT_HOST;
|
|
138
232
|
const port = toPort(config.port, DEFAULT_REDIS_PORT);
|
|
139
|
-
const queueName = String(config.
|
|
233
|
+
const queueName = String(config.name || "").trim();
|
|
140
234
|
if (!queueName) {
|
|
141
235
|
throw new Error("BullMQ queue name is required");
|
|
142
236
|
}
|
|
@@ -150,22 +244,22 @@ function normalizeQueueConfig(config = {}, credentials = {}) {
|
|
|
150
244
|
port,
|
|
151
245
|
db: isPresent(config.db) ? Number(config.db) : undefined,
|
|
152
246
|
username: config.username || undefined,
|
|
153
|
-
password: readSecret(
|
|
247
|
+
password: readSecret(credentials, "password"),
|
|
154
248
|
tls,
|
|
155
249
|
tlsRejectUnauthorized: toBoolean(config.tlsRejectUnauthorized, true),
|
|
156
|
-
tlsCa: readSecret(
|
|
157
|
-
tlsCert: readSecret(
|
|
158
|
-
tlsKey: readSecret(
|
|
250
|
+
tlsCa: readSecret(credentials, "tlsCa"),
|
|
251
|
+
tlsCert: readSecret(credentials, "tlsCert"),
|
|
252
|
+
tlsKey: readSecret(credentials, "tlsKey"),
|
|
159
253
|
tlsServerName: config.tlsServerName || undefined,
|
|
160
254
|
prefix: config.prefix || undefined,
|
|
255
|
+
defaultJobOptions: buildDefaultJobOptions(config),
|
|
161
256
|
clusterNodes: parseEndpointList(
|
|
162
|
-
config.clusterNodes
|
|
257
|
+
config.clusterNodes,
|
|
163
258
|
DEFAULT_REDIS_PORT,
|
|
164
259
|
tls,
|
|
165
260
|
"TLS",
|
|
166
261
|
),
|
|
167
|
-
sentinelMasterName:
|
|
168
|
-
config.sentinelMasterName || config.masterName || config.nameOfMaster,
|
|
262
|
+
sentinelMasterName: config.sentinelMasterName,
|
|
169
263
|
sentinels: parseEndpointList(
|
|
170
264
|
config.sentinels,
|
|
171
265
|
26379,
|
|
@@ -173,9 +267,11 @@ function normalizeQueueConfig(config = {}, credentials = {}) {
|
|
|
173
267
|
"Sentinel TLS",
|
|
174
268
|
),
|
|
175
269
|
sentinelUsername: config.sentinelUsername || undefined,
|
|
176
|
-
sentinelPassword:
|
|
177
|
-
readSecret(config, credentials, "sentinelPassword") || undefined,
|
|
270
|
+
sentinelPassword: readSecret(credentials, "sentinelPassword"),
|
|
178
271
|
sentinelTls,
|
|
272
|
+
telemetry: toBoolean(config.telemetry, false),
|
|
273
|
+
telemetryServiceName: config.telemetryServiceName || undefined,
|
|
274
|
+
telemetryMetrics: toBoolean(config.telemetryMetrics, false),
|
|
179
275
|
};
|
|
180
276
|
|
|
181
277
|
if (deployment === "cluster") {
|
|
@@ -229,8 +325,73 @@ function buildTlsOptions(config, enabled) {
|
|
|
229
325
|
return tls;
|
|
230
326
|
}
|
|
231
327
|
|
|
328
|
+
// Produces the pg pool config BullMQ's createPostgresBackend will receive as
|
|
329
|
+
// `connection`. Only the fields PostgreSQL needs are read; every Redis-only
|
|
330
|
+
// field a switched flow still carries in its saved JSON (deployment,
|
|
331
|
+
// clusterNodes, sentinels, prefix, ...) is simply never touched here, so it
|
|
332
|
+
// is ignored rather than rejected.
|
|
333
|
+
function normalizePostgresConfig(config, credentials) {
|
|
334
|
+
const queueName = String(config.name || "").trim();
|
|
335
|
+
if (!queueName) {
|
|
336
|
+
throw new Error("BullMQ queue name is required");
|
|
337
|
+
}
|
|
338
|
+
const tls = toBoolean(config.tls, false);
|
|
339
|
+
const ssl = buildTlsOptions(
|
|
340
|
+
{
|
|
341
|
+
tlsRejectUnauthorized: toBoolean(config.tlsRejectUnauthorized, true),
|
|
342
|
+
tlsServerName: config.tlsServerName || undefined,
|
|
343
|
+
tlsCa: readSecret(credentials, "tlsCa"),
|
|
344
|
+
tlsCert: readSecret(credentials, "tlsCert"),
|
|
345
|
+
tlsKey: readSecret(credentials, "tlsKey"),
|
|
346
|
+
},
|
|
347
|
+
tls,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const postgres = {
|
|
351
|
+
host: String(config.address || DEFAULT_HOST).trim(),
|
|
352
|
+
port: toPort(config.port, DEFAULT_POSTGRES_PORT, "PostgreSQL"),
|
|
353
|
+
database: config.database || undefined,
|
|
354
|
+
user: config.username || undefined,
|
|
355
|
+
password: readSecret(credentials, "password"),
|
|
356
|
+
schema: config.schema || undefined,
|
|
357
|
+
max: toPoolMax(config.max),
|
|
358
|
+
connectionTimeoutMillis: POSTGRES_CONNECTION_TIMEOUT_MS,
|
|
359
|
+
migrate: toBoolean(config.migrate, true),
|
|
360
|
+
};
|
|
361
|
+
if (ssl) {
|
|
362
|
+
postgres.ssl = ssl;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
backend: "postgres",
|
|
367
|
+
queueName,
|
|
368
|
+
postgres,
|
|
369
|
+
defaultJobOptions: buildDefaultJobOptions(config),
|
|
370
|
+
telemetry: toBoolean(config.telemetry, false),
|
|
371
|
+
telemetryServiceName: config.telemetryServiceName || undefined,
|
|
372
|
+
telemetryMetrics: toBoolean(config.telemetryMetrics, false),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// BullMQ's production guidance: exponential reconnect backoff with a 1s floor
|
|
377
|
+
// and a 20s ceiling, rather than ioredis's default 50ms-2s, which hammers a
|
|
378
|
+
// down Redis once per connection role per runtime node.
|
|
379
|
+
const RECONNECT_FLOOR_MS = 1000;
|
|
380
|
+
const RECONNECT_CEILING_MS = 20000;
|
|
381
|
+
|
|
382
|
+
function reconnectBackoff(attempt) {
|
|
383
|
+
return Math.min(
|
|
384
|
+
RECONNECT_FLOOR_MS * 2 ** (attempt - 1),
|
|
385
|
+
RECONNECT_CEILING_MS,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isConsumerRole(role) {
|
|
390
|
+
return role === "worker" || role === "events";
|
|
391
|
+
}
|
|
392
|
+
|
|
232
393
|
function retryValueForRole(role) {
|
|
233
|
-
return role
|
|
394
|
+
return isConsumerRole(role) ? null : 1;
|
|
234
395
|
}
|
|
235
396
|
|
|
236
397
|
function buildStandaloneOptions(config, role) {
|
|
@@ -240,8 +401,16 @@ function buildStandaloneOptions(config, role) {
|
|
|
240
401
|
maxRetriesPerRequest: retryValueForRole(role),
|
|
241
402
|
enableReadyCheck: true,
|
|
242
403
|
connectTimeout: 10000,
|
|
404
|
+
retryStrategy: reconnectBackoff,
|
|
243
405
|
};
|
|
244
406
|
|
|
407
|
+
// Deliberately NOT enableOfflineQueue:false, which BullMQ's production guide
|
|
408
|
+
// suggests for producers. Measured against a healthy Redis, it rejects a
|
|
409
|
+
// command issued during the connect window -- which in Node-RED is every
|
|
410
|
+
// deploy-time message. Keeping the offline queue accepts those, while
|
|
411
|
+
// maxRetriesPerRequest above still fails a genuinely-down Redis in about a
|
|
412
|
+
// second instead of hanging.
|
|
413
|
+
|
|
245
414
|
if (config.db !== undefined && !Number.isNaN(config.db)) {
|
|
246
415
|
options.db = config.db;
|
|
247
416
|
}
|
|
@@ -265,6 +434,7 @@ function buildRedisDescriptor(config, role = "producer") {
|
|
|
265
434
|
maxRetriesPerRequest: retryValueForRole(role),
|
|
266
435
|
enableReadyCheck: true,
|
|
267
436
|
connectTimeout: 10000,
|
|
437
|
+
retryStrategy: reconnectBackoff,
|
|
268
438
|
};
|
|
269
439
|
if (config.username) {
|
|
270
440
|
redisOptions.username = config.username;
|
|
@@ -285,6 +455,7 @@ function buildRedisDescriptor(config, role = "producer") {
|
|
|
285
455
|
slotsRefreshTimeout: 2000,
|
|
286
456
|
maxRedirections: 16,
|
|
287
457
|
dnsLookup: (address, callback) => callback(null, address),
|
|
458
|
+
clusterRetryStrategy: reconnectBackoff,
|
|
288
459
|
},
|
|
289
460
|
};
|
|
290
461
|
}
|
|
@@ -298,6 +469,7 @@ function buildRedisDescriptor(config, role = "producer") {
|
|
|
298
469
|
}
|
|
299
470
|
options.sentinels = config.sentinels;
|
|
300
471
|
options.name = config.sentinelMasterName;
|
|
472
|
+
options.sentinelRetryStrategy = reconnectBackoff;
|
|
301
473
|
if (config.sentinelUsername) {
|
|
302
474
|
options.sentinelUsername = config.sentinelUsername;
|
|
303
475
|
}
|
|
@@ -322,7 +494,7 @@ function createRedisConnection(descriptor, IORedis) {
|
|
|
322
494
|
return new IORedis(descriptor.options);
|
|
323
495
|
}
|
|
324
496
|
|
|
325
|
-
function buildBullMQOptions(config, connection) {
|
|
497
|
+
function buildBullMQOptions(config, connection, telemetry, role = "producer") {
|
|
326
498
|
const options = {};
|
|
327
499
|
if (connection) {
|
|
328
500
|
options.connection = connection;
|
|
@@ -330,13 +502,27 @@ function buildBullMQOptions(config, connection) {
|
|
|
330
502
|
if (config.prefix) {
|
|
331
503
|
options.prefix = config.prefix;
|
|
332
504
|
}
|
|
505
|
+
if (telemetry) {
|
|
506
|
+
options.telemetry = telemetry;
|
|
507
|
+
}
|
|
508
|
+
// Without this, BullMQ awaits a connection-ready promise that never settles
|
|
509
|
+
// while Redis is unreachable, so a producer command hangs forever instead of
|
|
510
|
+
// erroring. Consumers must keep waiting for the connection to come back.
|
|
511
|
+
// Only createRedisBackend forwards skipWaitingForReady (verified in
|
|
512
|
+
// node_modules/bullmq/dist/cjs/utils/create-backend.js) -- createPostgresBackend
|
|
513
|
+
// never reads it, so setting it there would be a misleading no-op.
|
|
514
|
+
if (!isConsumerRole(role) && config.backend !== "postgres") {
|
|
515
|
+
options.skipWaitingForReady = true;
|
|
516
|
+
}
|
|
333
517
|
return options;
|
|
334
518
|
}
|
|
335
519
|
|
|
336
520
|
module.exports = {
|
|
521
|
+
POSTGRES_CONNECTION_TIMEOUT_MS,
|
|
337
522
|
buildBullMQOptions,
|
|
338
523
|
buildRedisDescriptor,
|
|
339
524
|
createRedisConnection,
|
|
340
525
|
normalizeQueueConfig,
|
|
526
|
+
normalizePostgresConfig,
|
|
341
527
|
parseEndpointList,
|
|
342
528
|
};
|
package/lib/scheduler.js
CHANGED
|
@@ -4,78 +4,8 @@ function hasOwn(object, key) {
|
|
|
4
4
|
return Object.prototype.hasOwnProperty.call(object, key);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
return value;
|
|
10
|
-
}
|
|
11
|
-
return { ...value };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function normalizeRepeatOptions(repeat) {
|
|
15
|
-
const normalized = clonePlain(repeat) || {};
|
|
16
|
-
if (
|
|
17
|
-
normalized.cron &&
|
|
18
|
-
normalized.pattern &&
|
|
19
|
-
normalized.cron !== normalized.pattern
|
|
20
|
-
) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
"repeat.cron and repeat.pattern must match when both are supplied",
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
if (normalized.cron) {
|
|
26
|
-
normalized.pattern = normalized.cron;
|
|
27
|
-
delete normalized.cron;
|
|
28
|
-
}
|
|
29
|
-
return normalized;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getJobData(msg) {
|
|
33
|
-
if (hasOwn(msg, "jobData")) {
|
|
34
|
-
return msg.jobData;
|
|
35
|
-
}
|
|
36
|
-
return { payload: msg.payload };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function normalizeAddRequest(msg = {}) {
|
|
40
|
-
const jobopts = clonePlain(msg.jobopts) || {};
|
|
41
|
-
const name = msg.jobName || "default";
|
|
42
|
-
|
|
43
|
-
if (jobopts.repeat) {
|
|
44
|
-
const schedulerId = String(msg.schedulerId || jobopts.jobId || "").trim();
|
|
45
|
-
if (!schedulerId) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
"scheduled jobs require msg.schedulerId or msg.jobopts.jobId",
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const repeat = normalizeRepeatOptions(jobopts.repeat);
|
|
52
|
-
delete jobopts.repeat;
|
|
53
|
-
delete jobopts.jobId;
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
kind: "scheduler",
|
|
57
|
-
schedulerId,
|
|
58
|
-
repeat,
|
|
59
|
-
template: {
|
|
60
|
-
name,
|
|
61
|
-
data: getJobData(msg),
|
|
62
|
-
opts: jobopts,
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
kind: "job",
|
|
69
|
-
name,
|
|
70
|
-
data: getJobData(msg),
|
|
71
|
-
opts: jobopts,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function getLegacySchedulerId(msg = {}) {
|
|
76
|
-
const schedulerId = String(
|
|
77
|
-
msg.schedulerId || msg.jobid || msg.jobId || "",
|
|
78
|
-
).trim();
|
|
7
|
+
function getSchedulerId(msg = {}) {
|
|
8
|
+
const schedulerId = String(msg.schedulerId || "").trim();
|
|
79
9
|
if (!schedulerId) {
|
|
80
10
|
throw new Error("A scheduler id is required");
|
|
81
11
|
}
|
|
@@ -99,6 +29,8 @@ function serializeScheduler(scheduler) {
|
|
|
99
29
|
"offset",
|
|
100
30
|
"tz",
|
|
101
31
|
"endDate",
|
|
32
|
+
"startDate",
|
|
33
|
+
"iterationCount",
|
|
102
34
|
]) {
|
|
103
35
|
if (hasOwn(scheduler, key)) {
|
|
104
36
|
result[key] = scheduler[key];
|
|
@@ -108,7 +40,6 @@ function serializeScheduler(scheduler) {
|
|
|
108
40
|
}
|
|
109
41
|
|
|
110
42
|
module.exports = {
|
|
111
|
-
|
|
112
|
-
normalizeAddRequest,
|
|
43
|
+
getSchedulerId,
|
|
113
44
|
serializeScheduler,
|
|
114
45
|
};
|
package/lib/serialization.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
function hasOwn(object, key) {
|
|
4
|
-
return Object.prototype.hasOwnProperty.call(object, key);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
3
|
function copyKnownFields(source, fields) {
|
|
8
4
|
if (!source) {
|
|
9
5
|
return source;
|
|
@@ -31,6 +27,7 @@ function serializeJob(job) {
|
|
|
31
27
|
"opts",
|
|
32
28
|
"progress",
|
|
33
29
|
"attemptsMade",
|
|
30
|
+
"attemptsStarted",
|
|
34
31
|
"failedReason",
|
|
35
32
|
"stacktrace",
|
|
36
33
|
"returnvalue",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pauldeng/node-red-contrib-bullmq",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "BullMQ
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "BullMQ job queue nodes for Node-RED, backed by Redis or PostgreSQL",
|
|
5
5
|
"main": "bull-queue.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"bull-queue.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"docs/*.md"
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=22.9"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "node --test test/*.test.js",
|
|
@@ -25,9 +25,21 @@
|
|
|
25
25
|
"validate": "npx --yes node-red-dev validate"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"bullmq": "
|
|
28
|
+
"bullmq": "6.3.1",
|
|
29
29
|
"ioredis": "5.11.1"
|
|
30
30
|
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"bullmq-otel": ">=2.0.0",
|
|
33
|
+
"pg": ">=8.0.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"bullmq-otel": {
|
|
37
|
+
"optional": true
|
|
38
|
+
},
|
|
39
|
+
"pg": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
31
43
|
"license": "MIT",
|
|
32
44
|
"keywords": [
|
|
33
45
|
"node-red",
|
|
@@ -36,11 +48,13 @@
|
|
|
36
48
|
"queue",
|
|
37
49
|
"bull",
|
|
38
50
|
"redis",
|
|
51
|
+
"postgres",
|
|
52
|
+
"postgresql",
|
|
39
53
|
"worker",
|
|
40
54
|
"jobs"
|
|
41
55
|
],
|
|
42
56
|
"node-red": {
|
|
43
|
-
"version": ">=
|
|
57
|
+
"version": ">=5.0.0 <6",
|
|
44
58
|
"nodes": {
|
|
45
59
|
"bull-queue": "bull-queue.js"
|
|
46
60
|
}
|
|
@@ -62,9 +76,11 @@
|
|
|
62
76
|
"email": "dengpeng.cn@gmail.com"
|
|
63
77
|
},
|
|
64
78
|
"devDependencies": {
|
|
65
|
-
"@playwright/test": "1.
|
|
66
|
-
"
|
|
79
|
+
"@playwright/test": "1.62.1",
|
|
80
|
+
"bullmq-otel": "2.0.1",
|
|
81
|
+
"node-red": "5.0.4",
|
|
67
82
|
"node-red-node-test-helper": "^0.3.6",
|
|
68
|
-
"
|
|
83
|
+
"pg": "8.23.0",
|
|
84
|
+
"prettier": "3.9.6"
|
|
69
85
|
}
|
|
70
86
|
}
|