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.d.cts CHANGED
@@ -37,6 +37,7 @@ declare class Job<T = any> {
37
37
  readonly timestamp: number;
38
38
  readonly orderMs?: number;
39
39
  readonly status: Status$1 | 'unknown';
40
+ readonly parentId?: string;
40
41
  constructor(args: {
41
42
  queue: Queue<T>;
42
43
  id: string;
@@ -56,6 +57,7 @@ declare class Job<T = any> {
56
57
  timestamp: number;
57
58
  orderMs?: number;
58
59
  status?: Status$1 | 'unknown';
60
+ parentId?: string;
59
61
  });
60
62
  getState(): Promise<Status$1 | 'stuck' | 'waiting-children' | 'prioritized' | 'unknown'>;
61
63
  toJSON(): {
@@ -84,6 +86,31 @@ declare class Job<T = any> {
84
86
  retry(_state?: Extract<Status$1, 'completed' | 'failed'>): Promise<void>;
85
87
  updateData(jobData: T): Promise<void>;
86
88
  update(jobData: T): Promise<void>;
89
+ /**
90
+ * Wait until this job is completed or failed.
91
+ * @param timeoutMs Optional timeout in milliseconds (0 = no timeout)
92
+ */
93
+ waitUntilFinished(timeoutMs?: number): Promise<unknown>;
94
+ /**
95
+ * Get all child jobs of this job (if it's a parent in a flow).
96
+ * @returns Array of child Job instances
97
+ */
98
+ getChildren(): Promise<Job<any>[]>;
99
+ /**
100
+ * Get the return values of all child jobs in a flow.
101
+ * @returns Object mapping child job IDs to their return values
102
+ */
103
+ getChildrenValues(): Promise<Record<string, any>>;
104
+ /**
105
+ * Get the number of remaining child jobs that haven't completed yet.
106
+ * @returns Number of remaining dependencies, or null if not a parent job
107
+ */
108
+ getDependenciesCount(): Promise<number | null>;
109
+ /**
110
+ * Get the parent job of this job (if it's a child in a flow).
111
+ * @returns Parent Job instance, or undefined if no parent or parent was deleted
112
+ */
113
+ getParent(): Promise<Job<any> | undefined>;
87
114
  static fromReserved<T = any>(queue: Queue<T>, reserved: ReservedJob<T>, meta?: {
88
115
  processedOn?: number;
89
116
  finishedOn?: number;
@@ -493,6 +520,9 @@ declare class Queue<T = any> {
493
520
  orderingDelayMs: number;
494
521
  name: string;
495
522
  private _consecutiveEmptyReserves;
523
+ private subscriber?;
524
+ private eventsSubscribed;
525
+ private waitingJobs;
496
526
  private promoterRedis?;
497
527
  private promoterRunning;
498
528
  private promoterLockId?;
@@ -529,6 +559,18 @@ declare class Queue<T = any> {
529
559
  * @returns An object mapping child job IDs to their results
530
560
  */
531
561
  getFlowResults(parentId: string): Promise<Record<string, any>>;
562
+ /**
563
+ * Gets all child job IDs for a parent job in a flow.
564
+ * @param parentId The ID of the parent job
565
+ * @returns An array of child job IDs
566
+ */
567
+ getFlowChildrenIds(parentId: string): Promise<string[]>;
568
+ /**
569
+ * Gets all child jobs for a parent job in a flow.
570
+ * @param parentId The ID of the parent job
571
+ * @returns An array of Job instances for all children
572
+ */
573
+ getFlowChildren(parentId: string): Promise<Job<any>[]>;
532
574
  private addSingle;
533
575
  private flushBatch;
534
576
  reserve(): Promise<ReservedJob<T> | null>;
@@ -765,6 +807,12 @@ declare class Queue<T = any> {
765
807
  * Attempts to mimic BullMQ's Job shape for fields commonly used by BullBoard.
766
808
  */
767
809
  getJob(id: string): Promise<Job<T>>;
810
+ private setupSubscriber;
811
+ private handleJobEvent;
812
+ /**
813
+ * Wait for a job to complete or fail, similar to BullMQ's waitUntilFinished.
814
+ */
815
+ waitUntilFinished(jobId: string, timeoutMs?: number): Promise<unknown>;
768
816
  /**
769
817
  * Fetch jobs by statuses, emulating BullMQ's Queue.getJobs API used by BullBoard.
770
818
  * Only getter functionality; ordering is best-effort.
@@ -917,7 +965,10 @@ interface DispatchStrategy {
917
965
  }
918
966
  //#endregion
919
967
  //#region src/worker.d.ts
920
- type BackoffStrategy = (attempt: number) => number;
968
+ declare class UnrecoverableError extends Error {
969
+ constructor(message?: string);
970
+ }
971
+ type BackoffStrategy = (attempt: number, error: unknown) => number;
921
972
  interface WorkerEvents<T = any> extends Record<string, (...args: any[]) => void> {
922
973
  error: (error: Error) => void;
923
974
  closed: () => void;
@@ -950,10 +1001,10 @@ type WorkerOptions<T> = {
950
1001
  name?: string;
951
1002
  /**
952
1003
  * The function that processes jobs. Must be async and handle job failures gracefully.
953
- * @param job The reserved job to process
1004
+ * @param job The Job instance to process, with access to all Job methods
954
1005
  * @returns Promise that resolves when job is complete
955
1006
  */
956
- handler: (job: ReservedJob<T>) => Promise<unknown>;
1007
+ handler: (job: Job<T>) => Promise<unknown>;
957
1008
  /**
958
1009
  * Heartbeat interval in milliseconds to keep jobs alive during processing.
959
1010
  * Prevents jobs from timing out during long-running operations.
@@ -970,9 +1021,9 @@ type WorkerOptions<T> = {
970
1021
  /**
971
1022
  * Error handler called when job processing fails or worker encounters errors
972
1023
  * @param err The error that occurred
973
- * @param job The job that failed (if applicable)
1024
+ * @param job The Job instance that failed (if applicable)
974
1025
  */
975
- onError?: (err: unknown, job?: ReservedJob<T>) => void;
1026
+ onError?: (err: unknown, job?: Job<T>) => void;
976
1027
  /**
977
1028
  * Maximum number of retry attempts for failed jobs at the worker level.
978
1029
  * This overrides the queue's default maxAttempts setting.
@@ -988,9 +1039,11 @@ type WorkerOptions<T> = {
988
1039
  maxAttempts?: number;
989
1040
  /**
990
1041
  * Backoff strategy for retrying failed jobs. Determines delay between retries.
1042
+ * Receives the error object to allow smarter strategies.
991
1043
  *
992
1044
  * @default Exponential backoff with jitter (500ms, 1s, 2s, 4s, 8s, 16s, 30s max)
993
- * @example (attempt) => Math.min(10000, attempt * 1000) // Linear backoff
1045
+ * @example (attempt, err) =>
1046
+ * err instanceof RateLimitError ? err.retryAfterMs : Math.min(10000, attempt * 1000)
994
1047
  *
995
1048
  * **When to adjust:**
996
1049
  * - Rate-limited APIs: Use longer delays
@@ -1237,14 +1290,14 @@ declare class _Worker<T = any> extends TypedEventEmitter<WorkerEvents<T>> {
1237
1290
  * For concurrency > 1, returns the oldest job in progress
1238
1291
  */
1239
1292
  getCurrentJob(): {
1240
- job: ReservedJob<T>;
1293
+ job: Job<T>;
1241
1294
  processingTimeMs: number;
1242
1295
  } | null;
1243
1296
  /**
1244
1297
  * Get information about all currently processing jobs
1245
1298
  */
1246
1299
  getCurrentJobs(): Array<{
1247
- job: ReservedJob<T>;
1300
+ job: Job<T>;
1248
1301
  processingTimeMs: number;
1249
1302
  }>;
1250
1303
  /**
@@ -1296,5 +1349,5 @@ declare function getWorkersStatus<T = any>(workers: Worker<T>[]): {
1296
1349
  }>;
1297
1350
  };
1298
1351
  //#endregion
1299
- export { AddOptions, BackoffStrategy, BullBoardGroupMQAdapter, FlowJob, FlowOptions, GroupMQBullBoardAdapterOptions, Job, Queue, QueueOptions, RepeatOptions, ReservedJob, Worker, WorkerEvents, WorkerOptions, getWorkersStatus, waitForQueueToEmpty };
1352
+ export { AddOptions, BackoffStrategy, BullBoardGroupMQAdapter, FlowJob, FlowOptions, GroupMQBullBoardAdapterOptions, Job, Queue, QueueOptions, RepeatOptions, ReservedJob, UnrecoverableError, Worker, WorkerEvents, WorkerOptions, getWorkersStatus, waitForQueueToEmpty };
1300
1353
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -37,6 +37,7 @@ declare class Job<T = any> {
37
37
  readonly timestamp: number;
38
38
  readonly orderMs?: number;
39
39
  readonly status: Status$1 | 'unknown';
40
+ readonly parentId?: string;
40
41
  constructor(args: {
41
42
  queue: Queue<T>;
42
43
  id: string;
@@ -56,6 +57,7 @@ declare class Job<T = any> {
56
57
  timestamp: number;
57
58
  orderMs?: number;
58
59
  status?: Status$1 | 'unknown';
60
+ parentId?: string;
59
61
  });
60
62
  getState(): Promise<Status$1 | 'stuck' | 'waiting-children' | 'prioritized' | 'unknown'>;
61
63
  toJSON(): {
@@ -84,6 +86,31 @@ declare class Job<T = any> {
84
86
  retry(_state?: Extract<Status$1, 'completed' | 'failed'>): Promise<void>;
85
87
  updateData(jobData: T): Promise<void>;
86
88
  update(jobData: T): Promise<void>;
89
+ /**
90
+ * Wait until this job is completed or failed.
91
+ * @param timeoutMs Optional timeout in milliseconds (0 = no timeout)
92
+ */
93
+ waitUntilFinished(timeoutMs?: number): Promise<unknown>;
94
+ /**
95
+ * Get all child jobs of this job (if it's a parent in a flow).
96
+ * @returns Array of child Job instances
97
+ */
98
+ getChildren(): Promise<Job<any>[]>;
99
+ /**
100
+ * Get the return values of all child jobs in a flow.
101
+ * @returns Object mapping child job IDs to their return values
102
+ */
103
+ getChildrenValues(): Promise<Record<string, any>>;
104
+ /**
105
+ * Get the number of remaining child jobs that haven't completed yet.
106
+ * @returns Number of remaining dependencies, or null if not a parent job
107
+ */
108
+ getDependenciesCount(): Promise<number | null>;
109
+ /**
110
+ * Get the parent job of this job (if it's a child in a flow).
111
+ * @returns Parent Job instance, or undefined if no parent or parent was deleted
112
+ */
113
+ getParent(): Promise<Job<any> | undefined>;
87
114
  static fromReserved<T = any>(queue: Queue<T>, reserved: ReservedJob<T>, meta?: {
88
115
  processedOn?: number;
89
116
  finishedOn?: number;
@@ -493,6 +520,9 @@ declare class Queue<T = any> {
493
520
  orderingDelayMs: number;
494
521
  name: string;
495
522
  private _consecutiveEmptyReserves;
523
+ private subscriber?;
524
+ private eventsSubscribed;
525
+ private waitingJobs;
496
526
  private promoterRedis?;
497
527
  private promoterRunning;
498
528
  private promoterLockId?;
@@ -529,6 +559,18 @@ declare class Queue<T = any> {
529
559
  * @returns An object mapping child job IDs to their results
530
560
  */
531
561
  getFlowResults(parentId: string): Promise<Record<string, any>>;
562
+ /**
563
+ * Gets all child job IDs for a parent job in a flow.
564
+ * @param parentId The ID of the parent job
565
+ * @returns An array of child job IDs
566
+ */
567
+ getFlowChildrenIds(parentId: string): Promise<string[]>;
568
+ /**
569
+ * Gets all child jobs for a parent job in a flow.
570
+ * @param parentId The ID of the parent job
571
+ * @returns An array of Job instances for all children
572
+ */
573
+ getFlowChildren(parentId: string): Promise<Job<any>[]>;
532
574
  private addSingle;
533
575
  private flushBatch;
534
576
  reserve(): Promise<ReservedJob<T> | null>;
@@ -765,6 +807,12 @@ declare class Queue<T = any> {
765
807
  * Attempts to mimic BullMQ's Job shape for fields commonly used by BullBoard.
766
808
  */
767
809
  getJob(id: string): Promise<Job<T>>;
810
+ private setupSubscriber;
811
+ private handleJobEvent;
812
+ /**
813
+ * Wait for a job to complete or fail, similar to BullMQ's waitUntilFinished.
814
+ */
815
+ waitUntilFinished(jobId: string, timeoutMs?: number): Promise<unknown>;
768
816
  /**
769
817
  * Fetch jobs by statuses, emulating BullMQ's Queue.getJobs API used by BullBoard.
770
818
  * Only getter functionality; ordering is best-effort.
@@ -917,7 +965,10 @@ interface DispatchStrategy {
917
965
  }
918
966
  //#endregion
919
967
  //#region src/worker.d.ts
920
- type BackoffStrategy = (attempt: number) => number;
968
+ declare class UnrecoverableError extends Error {
969
+ constructor(message?: string);
970
+ }
971
+ type BackoffStrategy = (attempt: number, error: unknown) => number;
921
972
  interface WorkerEvents<T = any> extends Record<string, (...args: any[]) => void> {
922
973
  error: (error: Error) => void;
923
974
  closed: () => void;
@@ -950,10 +1001,10 @@ type WorkerOptions<T> = {
950
1001
  name?: string;
951
1002
  /**
952
1003
  * The function that processes jobs. Must be async and handle job failures gracefully.
953
- * @param job The reserved job to process
1004
+ * @param job The Job instance to process, with access to all Job methods
954
1005
  * @returns Promise that resolves when job is complete
955
1006
  */
956
- handler: (job: ReservedJob<T>) => Promise<unknown>;
1007
+ handler: (job: Job<T>) => Promise<unknown>;
957
1008
  /**
958
1009
  * Heartbeat interval in milliseconds to keep jobs alive during processing.
959
1010
  * Prevents jobs from timing out during long-running operations.
@@ -970,9 +1021,9 @@ type WorkerOptions<T> = {
970
1021
  /**
971
1022
  * Error handler called when job processing fails or worker encounters errors
972
1023
  * @param err The error that occurred
973
- * @param job The job that failed (if applicable)
1024
+ * @param job The Job instance that failed (if applicable)
974
1025
  */
975
- onError?: (err: unknown, job?: ReservedJob<T>) => void;
1026
+ onError?: (err: unknown, job?: Job<T>) => void;
976
1027
  /**
977
1028
  * Maximum number of retry attempts for failed jobs at the worker level.
978
1029
  * This overrides the queue's default maxAttempts setting.
@@ -988,9 +1039,11 @@ type WorkerOptions<T> = {
988
1039
  maxAttempts?: number;
989
1040
  /**
990
1041
  * Backoff strategy for retrying failed jobs. Determines delay between retries.
1042
+ * Receives the error object to allow smarter strategies.
991
1043
  *
992
1044
  * @default Exponential backoff with jitter (500ms, 1s, 2s, 4s, 8s, 16s, 30s max)
993
- * @example (attempt) => Math.min(10000, attempt * 1000) // Linear backoff
1045
+ * @example (attempt, err) =>
1046
+ * err instanceof RateLimitError ? err.retryAfterMs : Math.min(10000, attempt * 1000)
994
1047
  *
995
1048
  * **When to adjust:**
996
1049
  * - Rate-limited APIs: Use longer delays
@@ -1237,14 +1290,14 @@ declare class _Worker<T = any> extends TypedEventEmitter<WorkerEvents<T>> {
1237
1290
  * For concurrency > 1, returns the oldest job in progress
1238
1291
  */
1239
1292
  getCurrentJob(): {
1240
- job: ReservedJob<T>;
1293
+ job: Job<T>;
1241
1294
  processingTimeMs: number;
1242
1295
  } | null;
1243
1296
  /**
1244
1297
  * Get information about all currently processing jobs
1245
1298
  */
1246
1299
  getCurrentJobs(): Array<{
1247
- job: ReservedJob<T>;
1300
+ job: Job<T>;
1248
1301
  processingTimeMs: number;
1249
1302
  }>;
1250
1303
  /**
@@ -1296,5 +1349,5 @@ declare function getWorkersStatus<T = any>(workers: Worker<T>[]): {
1296
1349
  }>;
1297
1350
  };
1298
1351
  //#endregion
1299
- export { AddOptions, BackoffStrategy, BullBoardGroupMQAdapter, FlowJob, FlowOptions, GroupMQBullBoardAdapterOptions, Job, Queue, QueueOptions, RepeatOptions, ReservedJob, Worker, WorkerEvents, WorkerOptions, getWorkersStatus, waitForQueueToEmpty };
1352
+ export { AddOptions, BackoffStrategy, BullBoardGroupMQAdapter, FlowJob, FlowOptions, GroupMQBullBoardAdapterOptions, Job, Queue, QueueOptions, RepeatOptions, ReservedJob, UnrecoverableError, Worker, WorkerEvents, WorkerOptions, getWorkersStatus, waitForQueueToEmpty };
1300
1353
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -235,6 +235,7 @@ var Job = class Job {
235
235
  this.timestamp = args.timestamp;
236
236
  this.orderMs = args.orderMs;
237
237
  this.status = args.status ?? "unknown";
238
+ this.parentId = args.parentId;
238
239
  }
239
240
  async getState() {
240
241
  return this.status ?? "unknown";
@@ -276,6 +277,46 @@ var Job = class Job {
276
277
  async update(jobData) {
277
278
  await this.updateData(jobData);
278
279
  }
280
+ /**
281
+ * Wait until this job is completed or failed.
282
+ * @param timeoutMs Optional timeout in milliseconds (0 = no timeout)
283
+ */
284
+ async waitUntilFinished(timeoutMs = 0) {
285
+ return this.queue.waitUntilFinished(this.id, timeoutMs);
286
+ }
287
+ /**
288
+ * Get all child jobs of this job (if it's a parent in a flow).
289
+ * @returns Array of child Job instances
290
+ */
291
+ async getChildren() {
292
+ return this.queue.getFlowChildren(this.id);
293
+ }
294
+ /**
295
+ * Get the return values of all child jobs in a flow.
296
+ * @returns Object mapping child job IDs to their return values
297
+ */
298
+ async getChildrenValues() {
299
+ return this.queue.getFlowResults(this.id);
300
+ }
301
+ /**
302
+ * Get the number of remaining child jobs that haven't completed yet.
303
+ * @returns Number of remaining dependencies, or null if not a parent job
304
+ */
305
+ async getDependenciesCount() {
306
+ return this.queue.getFlowDependencies(this.id);
307
+ }
308
+ /**
309
+ * Get the parent job of this job (if it's a child in a flow).
310
+ * @returns Parent Job instance, or undefined if no parent or parent was deleted
311
+ */
312
+ async getParent() {
313
+ if (!this.parentId) return void 0;
314
+ try {
315
+ return await this.queue.getJob(this.parentId);
316
+ } catch (_e) {
317
+ return;
318
+ }
319
+ }
279
320
  static fromReserved(queue, reserved, meta) {
280
321
  return new Job({
281
322
  queue,
@@ -315,6 +356,7 @@ var Job = class Job {
315
356
  const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
316
357
  const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
317
358
  const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
359
+ const parentId = raw.parentId || void 0;
318
360
  return new Job({
319
361
  queue,
320
362
  id,
@@ -333,7 +375,8 @@ var Job = class Job {
333
375
  returnvalue,
334
376
  timestamp: timestampMs || Date.now(),
335
377
  orderMs,
336
- status: knownStatus ?? coerceStatus(raw.status)
378
+ status: knownStatus ?? coerceStatus(raw.status),
379
+ parentId
337
380
  });
338
381
  }
339
382
  static async fromStore(queue, id) {
@@ -352,6 +395,7 @@ var Job = class Job {
352
395
  const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
353
396
  const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
354
397
  const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
398
+ const parentId = raw.parentId || void 0;
355
399
  const [inProcessing, inDelayed] = await Promise.all([queue.redis.zscore(`${queue.namespace}:processing`, id), queue.redis.zscore(`${queue.namespace}:delayed`, id)]);
356
400
  let status = raw.status;
357
401
  if (inProcessing !== null) status = "active";
@@ -377,7 +421,8 @@ var Job = class Job {
377
421
  returnvalue,
378
422
  timestamp: timestampMs || Date.now(),
379
423
  orderMs,
380
- status: coerceStatus(status)
424
+ status: coerceStatus(status),
425
+ parentId
381
426
  });
382
427
  }
383
428
  };
@@ -469,6 +514,8 @@ function safeJsonParse(input) {
469
514
  var Queue = class {
470
515
  constructor(opts) {
471
516
  this._consecutiveEmptyReserves = 0;
517
+ this.eventsSubscribed = false;
518
+ this.waitingJobs = /* @__PURE__ */ new Map();
472
519
  this.promoterRunning = false;
473
520
  this.batchBuffer = [];
474
521
  this.flushing = false;
@@ -623,6 +670,38 @@ var Queue = class {
623
670
  }
624
671
  return parsed;
625
672
  }
673
+ /**
674
+ * Gets all child job IDs for a parent job in a flow.
675
+ * @param parentId The ID of the parent job
676
+ * @returns An array of child job IDs
677
+ */
678
+ async getFlowChildrenIds(parentId) {
679
+ return this.r.smembers(`${this.ns}:flow:children:${parentId}`);
680
+ }
681
+ /**
682
+ * Gets all child jobs for a parent job in a flow.
683
+ * @param parentId The ID of the parent job
684
+ * @returns An array of Job instances for all children
685
+ */
686
+ async getFlowChildren(parentId) {
687
+ const ids = await this.getFlowChildrenIds(parentId);
688
+ if (ids.length === 0) return [];
689
+ const pipe = this.r.multi();
690
+ for (const id of ids) pipe.hgetall(`${this.ns}:job:${id}`);
691
+ const rows = await pipe.exec();
692
+ const jobs = [];
693
+ for (let i = 0; i < ids.length; i++) {
694
+ const id = ids[i];
695
+ const raw = rows?.[i]?.[1] || {};
696
+ if (!raw || Object.keys(raw).length === 0) {
697
+ this.logger.warn(`Skipping child job ${id} - not found (likely cleaned up)`);
698
+ continue;
699
+ }
700
+ const job = Job.fromRawHash(this, id, raw);
701
+ jobs.push(job);
702
+ }
703
+ return jobs;
704
+ }
626
705
  async addSingle(opts) {
627
706
  const now = Date.now();
628
707
  let delayUntil = 0;
@@ -1317,6 +1396,91 @@ var Queue = class {
1317
1396
  async getJob(id) {
1318
1397
  return Job.fromStore(this, id);
1319
1398
  }
1399
+ async setupSubscriber() {
1400
+ if (this.eventsSubscribed && this.subscriber) return;
1401
+ if (!this.subscriber) {
1402
+ this.subscriber = this.r.duplicate();
1403
+ this.subscriber.on("message", (channel, message) => {
1404
+ if (channel === `${this.ns}:events`) this.handleJobEvent(message);
1405
+ });
1406
+ this.subscriber.on("error", (err) => {
1407
+ this.logger.error("Redis error (events subscriber):", err);
1408
+ });
1409
+ }
1410
+ await this.subscriber.subscribe(`${this.ns}:events`);
1411
+ this.eventsSubscribed = true;
1412
+ }
1413
+ handleJobEvent(message) {
1414
+ try {
1415
+ const event = safeJsonParse(message);
1416
+ if (!event || typeof event.id !== "string") return;
1417
+ const waiters = this.waitingJobs.get(event.id);
1418
+ if (!waiters || waiters.length === 0) return;
1419
+ if (event.status === "completed") {
1420
+ const parsed = typeof event.result === "string" ? safeJsonParse(event.result) ?? event.result : event.result;
1421
+ waiters.forEach((w) => w.resolve(parsed));
1422
+ } else if (event.status === "failed") {
1423
+ const info = typeof event.result === "string" ? safeJsonParse(event.result) ?? {} : event.result ?? {};
1424
+ const err = new Error(info && info.message || "Job failed");
1425
+ if (info && typeof info === "object") {
1426
+ if (typeof info.name === "string") err.name = info.name;
1427
+ if (typeof info.stack === "string") err.stack = info.stack;
1428
+ }
1429
+ waiters.forEach((w) => w.reject(err));
1430
+ }
1431
+ this.waitingJobs.delete(event.id);
1432
+ } catch (err) {
1433
+ this.logger.error("Failed to process job event:", err);
1434
+ }
1435
+ }
1436
+ /**
1437
+ * Wait for a job to complete or fail, similar to BullMQ's waitUntilFinished.
1438
+ */
1439
+ async waitUntilFinished(jobId, timeoutMs = 0) {
1440
+ const job = await this.getJob(jobId);
1441
+ const state = await job.getState();
1442
+ if (state === "completed") return job.returnvalue;
1443
+ if (state === "failed") throw new Error(job.failedReason || "Job failed");
1444
+ await this.setupSubscriber();
1445
+ return new Promise((resolve, reject) => {
1446
+ let timer;
1447
+ let waiter;
1448
+ const cleanup = () => {
1449
+ if (timer) clearTimeout(timer);
1450
+ const current = this.waitingJobs.get(jobId);
1451
+ if (!current) return;
1452
+ const remaining = current.filter((w) => w !== waiter);
1453
+ if (remaining.length === 0) this.waitingJobs.delete(jobId);
1454
+ else this.waitingJobs.set(jobId, remaining);
1455
+ };
1456
+ const wrappedResolve = (value) => {
1457
+ cleanup();
1458
+ resolve(value);
1459
+ };
1460
+ const wrappedReject = (err) => {
1461
+ cleanup();
1462
+ reject(err);
1463
+ };
1464
+ waiter = {
1465
+ resolve: wrappedResolve,
1466
+ reject: wrappedReject
1467
+ };
1468
+ const waiters = this.waitingJobs.get(jobId) ?? [];
1469
+ waiters.push(waiter);
1470
+ this.waitingJobs.set(jobId, waiters);
1471
+ if (timeoutMs > 0) timer = setTimeout(() => {
1472
+ wrappedReject(/* @__PURE__ */ new Error(`Timed out waiting for job ${jobId} to finish`));
1473
+ }, timeoutMs);
1474
+ (async () => {
1475
+ try {
1476
+ const latest = await this.getJob(jobId);
1477
+ const latestState = await latest.getState();
1478
+ if (latestState === "completed") wrappedResolve(latest.returnvalue);
1479
+ else if (latestState === "failed") wrappedReject(new Error(latest.failedReason ?? "Job failed"));
1480
+ } catch (_err) {}
1481
+ })();
1482
+ });
1483
+ }
1320
1484
  /**
1321
1485
  * Fetch jobs by statuses, emulating BullMQ's Queue.getJobs API used by BullBoard.
1322
1486
  * Only getter functionality; ordering is best-effort.
@@ -1511,6 +1675,25 @@ var Queue = class {
1511
1675
  await this.flushBatch();
1512
1676
  }
1513
1677
  await this.stopPromoter();
1678
+ if (this.subscriber) {
1679
+ try {
1680
+ await this.subscriber.unsubscribe(`${this.ns}:events`);
1681
+ await this.subscriber.quit();
1682
+ } catch (_err) {
1683
+ try {
1684
+ this.subscriber.disconnect();
1685
+ } catch (_e) {}
1686
+ }
1687
+ this.subscriber = void 0;
1688
+ this.eventsSubscribed = false;
1689
+ }
1690
+ if (this.waitingJobs.size > 0) {
1691
+ const err = /* @__PURE__ */ new Error("Queue closed");
1692
+ this.waitingJobs.forEach((waiters) => {
1693
+ waiters.forEach((w) => w.reject(err));
1694
+ });
1695
+ this.waitingJobs.clear();
1696
+ }
1514
1697
  try {
1515
1698
  await this.r.quit();
1516
1699
  } catch (_e) {
@@ -1938,6 +2121,12 @@ var AsyncFifoQueue = class {
1938
2121
 
1939
2122
  //#endregion
1940
2123
  //#region src/worker.ts
2124
+ var UnrecoverableError = class extends Error {
2125
+ constructor(message) {
2126
+ super(message);
2127
+ this.name = "UnrecoverableError";
2128
+ }
2129
+ };
1941
2130
  var TypedEventEmitter = class {
1942
2131
  constructor() {
1943
2132
  this.listeners = /* @__PURE__ */ new Map();
@@ -1973,7 +2162,7 @@ var TypedEventEmitter = class {
1973
2162
  return this;
1974
2163
  }
1975
2164
  };
1976
- const defaultBackoff = (attempt) => {
2165
+ const defaultBackoff = (attempt, _error) => {
1977
2166
  const base = Math.min(3e4, 2 ** (attempt - 1) * 500);
1978
2167
  const jitter = Math.floor(base * .25 * Math.random());
1979
2168
  return base + jitter;
@@ -2396,7 +2585,10 @@ var _Worker = class extends TypedEventEmitter {
2396
2585
  const oldest = Array.from(this.jobsInProgress)[0];
2397
2586
  const now = Date.now();
2398
2587
  return {
2399
- job: oldest.job,
2588
+ job: Job.fromReserved(this.q, oldest.job, {
2589
+ processedOn: oldest.ts,
2590
+ status: "active"
2591
+ }),
2400
2592
  processingTimeMs: now - oldest.ts
2401
2593
  };
2402
2594
  }
@@ -2406,7 +2598,10 @@ var _Worker = class extends TypedEventEmitter {
2406
2598
  getCurrentJobs() {
2407
2599
  const now = Date.now();
2408
2600
  return Array.from(this.jobsInProgress).map((item) => ({
2409
- job: item.job,
2601
+ job: Job.fromReserved(this.q, item.job, {
2602
+ processedOn: item.ts,
2603
+ status: "active"
2604
+ }),
2410
2605
  processingTimeMs: now - item.ts
2411
2606
  }));
2412
2607
  }
@@ -2436,7 +2631,7 @@ var _Worker = class extends TypedEventEmitter {
2436
2631
  } catch (e) {
2437
2632
  const isConnErr = this.q.isConnectionError(e);
2438
2633
  if (!isConnErr || !this.stopping) this.logger.error(`Heartbeat error for job ${job.id}:`, e instanceof Error ? e.message : String(e));
2439
- this.onError?.(e, job);
2634
+ this.onError?.(e, Job.fromReserved(this.q, job, { status: "active" }));
2440
2635
  if (!isConnErr || !this.stopping) this.emit("error", e instanceof Error ? e : new Error(String(e)));
2441
2636
  }
2442
2637
  }, minInterval);
@@ -2447,7 +2642,11 @@ var _Worker = class extends TypedEventEmitter {
2447
2642
  heartbeatDelayTimer = setTimeout(() => {
2448
2643
  startHeartbeat();
2449
2644
  }, heartbeatThreshold);
2450
- const handlerResult = await this.handler(job);
2645
+ const jobInstance = Job.fromReserved(this.q, job, {
2646
+ processedOn: jobStartWallTime,
2647
+ status: "active"
2648
+ });
2649
+ const handlerResult = await this.handler(jobInstance);
2451
2650
  if (heartbeatDelayTimer) clearTimeout(heartbeatDelayTimer);
2452
2651
  if (hbTimer) clearInterval(hbTimer);
2453
2652
  const finishedAtWall = Date.now();
@@ -2471,7 +2670,11 @@ var _Worker = class extends TypedEventEmitter {
2471
2670
  * Handle job failure: emit events, retry or dead-letter
2472
2671
  */
2473
2672
  async handleJobFailure(err, job, jobStartWallTime) {
2474
- this.onError?.(err, job);
2673
+ const jobInstance = Job.fromReserved(this.q, job, {
2674
+ processedOn: jobStartWallTime,
2675
+ status: "active"
2676
+ });
2677
+ this.onError?.(err, jobInstance);
2475
2678
  this.blockingStats.consecutiveEmptyReserves = 0;
2476
2679
  this.emptyReserveBackoffMs = 0;
2477
2680
  try {
@@ -2486,7 +2689,12 @@ var _Worker = class extends TypedEventEmitter {
2486
2689
  status: "failed"
2487
2690
  }));
2488
2691
  const nextAttempt = job.attempts + 1;
2489
- const backoffMs = this.backoff(nextAttempt);
2692
+ if (err instanceof UnrecoverableError) {
2693
+ this.logger.info(`Unrecoverable error for job ${job.id}: ${err instanceof Error ? err.message : String(err)}. Skipping retries.`);
2694
+ await this.deadLetterJob(err, job, jobStartWallTime, failedAt, nextAttempt);
2695
+ return;
2696
+ }
2697
+ const backoffMs = this.backoff(nextAttempt, err);
2490
2698
  if (nextAttempt >= this.maxAttempts) {
2491
2699
  await this.deadLetterJob(err, job, jobStartWallTime, failedAt, nextAttempt);
2492
2700
  return;
@@ -2553,5 +2761,5 @@ function sleep(ms) {
2553
2761
  }
2554
2762
 
2555
2763
  //#endregion
2556
- export { BullBoardGroupMQAdapter, Job, Queue, Worker, getWorkersStatus, waitForQueueToEmpty };
2764
+ export { BullBoardGroupMQAdapter, Job, Queue, UnrecoverableError, Worker, getWorkersStatus, waitForQueueToEmpty };
2557
2765
  //# sourceMappingURL=index.js.map