groupmq-plus 1.1.0 → 1.1.1

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 CHANGED
@@ -281,6 +281,13 @@ var Job = class Job {
281
281
  async update(jobData) {
282
282
  await this.updateData(jobData);
283
283
  }
284
+ /**
285
+ * Wait until this job is completed or failed.
286
+ * @param timeoutMs Optional timeout in milliseconds (0 = no timeout)
287
+ */
288
+ async waitUntilFinished(timeoutMs = 0) {
289
+ return this.queue.waitUntilFinished(this.id, timeoutMs);
290
+ }
284
291
  static fromReserved(queue, reserved, meta) {
285
292
  return new Job({
286
293
  queue,
@@ -474,6 +481,8 @@ function safeJsonParse(input) {
474
481
  var Queue = class {
475
482
  constructor(opts) {
476
483
  this._consecutiveEmptyReserves = 0;
484
+ this.eventsSubscribed = false;
485
+ this.waitingJobs = /* @__PURE__ */ new Map();
477
486
  this.promoterRunning = false;
478
487
  this.batchBuffer = [];
479
488
  this.flushing = false;
@@ -1322,6 +1331,91 @@ var Queue = class {
1322
1331
  async getJob(id) {
1323
1332
  return Job.fromStore(this, id);
1324
1333
  }
1334
+ async setupSubscriber() {
1335
+ if (this.eventsSubscribed && this.subscriber) return;
1336
+ if (!this.subscriber) {
1337
+ this.subscriber = this.r.duplicate();
1338
+ this.subscriber.on("message", (channel, message) => {
1339
+ if (channel === `${this.ns}:events`) this.handleJobEvent(message);
1340
+ });
1341
+ this.subscriber.on("error", (err) => {
1342
+ this.logger.error("Redis error (events subscriber):", err);
1343
+ });
1344
+ }
1345
+ await this.subscriber.subscribe(`${this.ns}:events`);
1346
+ this.eventsSubscribed = true;
1347
+ }
1348
+ handleJobEvent(message) {
1349
+ try {
1350
+ const event = safeJsonParse(message);
1351
+ if (!event || typeof event.id !== "string") return;
1352
+ const waiters = this.waitingJobs.get(event.id);
1353
+ if (!waiters || waiters.length === 0) return;
1354
+ if (event.status === "completed") {
1355
+ const parsed = typeof event.result === "string" ? safeJsonParse(event.result) ?? event.result : event.result;
1356
+ waiters.forEach((w) => w.resolve(parsed));
1357
+ } else if (event.status === "failed") {
1358
+ const info = typeof event.result === "string" ? safeJsonParse(event.result) ?? {} : event.result ?? {};
1359
+ const err = new Error(info && info.message || "Job failed");
1360
+ if (info && typeof info === "object") {
1361
+ if (typeof info.name === "string") err.name = info.name;
1362
+ if (typeof info.stack === "string") err.stack = info.stack;
1363
+ }
1364
+ waiters.forEach((w) => w.reject(err));
1365
+ }
1366
+ this.waitingJobs.delete(event.id);
1367
+ } catch (err) {
1368
+ this.logger.error("Failed to process job event:", err);
1369
+ }
1370
+ }
1371
+ /**
1372
+ * Wait for a job to complete or fail, similar to BullMQ's waitUntilFinished.
1373
+ */
1374
+ async waitUntilFinished(jobId, timeoutMs = 0) {
1375
+ const job = await this.getJob(jobId);
1376
+ const state = await job.getState();
1377
+ if (state === "completed") return job.returnvalue;
1378
+ if (state === "failed") throw new Error(job.failedReason || "Job failed");
1379
+ await this.setupSubscriber();
1380
+ return new Promise((resolve, reject) => {
1381
+ let timer;
1382
+ let waiter;
1383
+ const cleanup = () => {
1384
+ if (timer) clearTimeout(timer);
1385
+ const current = this.waitingJobs.get(jobId);
1386
+ if (!current) return;
1387
+ const remaining = current.filter((w) => w !== waiter);
1388
+ if (remaining.length === 0) this.waitingJobs.delete(jobId);
1389
+ else this.waitingJobs.set(jobId, remaining);
1390
+ };
1391
+ const wrappedResolve = (value) => {
1392
+ cleanup();
1393
+ resolve(value);
1394
+ };
1395
+ const wrappedReject = (err) => {
1396
+ cleanup();
1397
+ reject(err);
1398
+ };
1399
+ waiter = {
1400
+ resolve: wrappedResolve,
1401
+ reject: wrappedReject
1402
+ };
1403
+ const waiters = this.waitingJobs.get(jobId) ?? [];
1404
+ waiters.push(waiter);
1405
+ this.waitingJobs.set(jobId, waiters);
1406
+ if (timeoutMs > 0) timer = setTimeout(() => {
1407
+ wrappedReject(/* @__PURE__ */ new Error(`Timed out waiting for job ${jobId} to finish`));
1408
+ }, timeoutMs);
1409
+ (async () => {
1410
+ try {
1411
+ const latest = await this.getJob(jobId);
1412
+ const latestState = await latest.getState();
1413
+ if (latestState === "completed") wrappedResolve(latest.returnvalue);
1414
+ else if (latestState === "failed") wrappedReject(new Error(latest.failedReason ?? "Job failed"));
1415
+ } catch (_err) {}
1416
+ })();
1417
+ });
1418
+ }
1325
1419
  /**
1326
1420
  * Fetch jobs by statuses, emulating BullMQ's Queue.getJobs API used by BullBoard.
1327
1421
  * Only getter functionality; ordering is best-effort.
@@ -1516,6 +1610,25 @@ var Queue = class {
1516
1610
  await this.flushBatch();
1517
1611
  }
1518
1612
  await this.stopPromoter();
1613
+ if (this.subscriber) {
1614
+ try {
1615
+ await this.subscriber.unsubscribe(`${this.ns}:events`);
1616
+ await this.subscriber.quit();
1617
+ } catch (_err) {
1618
+ try {
1619
+ this.subscriber.disconnect();
1620
+ } catch (_e) {}
1621
+ }
1622
+ this.subscriber = void 0;
1623
+ this.eventsSubscribed = false;
1624
+ }
1625
+ if (this.waitingJobs.size > 0) {
1626
+ const err = /* @__PURE__ */ new Error("Queue closed");
1627
+ this.waitingJobs.forEach((waiters) => {
1628
+ waiters.forEach((w) => w.reject(err));
1629
+ });
1630
+ this.waitingJobs.clear();
1631
+ }
1519
1632
  try {
1520
1633
  await this.r.quit();
1521
1634
  } catch (_e) {
@@ -1943,6 +2056,12 @@ var AsyncFifoQueue = class {
1943
2056
 
1944
2057
  //#endregion
1945
2058
  //#region src/worker.ts
2059
+ var UnrecoverableError = class extends Error {
2060
+ constructor(message) {
2061
+ super(message);
2062
+ this.name = "UnrecoverableError";
2063
+ }
2064
+ };
1946
2065
  var TypedEventEmitter = class {
1947
2066
  constructor() {
1948
2067
  this.listeners = /* @__PURE__ */ new Map();
@@ -1978,7 +2097,7 @@ var TypedEventEmitter = class {
1978
2097
  return this;
1979
2098
  }
1980
2099
  };
1981
- const defaultBackoff = (attempt) => {
2100
+ const defaultBackoff = (attempt, _error) => {
1982
2101
  const base = Math.min(3e4, 2 ** (attempt - 1) * 500);
1983
2102
  const jitter = Math.floor(base * .25 * Math.random());
1984
2103
  return base + jitter;
@@ -2491,7 +2610,12 @@ var _Worker = class extends TypedEventEmitter {
2491
2610
  status: "failed"
2492
2611
  }));
2493
2612
  const nextAttempt = job.attempts + 1;
2494
- const backoffMs = this.backoff(nextAttempt);
2613
+ if (err instanceof UnrecoverableError) {
2614
+ this.logger.info(`Unrecoverable error for job ${job.id}: ${err instanceof Error ? err.message : String(err)}. Skipping retries.`);
2615
+ await this.deadLetterJob(err, job, jobStartWallTime, failedAt, nextAttempt);
2616
+ return;
2617
+ }
2618
+ const backoffMs = this.backoff(nextAttempt, err);
2495
2619
  if (nextAttempt >= this.maxAttempts) {
2496
2620
  await this.deadLetterJob(err, job, jobStartWallTime, failedAt, nextAttempt);
2497
2621
  return;
@@ -2561,6 +2685,7 @@ function sleep(ms) {
2561
2685
  exports.BullBoardGroupMQAdapter = BullBoardGroupMQAdapter;
2562
2686
  exports.Job = Job;
2563
2687
  exports.Queue = Queue;
2688
+ exports.UnrecoverableError = UnrecoverableError;
2564
2689
  exports.Worker = Worker;
2565
2690
  exports.getWorkersStatus = getWorkersStatus;
2566
2691
  exports.waitForQueueToEmpty = waitForQueueToEmpty;