groupmq-plus 1.1.0 → 1.1.2
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/dist/index.cjs +218 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -9
- package/dist/index.d.ts +62 -9
- package/dist/index.js +218 -10
- package/dist/index.js.map +1 -1
- package/dist/lua/change-delay.lua +20 -13
- package/dist/lua/clean-status.lua +13 -4
- package/dist/lua/complete-and-reserve-next-with-metadata.lua +8 -0
- package/dist/lua/complete-with-metadata.lua +8 -0
- package/dist/lua/enqueue-flow.lua +4 -0
- package/dist/lua/record-job-result.lua +10 -0
- package/dist/lua/remove.lua +13 -3
- package/dist/lua/reserve.lua +2 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -240,6 +240,7 @@ var Job = class Job {
|
|
|
240
240
|
this.timestamp = args.timestamp;
|
|
241
241
|
this.orderMs = args.orderMs;
|
|
242
242
|
this.status = args.status ?? "unknown";
|
|
243
|
+
this.parentId = args.parentId;
|
|
243
244
|
}
|
|
244
245
|
async getState() {
|
|
245
246
|
return this.status ?? "unknown";
|
|
@@ -281,6 +282,46 @@ var Job = class Job {
|
|
|
281
282
|
async update(jobData) {
|
|
282
283
|
await this.updateData(jobData);
|
|
283
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Wait until this job is completed or failed.
|
|
287
|
+
* @param timeoutMs Optional timeout in milliseconds (0 = no timeout)
|
|
288
|
+
*/
|
|
289
|
+
async waitUntilFinished(timeoutMs = 0) {
|
|
290
|
+
return this.queue.waitUntilFinished(this.id, timeoutMs);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get all child jobs of this job (if it's a parent in a flow).
|
|
294
|
+
* @returns Array of child Job instances
|
|
295
|
+
*/
|
|
296
|
+
async getChildren() {
|
|
297
|
+
return this.queue.getFlowChildren(this.id);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Get the return values of all child jobs in a flow.
|
|
301
|
+
* @returns Object mapping child job IDs to their return values
|
|
302
|
+
*/
|
|
303
|
+
async getChildrenValues() {
|
|
304
|
+
return this.queue.getFlowResults(this.id);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Get the number of remaining child jobs that haven't completed yet.
|
|
308
|
+
* @returns Number of remaining dependencies, or null if not a parent job
|
|
309
|
+
*/
|
|
310
|
+
async getDependenciesCount() {
|
|
311
|
+
return this.queue.getFlowDependencies(this.id);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get the parent job of this job (if it's a child in a flow).
|
|
315
|
+
* @returns Parent Job instance, or undefined if no parent or parent was deleted
|
|
316
|
+
*/
|
|
317
|
+
async getParent() {
|
|
318
|
+
if (!this.parentId) return void 0;
|
|
319
|
+
try {
|
|
320
|
+
return await this.queue.getJob(this.parentId);
|
|
321
|
+
} catch (_e) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
284
325
|
static fromReserved(queue, reserved, meta) {
|
|
285
326
|
return new Job({
|
|
286
327
|
queue,
|
|
@@ -320,6 +361,7 @@ var Job = class Job {
|
|
|
320
361
|
const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
|
|
321
362
|
const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
|
|
322
363
|
const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
|
|
364
|
+
const parentId = raw.parentId || void 0;
|
|
323
365
|
return new Job({
|
|
324
366
|
queue,
|
|
325
367
|
id,
|
|
@@ -338,7 +380,8 @@ var Job = class Job {
|
|
|
338
380
|
returnvalue,
|
|
339
381
|
timestamp: timestampMs || Date.now(),
|
|
340
382
|
orderMs,
|
|
341
|
-
status: knownStatus ?? coerceStatus(raw.status)
|
|
383
|
+
status: knownStatus ?? coerceStatus(raw.status),
|
|
384
|
+
parentId
|
|
342
385
|
});
|
|
343
386
|
}
|
|
344
387
|
static async fromStore(queue, id) {
|
|
@@ -357,6 +400,7 @@ var Job = class Job {
|
|
|
357
400
|
const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
|
|
358
401
|
const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
|
|
359
402
|
const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
|
|
403
|
+
const parentId = raw.parentId || void 0;
|
|
360
404
|
const [inProcessing, inDelayed] = await Promise.all([queue.redis.zscore(`${queue.namespace}:processing`, id), queue.redis.zscore(`${queue.namespace}:delayed`, id)]);
|
|
361
405
|
let status = raw.status;
|
|
362
406
|
if (inProcessing !== null) status = "active";
|
|
@@ -382,7 +426,8 @@ var Job = class Job {
|
|
|
382
426
|
returnvalue,
|
|
383
427
|
timestamp: timestampMs || Date.now(),
|
|
384
428
|
orderMs,
|
|
385
|
-
status: coerceStatus(status)
|
|
429
|
+
status: coerceStatus(status),
|
|
430
|
+
parentId
|
|
386
431
|
});
|
|
387
432
|
}
|
|
388
433
|
};
|
|
@@ -474,6 +519,8 @@ function safeJsonParse(input) {
|
|
|
474
519
|
var Queue = class {
|
|
475
520
|
constructor(opts) {
|
|
476
521
|
this._consecutiveEmptyReserves = 0;
|
|
522
|
+
this.eventsSubscribed = false;
|
|
523
|
+
this.waitingJobs = /* @__PURE__ */ new Map();
|
|
477
524
|
this.promoterRunning = false;
|
|
478
525
|
this.batchBuffer = [];
|
|
479
526
|
this.flushing = false;
|
|
@@ -628,6 +675,38 @@ var Queue = class {
|
|
|
628
675
|
}
|
|
629
676
|
return parsed;
|
|
630
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* Gets all child job IDs for a parent job in a flow.
|
|
680
|
+
* @param parentId The ID of the parent job
|
|
681
|
+
* @returns An array of child job IDs
|
|
682
|
+
*/
|
|
683
|
+
async getFlowChildrenIds(parentId) {
|
|
684
|
+
return this.r.smembers(`${this.ns}:flow:children:${parentId}`);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Gets all child jobs for a parent job in a flow.
|
|
688
|
+
* @param parentId The ID of the parent job
|
|
689
|
+
* @returns An array of Job instances for all children
|
|
690
|
+
*/
|
|
691
|
+
async getFlowChildren(parentId) {
|
|
692
|
+
const ids = await this.getFlowChildrenIds(parentId);
|
|
693
|
+
if (ids.length === 0) return [];
|
|
694
|
+
const pipe = this.r.multi();
|
|
695
|
+
for (const id of ids) pipe.hgetall(`${this.ns}:job:${id}`);
|
|
696
|
+
const rows = await pipe.exec();
|
|
697
|
+
const jobs = [];
|
|
698
|
+
for (let i = 0; i < ids.length; i++) {
|
|
699
|
+
const id = ids[i];
|
|
700
|
+
const raw = rows?.[i]?.[1] || {};
|
|
701
|
+
if (!raw || Object.keys(raw).length === 0) {
|
|
702
|
+
this.logger.warn(`Skipping child job ${id} - not found (likely cleaned up)`);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const job = Job.fromRawHash(this, id, raw);
|
|
706
|
+
jobs.push(job);
|
|
707
|
+
}
|
|
708
|
+
return jobs;
|
|
709
|
+
}
|
|
631
710
|
async addSingle(opts) {
|
|
632
711
|
const now = Date.now();
|
|
633
712
|
let delayUntil = 0;
|
|
@@ -1322,6 +1401,91 @@ var Queue = class {
|
|
|
1322
1401
|
async getJob(id) {
|
|
1323
1402
|
return Job.fromStore(this, id);
|
|
1324
1403
|
}
|
|
1404
|
+
async setupSubscriber() {
|
|
1405
|
+
if (this.eventsSubscribed && this.subscriber) return;
|
|
1406
|
+
if (!this.subscriber) {
|
|
1407
|
+
this.subscriber = this.r.duplicate();
|
|
1408
|
+
this.subscriber.on("message", (channel, message) => {
|
|
1409
|
+
if (channel === `${this.ns}:events`) this.handleJobEvent(message);
|
|
1410
|
+
});
|
|
1411
|
+
this.subscriber.on("error", (err) => {
|
|
1412
|
+
this.logger.error("Redis error (events subscriber):", err);
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
await this.subscriber.subscribe(`${this.ns}:events`);
|
|
1416
|
+
this.eventsSubscribed = true;
|
|
1417
|
+
}
|
|
1418
|
+
handleJobEvent(message) {
|
|
1419
|
+
try {
|
|
1420
|
+
const event = safeJsonParse(message);
|
|
1421
|
+
if (!event || typeof event.id !== "string") return;
|
|
1422
|
+
const waiters = this.waitingJobs.get(event.id);
|
|
1423
|
+
if (!waiters || waiters.length === 0) return;
|
|
1424
|
+
if (event.status === "completed") {
|
|
1425
|
+
const parsed = typeof event.result === "string" ? safeJsonParse(event.result) ?? event.result : event.result;
|
|
1426
|
+
waiters.forEach((w) => w.resolve(parsed));
|
|
1427
|
+
} else if (event.status === "failed") {
|
|
1428
|
+
const info = typeof event.result === "string" ? safeJsonParse(event.result) ?? {} : event.result ?? {};
|
|
1429
|
+
const err = new Error(info && info.message || "Job failed");
|
|
1430
|
+
if (info && typeof info === "object") {
|
|
1431
|
+
if (typeof info.name === "string") err.name = info.name;
|
|
1432
|
+
if (typeof info.stack === "string") err.stack = info.stack;
|
|
1433
|
+
}
|
|
1434
|
+
waiters.forEach((w) => w.reject(err));
|
|
1435
|
+
}
|
|
1436
|
+
this.waitingJobs.delete(event.id);
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
this.logger.error("Failed to process job event:", err);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Wait for a job to complete or fail, similar to BullMQ's waitUntilFinished.
|
|
1443
|
+
*/
|
|
1444
|
+
async waitUntilFinished(jobId, timeoutMs = 0) {
|
|
1445
|
+
const job = await this.getJob(jobId);
|
|
1446
|
+
const state = await job.getState();
|
|
1447
|
+
if (state === "completed") return job.returnvalue;
|
|
1448
|
+
if (state === "failed") throw new Error(job.failedReason || "Job failed");
|
|
1449
|
+
await this.setupSubscriber();
|
|
1450
|
+
return new Promise((resolve, reject) => {
|
|
1451
|
+
let timer;
|
|
1452
|
+
let waiter;
|
|
1453
|
+
const cleanup = () => {
|
|
1454
|
+
if (timer) clearTimeout(timer);
|
|
1455
|
+
const current = this.waitingJobs.get(jobId);
|
|
1456
|
+
if (!current) return;
|
|
1457
|
+
const remaining = current.filter((w) => w !== waiter);
|
|
1458
|
+
if (remaining.length === 0) this.waitingJobs.delete(jobId);
|
|
1459
|
+
else this.waitingJobs.set(jobId, remaining);
|
|
1460
|
+
};
|
|
1461
|
+
const wrappedResolve = (value) => {
|
|
1462
|
+
cleanup();
|
|
1463
|
+
resolve(value);
|
|
1464
|
+
};
|
|
1465
|
+
const wrappedReject = (err) => {
|
|
1466
|
+
cleanup();
|
|
1467
|
+
reject(err);
|
|
1468
|
+
};
|
|
1469
|
+
waiter = {
|
|
1470
|
+
resolve: wrappedResolve,
|
|
1471
|
+
reject: wrappedReject
|
|
1472
|
+
};
|
|
1473
|
+
const waiters = this.waitingJobs.get(jobId) ?? [];
|
|
1474
|
+
waiters.push(waiter);
|
|
1475
|
+
this.waitingJobs.set(jobId, waiters);
|
|
1476
|
+
if (timeoutMs > 0) timer = setTimeout(() => {
|
|
1477
|
+
wrappedReject(/* @__PURE__ */ new Error(`Timed out waiting for job ${jobId} to finish`));
|
|
1478
|
+
}, timeoutMs);
|
|
1479
|
+
(async () => {
|
|
1480
|
+
try {
|
|
1481
|
+
const latest = await this.getJob(jobId);
|
|
1482
|
+
const latestState = await latest.getState();
|
|
1483
|
+
if (latestState === "completed") wrappedResolve(latest.returnvalue);
|
|
1484
|
+
else if (latestState === "failed") wrappedReject(new Error(latest.failedReason ?? "Job failed"));
|
|
1485
|
+
} catch (_err) {}
|
|
1486
|
+
})();
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1325
1489
|
/**
|
|
1326
1490
|
* Fetch jobs by statuses, emulating BullMQ's Queue.getJobs API used by BullBoard.
|
|
1327
1491
|
* Only getter functionality; ordering is best-effort.
|
|
@@ -1516,6 +1680,25 @@ var Queue = class {
|
|
|
1516
1680
|
await this.flushBatch();
|
|
1517
1681
|
}
|
|
1518
1682
|
await this.stopPromoter();
|
|
1683
|
+
if (this.subscriber) {
|
|
1684
|
+
try {
|
|
1685
|
+
await this.subscriber.unsubscribe(`${this.ns}:events`);
|
|
1686
|
+
await this.subscriber.quit();
|
|
1687
|
+
} catch (_err) {
|
|
1688
|
+
try {
|
|
1689
|
+
this.subscriber.disconnect();
|
|
1690
|
+
} catch (_e) {}
|
|
1691
|
+
}
|
|
1692
|
+
this.subscriber = void 0;
|
|
1693
|
+
this.eventsSubscribed = false;
|
|
1694
|
+
}
|
|
1695
|
+
if (this.waitingJobs.size > 0) {
|
|
1696
|
+
const err = /* @__PURE__ */ new Error("Queue closed");
|
|
1697
|
+
this.waitingJobs.forEach((waiters) => {
|
|
1698
|
+
waiters.forEach((w) => w.reject(err));
|
|
1699
|
+
});
|
|
1700
|
+
this.waitingJobs.clear();
|
|
1701
|
+
}
|
|
1519
1702
|
try {
|
|
1520
1703
|
await this.r.quit();
|
|
1521
1704
|
} catch (_e) {
|
|
@@ -1943,6 +2126,12 @@ var AsyncFifoQueue = class {
|
|
|
1943
2126
|
|
|
1944
2127
|
//#endregion
|
|
1945
2128
|
//#region src/worker.ts
|
|
2129
|
+
var UnrecoverableError = class extends Error {
|
|
2130
|
+
constructor(message) {
|
|
2131
|
+
super(message);
|
|
2132
|
+
this.name = "UnrecoverableError";
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
1946
2135
|
var TypedEventEmitter = class {
|
|
1947
2136
|
constructor() {
|
|
1948
2137
|
this.listeners = /* @__PURE__ */ new Map();
|
|
@@ -1978,7 +2167,7 @@ var TypedEventEmitter = class {
|
|
|
1978
2167
|
return this;
|
|
1979
2168
|
}
|
|
1980
2169
|
};
|
|
1981
|
-
const defaultBackoff = (attempt) => {
|
|
2170
|
+
const defaultBackoff = (attempt, _error) => {
|
|
1982
2171
|
const base = Math.min(3e4, 2 ** (attempt - 1) * 500);
|
|
1983
2172
|
const jitter = Math.floor(base * .25 * Math.random());
|
|
1984
2173
|
return base + jitter;
|
|
@@ -2401,7 +2590,10 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2401
2590
|
const oldest = Array.from(this.jobsInProgress)[0];
|
|
2402
2591
|
const now = Date.now();
|
|
2403
2592
|
return {
|
|
2404
|
-
job: oldest.job,
|
|
2593
|
+
job: Job.fromReserved(this.q, oldest.job, {
|
|
2594
|
+
processedOn: oldest.ts,
|
|
2595
|
+
status: "active"
|
|
2596
|
+
}),
|
|
2405
2597
|
processingTimeMs: now - oldest.ts
|
|
2406
2598
|
};
|
|
2407
2599
|
}
|
|
@@ -2411,7 +2603,10 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2411
2603
|
getCurrentJobs() {
|
|
2412
2604
|
const now = Date.now();
|
|
2413
2605
|
return Array.from(this.jobsInProgress).map((item) => ({
|
|
2414
|
-
job: item.job,
|
|
2606
|
+
job: Job.fromReserved(this.q, item.job, {
|
|
2607
|
+
processedOn: item.ts,
|
|
2608
|
+
status: "active"
|
|
2609
|
+
}),
|
|
2415
2610
|
processingTimeMs: now - item.ts
|
|
2416
2611
|
}));
|
|
2417
2612
|
}
|
|
@@ -2441,7 +2636,7 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2441
2636
|
} catch (e) {
|
|
2442
2637
|
const isConnErr = this.q.isConnectionError(e);
|
|
2443
2638
|
if (!isConnErr || !this.stopping) this.logger.error(`Heartbeat error for job ${job.id}:`, e instanceof Error ? e.message : String(e));
|
|
2444
|
-
this.onError?.(e, job);
|
|
2639
|
+
this.onError?.(e, Job.fromReserved(this.q, job, { status: "active" }));
|
|
2445
2640
|
if (!isConnErr || !this.stopping) this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
2446
2641
|
}
|
|
2447
2642
|
}, minInterval);
|
|
@@ -2452,7 +2647,11 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2452
2647
|
heartbeatDelayTimer = setTimeout(() => {
|
|
2453
2648
|
startHeartbeat();
|
|
2454
2649
|
}, heartbeatThreshold);
|
|
2455
|
-
const
|
|
2650
|
+
const jobInstance = Job.fromReserved(this.q, job, {
|
|
2651
|
+
processedOn: jobStartWallTime,
|
|
2652
|
+
status: "active"
|
|
2653
|
+
});
|
|
2654
|
+
const handlerResult = await this.handler(jobInstance);
|
|
2456
2655
|
if (heartbeatDelayTimer) clearTimeout(heartbeatDelayTimer);
|
|
2457
2656
|
if (hbTimer) clearInterval(hbTimer);
|
|
2458
2657
|
const finishedAtWall = Date.now();
|
|
@@ -2476,7 +2675,11 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2476
2675
|
* Handle job failure: emit events, retry or dead-letter
|
|
2477
2676
|
*/
|
|
2478
2677
|
async handleJobFailure(err, job, jobStartWallTime) {
|
|
2479
|
-
this.
|
|
2678
|
+
const jobInstance = Job.fromReserved(this.q, job, {
|
|
2679
|
+
processedOn: jobStartWallTime,
|
|
2680
|
+
status: "active"
|
|
2681
|
+
});
|
|
2682
|
+
this.onError?.(err, jobInstance);
|
|
2480
2683
|
this.blockingStats.consecutiveEmptyReserves = 0;
|
|
2481
2684
|
this.emptyReserveBackoffMs = 0;
|
|
2482
2685
|
try {
|
|
@@ -2491,7 +2694,12 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2491
2694
|
status: "failed"
|
|
2492
2695
|
}));
|
|
2493
2696
|
const nextAttempt = job.attempts + 1;
|
|
2494
|
-
|
|
2697
|
+
if (err instanceof UnrecoverableError) {
|
|
2698
|
+
this.logger.info(`Unrecoverable error for job ${job.id}: ${err instanceof Error ? err.message : String(err)}. Skipping retries.`);
|
|
2699
|
+
await this.deadLetterJob(err, job, jobStartWallTime, failedAt, nextAttempt);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
const backoffMs = this.backoff(nextAttempt, err);
|
|
2495
2703
|
if (nextAttempt >= this.maxAttempts) {
|
|
2496
2704
|
await this.deadLetterJob(err, job, jobStartWallTime, failedAt, nextAttempt);
|
|
2497
2705
|
return;
|
|
@@ -2561,6 +2769,7 @@ function sleep(ms) {
|
|
|
2561
2769
|
exports.BullBoardGroupMQAdapter = BullBoardGroupMQAdapter;
|
|
2562
2770
|
exports.Job = Job;
|
|
2563
2771
|
exports.Queue = Queue;
|
|
2772
|
+
exports.UnrecoverableError = UnrecoverableError;
|
|
2564
2773
|
exports.Worker = Worker;
|
|
2565
2774
|
exports.getWorkersStatus = getWorkersStatus;
|
|
2566
2775
|
exports.waitForQueueToEmpty = waitForQueueToEmpty;
|