@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.
@@ -1,8 +1,17 @@
1
1
  "use strict";
2
2
 
3
- const DEFAULT_REDIS_HOST = "localhost";
3
+ const { isIP } = require("node:net");
4
+
5
+ // Shared by both backends; "localhost" is the right default either way.
6
+ const DEFAULT_HOST = "localhost";
4
7
  const DEFAULT_REDIS_PORT = 6379;
5
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"];
6
15
 
7
16
  function isPresent(value) {
8
17
  return value !== undefined && value !== null && value !== "";
@@ -21,18 +30,50 @@ function toBoolean(value, defaultValue = false) {
21
30
  return ["true", "1", "yes", "on"].includes(String(value).toLowerCase());
22
31
  }
23
32
 
24
- function toPort(value, defaultValue = DEFAULT_REDIS_PORT) {
33
+ function toPort(value, defaultValue = DEFAULT_REDIS_PORT, label = "Redis") {
25
34
  if (!isPresent(value)) {
26
35
  return defaultValue;
27
36
  }
28
37
  const port = Number(value);
29
38
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
30
- throw new Error(`Invalid Redis port: ${value}`);
39
+ throw new Error(`Invalid ${label} port: ${value}`);
31
40
  }
32
41
  return port;
33
42
  }
34
43
 
35
- function parseEndpoint(endpoint, defaultPort = DEFAULT_REDIS_PORT) {
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
+
71
+ function parseEndpoint(
72
+ endpoint,
73
+ defaultPort = DEFAULT_REDIS_PORT,
74
+ tls,
75
+ tlsName = "TLS",
76
+ ) {
36
77
  if (typeof endpoint === "object" && endpoint !== null) {
37
78
  const host = endpoint.host || endpoint.address;
38
79
  if (!isPresent(host)) {
@@ -48,36 +89,54 @@ function parseEndpoint(endpoint, defaultPort = DEFAULT_REDIS_PORT) {
48
89
  if (!text) {
49
90
  throw new Error("Redis endpoint cannot be empty");
50
91
  }
92
+ if (isIP(text)) {
93
+ return { host: text, port: defaultPort };
94
+ }
51
95
 
52
- let host = text;
53
- let port = defaultPort;
54
- const urlMatch = text.match(
55
- /^(?:redis|rediss):\/\/(?:[^@]+@)?([^/:]+)(?::(\d+))?/i,
56
- );
57
- if (urlMatch) {
58
- host = urlMatch[1];
59
- port = toPort(urlMatch[2], defaultPort);
60
- } else if (text.includes(":")) {
61
- const parts = text.split(":");
62
- port = toPort(parts.pop(), defaultPort);
63
- host = parts.join(":");
96
+ const hasScheme = /^[a-z][a-z\d+.-]*:\/\//i.test(text);
97
+ let url;
98
+ try {
99
+ url = new URL(hasScheme ? text : `redis://${text}`);
100
+ } catch {
101
+ throw new Error(`Invalid Redis endpoint: ${text}`);
102
+ }
103
+ if (url.protocol !== "redis:" && url.protocol !== "rediss:") {
104
+ throw new Error(`Unsupported Redis endpoint protocol: ${url.protocol}`);
105
+ }
106
+ if (url.username || url.password) {
107
+ throw new Error(
108
+ "Redis endpoint URLs cannot include credentials; use the config node credential fields",
109
+ );
110
+ }
111
+ if (hasScheme && tls !== undefined) {
112
+ const secure = url.protocol === "rediss:";
113
+ if (secure !== tls) {
114
+ throw new Error(
115
+ `${url.protocol}// endpoint requires ${tlsName} to be ${secure ? "enabled" : "disabled"}`,
116
+ );
117
+ }
64
118
  }
65
119
 
66
- if (!host.trim()) {
120
+ const host = url.hostname.replace(/^\[|\]$/g, "");
121
+ if (!host) {
67
122
  throw new Error(`Redis endpoint requires a host: ${text}`);
68
123
  }
69
-
70
- return { host: host.trim(), port };
124
+ return { host, port: toPort(url.port, defaultPort) };
71
125
  }
72
126
 
73
- function parseEndpointList(value, defaultPort = DEFAULT_REDIS_PORT) {
127
+ function parseEndpointList(
128
+ value,
129
+ defaultPort = DEFAULT_REDIS_PORT,
130
+ tls,
131
+ tlsName = "TLS",
132
+ ) {
74
133
  if (Array.isArray(value)) {
75
134
  return value
76
135
  .flatMap((item) =>
77
136
  typeof item === "string" ? item.split(/[\n,]+/) : [item],
78
137
  )
79
138
  .filter((item) => isPresent(item))
80
- .map((item) => parseEndpoint(item, defaultPort));
139
+ .map((item) => parseEndpoint(item, defaultPort, tls, tlsName));
81
140
  }
82
141
 
83
142
  if (!isPresent(value)) {
@@ -88,33 +147,95 @@ function parseEndpointList(value, defaultPort = DEFAULT_REDIS_PORT) {
88
147
  .split(/[\n,]+/)
89
148
  .map((item) => item.trim())
90
149
  .filter(Boolean)
91
- .map((item) => parseEndpoint(item, defaultPort));
150
+ .map((item) => parseEndpoint(item, defaultPort, tls, tlsName));
151
+ }
152
+
153
+ function readSecret(credentials, name) {
154
+ return credentials && isPresent(credentials[name])
155
+ ? credentials[name]
156
+ : undefined;
92
157
  }
93
158
 
94
- function readSecret(config, credentials, name) {
95
- if (credentials && isPresent(credentials[name])) {
96
- return credentials[name];
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;
97
164
  }
98
- return config[name];
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;
183
+ }
184
+ if (removeOnFail !== undefined) {
185
+ defaults.removeOnFail = removeOnFail;
186
+ }
187
+ return Object.keys(defaults).length > 0 ? defaults : undefined;
99
188
  }
100
189
 
101
190
  function normalizeQueueConfig(config = {}, credentials = {}) {
102
- const deploymentInput = String(
103
- config.deployment || config.mode || config.redisMode || "single",
104
- ).toLowerCase();
105
- const deployment =
106
- deploymentInput === "memorydb" ? "cluster" : deploymentInput;
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();
107
226
 
108
227
  if (!["single", "cluster", "sentinel"].includes(deployment)) {
109
- throw new Error(`Unsupported Redis deployment mode: ${deploymentInput}`);
228
+ throw new Error(`Unsupported Redis deployment mode: ${deployment}`);
110
229
  }
111
230
 
112
- const host = config.host || config.address || DEFAULT_REDIS_HOST;
231
+ const host = config.address || DEFAULT_HOST;
113
232
  const port = toPort(config.port, DEFAULT_REDIS_PORT);
114
- const queueName = String(config.queueName || config.name || "").trim();
233
+ const queueName = String(config.name || "").trim();
115
234
  if (!queueName) {
116
235
  throw new Error("BullMQ queue name is required");
117
236
  }
237
+ const tls = toBoolean(config.tls, false);
238
+ const sentinelTls = toBoolean(config.sentinelTls, false);
118
239
 
119
240
  const normalized = {
120
241
  queueName,
@@ -123,22 +244,34 @@ function normalizeQueueConfig(config = {}, credentials = {}) {
123
244
  port,
124
245
  db: isPresent(config.db) ? Number(config.db) : undefined,
125
246
  username: config.username || undefined,
126
- password: readSecret(config, credentials, "password") || undefined,
127
- tls: toBoolean(config.tls, false),
247
+ password: readSecret(credentials, "password"),
248
+ tls,
128
249
  tlsRejectUnauthorized: toBoolean(config.tlsRejectUnauthorized, true),
129
- tlsCa: readSecret(config, credentials, "tlsCa") || undefined,
130
- tlsCert: readSecret(config, credentials, "tlsCert") || undefined,
131
- tlsKey: readSecret(config, credentials, "tlsKey") || undefined,
250
+ tlsCa: readSecret(credentials, "tlsCa"),
251
+ tlsCert: readSecret(credentials, "tlsCert"),
252
+ tlsKey: readSecret(credentials, "tlsKey"),
132
253
  tlsServerName: config.tlsServerName || undefined,
133
254
  prefix: config.prefix || undefined,
134
- clusterNodes: parseEndpointList(config.clusterNodes || config.startupNodes),
135
- sentinelMasterName:
136
- config.sentinelMasterName || config.masterName || config.nameOfMaster,
137
- sentinels: parseEndpointList(config.sentinels, 26379),
255
+ defaultJobOptions: buildDefaultJobOptions(config),
256
+ clusterNodes: parseEndpointList(
257
+ config.clusterNodes,
258
+ DEFAULT_REDIS_PORT,
259
+ tls,
260
+ "TLS",
261
+ ),
262
+ sentinelMasterName: config.sentinelMasterName,
263
+ sentinels: parseEndpointList(
264
+ config.sentinels,
265
+ 26379,
266
+ sentinelTls,
267
+ "Sentinel TLS",
268
+ ),
138
269
  sentinelUsername: config.sentinelUsername || undefined,
139
- sentinelPassword:
140
- readSecret(config, credentials, "sentinelPassword") || undefined,
141
- sentinelTls: toBoolean(config.sentinelTls, false),
270
+ sentinelPassword: readSecret(credentials, "sentinelPassword"),
271
+ sentinelTls,
272
+ telemetry: toBoolean(config.telemetry, false),
273
+ telemetryServiceName: config.telemetryServiceName || undefined,
274
+ telemetryMetrics: toBoolean(config.telemetryMetrics, false),
142
275
  };
143
276
 
144
277
  if (deployment === "cluster") {
@@ -192,8 +325,73 @@ function buildTlsOptions(config, enabled) {
192
325
  return tls;
193
326
  }
194
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
+
195
393
  function retryValueForRole(role) {
196
- return role === "worker" || role === "events" ? null : 1;
394
+ return isConsumerRole(role) ? null : 1;
197
395
  }
198
396
 
199
397
  function buildStandaloneOptions(config, role) {
@@ -203,8 +401,16 @@ function buildStandaloneOptions(config, role) {
203
401
  maxRetriesPerRequest: retryValueForRole(role),
204
402
  enableReadyCheck: true,
205
403
  connectTimeout: 10000,
404
+ retryStrategy: reconnectBackoff,
206
405
  };
207
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
+
208
414
  if (config.db !== undefined && !Number.isNaN(config.db)) {
209
415
  options.db = config.db;
210
416
  }
@@ -228,6 +434,7 @@ function buildRedisDescriptor(config, role = "producer") {
228
434
  maxRetriesPerRequest: retryValueForRole(role),
229
435
  enableReadyCheck: true,
230
436
  connectTimeout: 10000,
437
+ retryStrategy: reconnectBackoff,
231
438
  };
232
439
  if (config.username) {
233
440
  redisOptions.username = config.username;
@@ -248,6 +455,7 @@ function buildRedisDescriptor(config, role = "producer") {
248
455
  slotsRefreshTimeout: 2000,
249
456
  maxRedirections: 16,
250
457
  dnsLookup: (address, callback) => callback(null, address),
458
+ clusterRetryStrategy: reconnectBackoff,
251
459
  },
252
460
  };
253
461
  }
@@ -261,6 +469,7 @@ function buildRedisDescriptor(config, role = "producer") {
261
469
  }
262
470
  options.sentinels = config.sentinels;
263
471
  options.name = config.sentinelMasterName;
472
+ options.sentinelRetryStrategy = reconnectBackoff;
264
473
  if (config.sentinelUsername) {
265
474
  options.sentinelUsername = config.sentinelUsername;
266
475
  }
@@ -285,7 +494,7 @@ function createRedisConnection(descriptor, IORedis) {
285
494
  return new IORedis(descriptor.options);
286
495
  }
287
496
 
288
- function buildBullMQOptions(config, connection) {
497
+ function buildBullMQOptions(config, connection, telemetry, role = "producer") {
289
498
  const options = {};
290
499
  if (connection) {
291
500
  options.connection = connection;
@@ -293,13 +502,27 @@ function buildBullMQOptions(config, connection) {
293
502
  if (config.prefix) {
294
503
  options.prefix = config.prefix;
295
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
+ }
296
517
  return options;
297
518
  }
298
519
 
299
520
  module.exports = {
521
+ POSTGRES_CONNECTION_TIMEOUT_MS,
300
522
  buildBullMQOptions,
301
523
  buildRedisDescriptor,
302
524
  createRedisConnection,
303
525
  normalizeQueueConfig,
526
+ normalizePostgresConfig,
304
527
  parseEndpointList,
305
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 clonePlain(value) {
8
- if (!value || typeof value !== "object" || Array.isArray(value)) {
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
- getLegacySchedulerId,
112
- normalizeAddRequest,
43
+ getSchedulerId,
113
44
  serializeScheduler,
114
45
  };
@@ -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": "1.0.2",
4
- "description": "BullMQ-backed Redis job queue nodes for Node-RED",
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": ">=18"
15
+ "node": ">=22.9"
16
16
  },
17
17
  "scripts": {
18
18
  "test": "node --test test/*.test.js",
@@ -25,8 +25,20 @@
25
25
  "validate": "npx --yes node-red-dev validate"
26
26
  },
27
27
  "dependencies": {
28
- "bullmq": "5.80.2",
29
- "ioredis": "5.10.1"
28
+ "bullmq": "6.3.1",
29
+ "ioredis": "5.11.1"
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
+ }
30
42
  },
31
43
  "license": "MIT",
32
44
  "keywords": [
@@ -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": ">=4.1.0 <6",
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.61.1",
66
- "node-red": "5.0.1",
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
- "prettier": "3.9.4"
83
+ "pg": "8.23.0",
84
+ "prettier": "3.9.6"
69
85
  }
70
86
  }