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.d.cts CHANGED
@@ -84,6 +84,11 @@ declare class Job<T = any> {
84
84
  retry(_state?: Extract<Status$1, 'completed' | 'failed'>): Promise<void>;
85
85
  updateData(jobData: T): Promise<void>;
86
86
  update(jobData: T): Promise<void>;
87
+ /**
88
+ * Wait until this job is completed or failed.
89
+ * @param timeoutMs Optional timeout in milliseconds (0 = no timeout)
90
+ */
91
+ waitUntilFinished(timeoutMs?: number): Promise<unknown>;
87
92
  static fromReserved<T = any>(queue: Queue<T>, reserved: ReservedJob<T>, meta?: {
88
93
  processedOn?: number;
89
94
  finishedOn?: number;
@@ -493,6 +498,9 @@ declare class Queue<T = any> {
493
498
  orderingDelayMs: number;
494
499
  name: string;
495
500
  private _consecutiveEmptyReserves;
501
+ private subscriber?;
502
+ private eventsSubscribed;
503
+ private waitingJobs;
496
504
  private promoterRedis?;
497
505
  private promoterRunning;
498
506
  private promoterLockId?;
@@ -765,6 +773,12 @@ declare class Queue<T = any> {
765
773
  * Attempts to mimic BullMQ's Job shape for fields commonly used by BullBoard.
766
774
  */
767
775
  getJob(id: string): Promise<Job<T>>;
776
+ private setupSubscriber;
777
+ private handleJobEvent;
778
+ /**
779
+ * Wait for a job to complete or fail, similar to BullMQ's waitUntilFinished.
780
+ */
781
+ waitUntilFinished(jobId: string, timeoutMs?: number): Promise<unknown>;
768
782
  /**
769
783
  * Fetch jobs by statuses, emulating BullMQ's Queue.getJobs API used by BullBoard.
770
784
  * Only getter functionality; ordering is best-effort.
@@ -917,7 +931,10 @@ interface DispatchStrategy {
917
931
  }
918
932
  //#endregion
919
933
  //#region src/worker.d.ts
920
- type BackoffStrategy = (attempt: number) => number;
934
+ declare class UnrecoverableError extends Error {
935
+ constructor(message?: string);
936
+ }
937
+ type BackoffStrategy = (attempt: number, error: unknown) => number;
921
938
  interface WorkerEvents<T = any> extends Record<string, (...args: any[]) => void> {
922
939
  error: (error: Error) => void;
923
940
  closed: () => void;
@@ -988,9 +1005,11 @@ type WorkerOptions<T> = {
988
1005
  maxAttempts?: number;
989
1006
  /**
990
1007
  * Backoff strategy for retrying failed jobs. Determines delay between retries.
1008
+ * Receives the error object to allow smarter strategies.
991
1009
  *
992
1010
  * @default Exponential backoff with jitter (500ms, 1s, 2s, 4s, 8s, 16s, 30s max)
993
- * @example (attempt) => Math.min(10000, attempt * 1000) // Linear backoff
1011
+ * @example (attempt, err) =>
1012
+ * err instanceof RateLimitError ? err.retryAfterMs : Math.min(10000, attempt * 1000)
994
1013
  *
995
1014
  * **When to adjust:**
996
1015
  * - Rate-limited APIs: Use longer delays
@@ -1296,5 +1315,5 @@ declare function getWorkersStatus<T = any>(workers: Worker<T>[]): {
1296
1315
  }>;
1297
1316
  };
1298
1317
  //#endregion
1299
- export { AddOptions, BackoffStrategy, BullBoardGroupMQAdapter, FlowJob, FlowOptions, GroupMQBullBoardAdapterOptions, Job, Queue, QueueOptions, RepeatOptions, ReservedJob, Worker, WorkerEvents, WorkerOptions, getWorkersStatus, waitForQueueToEmpty };
1318
+ export { AddOptions, BackoffStrategy, BullBoardGroupMQAdapter, FlowJob, FlowOptions, GroupMQBullBoardAdapterOptions, Job, Queue, QueueOptions, RepeatOptions, ReservedJob, UnrecoverableError, Worker, WorkerEvents, WorkerOptions, getWorkersStatus, waitForQueueToEmpty };
1300
1319
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -84,6 +84,11 @@ declare class Job<T = any> {
84
84
  retry(_state?: Extract<Status$1, 'completed' | 'failed'>): Promise<void>;
85
85
  updateData(jobData: T): Promise<void>;
86
86
  update(jobData: T): Promise<void>;
87
+ /**
88
+ * Wait until this job is completed or failed.
89
+ * @param timeoutMs Optional timeout in milliseconds (0 = no timeout)
90
+ */
91
+ waitUntilFinished(timeoutMs?: number): Promise<unknown>;
87
92
  static fromReserved<T = any>(queue: Queue<T>, reserved: ReservedJob<T>, meta?: {
88
93
  processedOn?: number;
89
94
  finishedOn?: number;
@@ -493,6 +498,9 @@ declare class Queue<T = any> {
493
498
  orderingDelayMs: number;
494
499
  name: string;
495
500
  private _consecutiveEmptyReserves;
501
+ private subscriber?;
502
+ private eventsSubscribed;
503
+ private waitingJobs;
496
504
  private promoterRedis?;
497
505
  private promoterRunning;
498
506
  private promoterLockId?;
@@ -765,6 +773,12 @@ declare class Queue<T = any> {
765
773
  * Attempts to mimic BullMQ's Job shape for fields commonly used by BullBoard.
766
774
  */
767
775
  getJob(id: string): Promise<Job<T>>;
776
+ private setupSubscriber;
777
+ private handleJobEvent;
778
+ /**
779
+ * Wait for a job to complete or fail, similar to BullMQ's waitUntilFinished.
780
+ */
781
+ waitUntilFinished(jobId: string, timeoutMs?: number): Promise<unknown>;
768
782
  /**
769
783
  * Fetch jobs by statuses, emulating BullMQ's Queue.getJobs API used by BullBoard.
770
784
  * Only getter functionality; ordering is best-effort.
@@ -917,7 +931,10 @@ interface DispatchStrategy {
917
931
  }
918
932
  //#endregion
919
933
  //#region src/worker.d.ts
920
- type BackoffStrategy = (attempt: number) => number;
934
+ declare class UnrecoverableError extends Error {
935
+ constructor(message?: string);
936
+ }
937
+ type BackoffStrategy = (attempt: number, error: unknown) => number;
921
938
  interface WorkerEvents<T = any> extends Record<string, (...args: any[]) => void> {
922
939
  error: (error: Error) => void;
923
940
  closed: () => void;
@@ -988,9 +1005,11 @@ type WorkerOptions<T> = {
988
1005
  maxAttempts?: number;
989
1006
  /**
990
1007
  * Backoff strategy for retrying failed jobs. Determines delay between retries.
1008
+ * Receives the error object to allow smarter strategies.
991
1009
  *
992
1010
  * @default Exponential backoff with jitter (500ms, 1s, 2s, 4s, 8s, 16s, 30s max)
993
- * @example (attempt) => Math.min(10000, attempt * 1000) // Linear backoff
1011
+ * @example (attempt, err) =>
1012
+ * err instanceof RateLimitError ? err.retryAfterMs : Math.min(10000, attempt * 1000)
994
1013
  *
995
1014
  * **When to adjust:**
996
1015
  * - Rate-limited APIs: Use longer delays
@@ -1296,5 +1315,5 @@ declare function getWorkersStatus<T = any>(workers: Worker<T>[]): {
1296
1315
  }>;
1297
1316
  };
1298
1317
  //#endregion
1299
- export { AddOptions, BackoffStrategy, BullBoardGroupMQAdapter, FlowJob, FlowOptions, GroupMQBullBoardAdapterOptions, Job, Queue, QueueOptions, RepeatOptions, ReservedJob, Worker, WorkerEvents, WorkerOptions, getWorkersStatus, waitForQueueToEmpty };
1318
+ export { AddOptions, BackoffStrategy, BullBoardGroupMQAdapter, FlowJob, FlowOptions, GroupMQBullBoardAdapterOptions, Job, Queue, QueueOptions, RepeatOptions, ReservedJob, UnrecoverableError, Worker, WorkerEvents, WorkerOptions, getWorkersStatus, waitForQueueToEmpty };
1300
1319
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -276,6 +276,13 @@ var Job = class Job {
276
276
  async update(jobData) {
277
277
  await this.updateData(jobData);
278
278
  }
279
+ /**
280
+ * Wait until this job is completed or failed.
281
+ * @param timeoutMs Optional timeout in milliseconds (0 = no timeout)
282
+ */
283
+ async waitUntilFinished(timeoutMs = 0) {
284
+ return this.queue.waitUntilFinished(this.id, timeoutMs);
285
+ }
279
286
  static fromReserved(queue, reserved, meta) {
280
287
  return new Job({
281
288
  queue,
@@ -469,6 +476,8 @@ function safeJsonParse(input) {
469
476
  var Queue = class {
470
477
  constructor(opts) {
471
478
  this._consecutiveEmptyReserves = 0;
479
+ this.eventsSubscribed = false;
480
+ this.waitingJobs = /* @__PURE__ */ new Map();
472
481
  this.promoterRunning = false;
473
482
  this.batchBuffer = [];
474
483
  this.flushing = false;
@@ -1317,6 +1326,91 @@ var Queue = class {
1317
1326
  async getJob(id) {
1318
1327
  return Job.fromStore(this, id);
1319
1328
  }
1329
+ async setupSubscriber() {
1330
+ if (this.eventsSubscribed && this.subscriber) return;
1331
+ if (!this.subscriber) {
1332
+ this.subscriber = this.r.duplicate();
1333
+ this.subscriber.on("message", (channel, message) => {
1334
+ if (channel === `${this.ns}:events`) this.handleJobEvent(message);
1335
+ });
1336
+ this.subscriber.on("error", (err) => {
1337
+ this.logger.error("Redis error (events subscriber):", err);
1338
+ });
1339
+ }
1340
+ await this.subscriber.subscribe(`${this.ns}:events`);
1341
+ this.eventsSubscribed = true;
1342
+ }
1343
+ handleJobEvent(message) {
1344
+ try {
1345
+ const event = safeJsonParse(message);
1346
+ if (!event || typeof event.id !== "string") return;
1347
+ const waiters = this.waitingJobs.get(event.id);
1348
+ if (!waiters || waiters.length === 0) return;
1349
+ if (event.status === "completed") {
1350
+ const parsed = typeof event.result === "string" ? safeJsonParse(event.result) ?? event.result : event.result;
1351
+ waiters.forEach((w) => w.resolve(parsed));
1352
+ } else if (event.status === "failed") {
1353
+ const info = typeof event.result === "string" ? safeJsonParse(event.result) ?? {} : event.result ?? {};
1354
+ const err = new Error(info && info.message || "Job failed");
1355
+ if (info && typeof info === "object") {
1356
+ if (typeof info.name === "string") err.name = info.name;
1357
+ if (typeof info.stack === "string") err.stack = info.stack;
1358
+ }
1359
+ waiters.forEach((w) => w.reject(err));
1360
+ }
1361
+ this.waitingJobs.delete(event.id);
1362
+ } catch (err) {
1363
+ this.logger.error("Failed to process job event:", err);
1364
+ }
1365
+ }
1366
+ /**
1367
+ * Wait for a job to complete or fail, similar to BullMQ's waitUntilFinished.
1368
+ */
1369
+ async waitUntilFinished(jobId, timeoutMs = 0) {
1370
+ const job = await this.getJob(jobId);
1371
+ const state = await job.getState();
1372
+ if (state === "completed") return job.returnvalue;
1373
+ if (state === "failed") throw new Error(job.failedReason || "Job failed");
1374
+ await this.setupSubscriber();
1375
+ return new Promise((resolve, reject) => {
1376
+ let timer;
1377
+ let waiter;
1378
+ const cleanup = () => {
1379
+ if (timer) clearTimeout(timer);
1380
+ const current = this.waitingJobs.get(jobId);
1381
+ if (!current) return;
1382
+ const remaining = current.filter((w) => w !== waiter);
1383
+ if (remaining.length === 0) this.waitingJobs.delete(jobId);
1384
+ else this.waitingJobs.set(jobId, remaining);
1385
+ };
1386
+ const wrappedResolve = (value) => {
1387
+ cleanup();
1388
+ resolve(value);
1389
+ };
1390
+ const wrappedReject = (err) => {
1391
+ cleanup();
1392
+ reject(err);
1393
+ };
1394
+ waiter = {
1395
+ resolve: wrappedResolve,
1396
+ reject: wrappedReject
1397
+ };
1398
+ const waiters = this.waitingJobs.get(jobId) ?? [];
1399
+ waiters.push(waiter);
1400
+ this.waitingJobs.set(jobId, waiters);
1401
+ if (timeoutMs > 0) timer = setTimeout(() => {
1402
+ wrappedReject(/* @__PURE__ */ new Error(`Timed out waiting for job ${jobId} to finish`));
1403
+ }, timeoutMs);
1404
+ (async () => {
1405
+ try {
1406
+ const latest = await this.getJob(jobId);
1407
+ const latestState = await latest.getState();
1408
+ if (latestState === "completed") wrappedResolve(latest.returnvalue);
1409
+ else if (latestState === "failed") wrappedReject(new Error(latest.failedReason ?? "Job failed"));
1410
+ } catch (_err) {}
1411
+ })();
1412
+ });
1413
+ }
1320
1414
  /**
1321
1415
  * Fetch jobs by statuses, emulating BullMQ's Queue.getJobs API used by BullBoard.
1322
1416
  * Only getter functionality; ordering is best-effort.
@@ -1511,6 +1605,25 @@ var Queue = class {
1511
1605
  await this.flushBatch();
1512
1606
  }
1513
1607
  await this.stopPromoter();
1608
+ if (this.subscriber) {
1609
+ try {
1610
+ await this.subscriber.unsubscribe(`${this.ns}:events`);
1611
+ await this.subscriber.quit();
1612
+ } catch (_err) {
1613
+ try {
1614
+ this.subscriber.disconnect();
1615
+ } catch (_e) {}
1616
+ }
1617
+ this.subscriber = void 0;
1618
+ this.eventsSubscribed = false;
1619
+ }
1620
+ if (this.waitingJobs.size > 0) {
1621
+ const err = /* @__PURE__ */ new Error("Queue closed");
1622
+ this.waitingJobs.forEach((waiters) => {
1623
+ waiters.forEach((w) => w.reject(err));
1624
+ });
1625
+ this.waitingJobs.clear();
1626
+ }
1514
1627
  try {
1515
1628
  await this.r.quit();
1516
1629
  } catch (_e) {
@@ -1938,6 +2051,12 @@ var AsyncFifoQueue = class {
1938
2051
 
1939
2052
  //#endregion
1940
2053
  //#region src/worker.ts
2054
+ var UnrecoverableError = class extends Error {
2055
+ constructor(message) {
2056
+ super(message);
2057
+ this.name = "UnrecoverableError";
2058
+ }
2059
+ };
1941
2060
  var TypedEventEmitter = class {
1942
2061
  constructor() {
1943
2062
  this.listeners = /* @__PURE__ */ new Map();
@@ -1973,7 +2092,7 @@ var TypedEventEmitter = class {
1973
2092
  return this;
1974
2093
  }
1975
2094
  };
1976
- const defaultBackoff = (attempt) => {
2095
+ const defaultBackoff = (attempt, _error) => {
1977
2096
  const base = Math.min(3e4, 2 ** (attempt - 1) * 500);
1978
2097
  const jitter = Math.floor(base * .25 * Math.random());
1979
2098
  return base + jitter;
@@ -2486,7 +2605,12 @@ var _Worker = class extends TypedEventEmitter {
2486
2605
  status: "failed"
2487
2606
  }));
2488
2607
  const nextAttempt = job.attempts + 1;
2489
- const backoffMs = this.backoff(nextAttempt);
2608
+ if (err instanceof UnrecoverableError) {
2609
+ this.logger.info(`Unrecoverable error for job ${job.id}: ${err instanceof Error ? err.message : String(err)}. Skipping retries.`);
2610
+ await this.deadLetterJob(err, job, jobStartWallTime, failedAt, nextAttempt);
2611
+ return;
2612
+ }
2613
+ const backoffMs = this.backoff(nextAttempt, err);
2490
2614
  if (nextAttempt >= this.maxAttempts) {
2491
2615
  await this.deadLetterJob(err, job, jobStartWallTime, failedAt, nextAttempt);
2492
2616
  return;
@@ -2553,5 +2677,5 @@ function sleep(ms) {
2553
2677
  }
2554
2678
 
2555
2679
  //#endregion
2556
- export { BullBoardGroupMQAdapter, Job, Queue, Worker, getWorkersStatus, waitForQueueToEmpty };
2680
+ export { BullBoardGroupMQAdapter, Job, Queue, UnrecoverableError, Worker, getWorkersStatus, waitForQueueToEmpty };
2557
2681
  //# sourceMappingURL=index.js.map