@zudojs/queue 0.1.0 → 1.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.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -8
  3. package/dist/deadLetter/deadLetter.core.js +1 -1
  4. package/dist/inMemoryQueue/inMemoryQueue.core.d.ts +95 -4
  5. package/dist/inMemoryQueue/inMemoryQueue.core.js +398 -73
  6. package/dist/inMemoryQueue/inMemoryQueue.processing.d.ts +53 -8
  7. package/dist/inMemoryQueue/inMemoryQueue.processing.js +83 -36
  8. package/dist/inMemoryQueue/inMemoryQueue.scheduling.d.ts +17 -1
  9. package/dist/inMemoryQueue/inMemoryQueue.scheduling.js +43 -11
  10. package/dist/index.d.ts +0 -1
  11. package/dist/index.js +0 -2
  12. package/dist/job/job.core.js +20 -12
  13. package/dist/job/job.type.d.ts +3 -2
  14. package/dist/jobOptions/jobOptions.type.d.ts +12 -3
  15. package/dist/jobTypes/index.d.ts +1 -1
  16. package/dist/jobTypes/jobTypes.type.d.ts +9 -0
  17. package/dist/middleware/middleware.core.d.ts +10 -1
  18. package/dist/middleware/middleware.core.js +40 -17
  19. package/dist/processor/index.d.ts +1 -1
  20. package/dist/processor/index.js +1 -1
  21. package/dist/processor/processor.core.js +2 -0
  22. package/dist/processor/processor.type.d.ts +18 -1
  23. package/dist/processor/processor.type.js +15 -0
  24. package/dist/queue/index.d.ts +1 -1
  25. package/dist/queue/queue.core.js +2 -2
  26. package/dist/queue/queue.type.d.ts +108 -2
  27. package/dist/queueEmitter/queueEmitter.core.d.ts +22 -2
  28. package/dist/queueEmitter/queueEmitter.core.js +47 -7
  29. package/dist/queueManager/queueManager.core.js +24 -5
  30. package/dist/queueRegistry/queueRegistry.core.js +15 -0
  31. package/dist/queueRegistry/queueRegistry.type.d.ts +2 -0
  32. package/dist/retryPolicy/retryPolicy.core.d.ts +42 -3
  33. package/dist/retryPolicy/retryPolicy.core.js +82 -13
  34. package/dist/serializer/serializer.core.d.ts +2 -1
  35. package/dist/worker/worker.core.d.ts +4 -0
  36. package/dist/worker/worker.core.js +87 -23
  37. package/dist/worker/worker.type.d.ts +17 -3
  38. package/package.json +21 -14
  39. package/dist/.tsbuildinfo +0 -1
  40. package/dist/deadLetter/deadLetter.core.d.ts.map +0 -1
  41. package/dist/deadLetter/deadLetter.core.js.map +0 -1
  42. package/dist/deadLetter/deadLetter.type.d.ts.map +0 -1
  43. package/dist/deadLetter/deadLetter.type.js.map +0 -1
  44. package/dist/deadLetter/index.d.ts.map +0 -1
  45. package/dist/deadLetter/index.js.map +0 -1
  46. package/dist/inMemoryQueue/inMemoryQueue.core.d.ts.map +0 -1
  47. package/dist/inMemoryQueue/inMemoryQueue.core.js.map +0 -1
  48. package/dist/inMemoryQueue/inMemoryQueue.factory.d.ts.map +0 -1
  49. package/dist/inMemoryQueue/inMemoryQueue.factory.js.map +0 -1
  50. package/dist/inMemoryQueue/inMemoryQueue.processing.d.ts.map +0 -1
  51. package/dist/inMemoryQueue/inMemoryQueue.processing.js.map +0 -1
  52. package/dist/inMemoryQueue/inMemoryQueue.scheduling.d.ts.map +0 -1
  53. package/dist/inMemoryQueue/inMemoryQueue.scheduling.js.map +0 -1
  54. package/dist/inMemoryQueue/index.d.ts.map +0 -1
  55. package/dist/inMemoryQueue/index.js.map +0 -1
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/index.js.map +0 -1
  58. package/dist/job/index.d.ts.map +0 -1
  59. package/dist/job/index.js.map +0 -1
  60. package/dist/job/job.core.d.ts.map +0 -1
  61. package/dist/job/job.core.js.map +0 -1
  62. package/dist/job/job.type.d.ts.map +0 -1
  63. package/dist/job/job.type.js.map +0 -1
  64. package/dist/jobContext/index.d.ts.map +0 -1
  65. package/dist/jobContext/index.js.map +0 -1
  66. package/dist/jobContext/jobContext.core.d.ts.map +0 -1
  67. package/dist/jobContext/jobContext.core.js.map +0 -1
  68. package/dist/jobContext/jobContext.type.d.ts.map +0 -1
  69. package/dist/jobContext/jobContext.type.js.map +0 -1
  70. package/dist/jobOptions/index.d.ts.map +0 -1
  71. package/dist/jobOptions/index.js.map +0 -1
  72. package/dist/jobOptions/jobOptions.core.d.ts.map +0 -1
  73. package/dist/jobOptions/jobOptions.core.js.map +0 -1
  74. package/dist/jobOptions/jobOptions.type.d.ts.map +0 -1
  75. package/dist/jobOptions/jobOptions.type.js.map +0 -1
  76. package/dist/jobResult/index.d.ts.map +0 -1
  77. package/dist/jobResult/index.js.map +0 -1
  78. package/dist/jobResult/jobResult.core.d.ts.map +0 -1
  79. package/dist/jobResult/jobResult.core.js.map +0 -1
  80. package/dist/jobResult/jobResult.type.d.ts.map +0 -1
  81. package/dist/jobResult/jobResult.type.js.map +0 -1
  82. package/dist/jobTypes/index.d.ts.map +0 -1
  83. package/dist/jobTypes/index.js.map +0 -1
  84. package/dist/jobTypes/jobTypes.type.d.ts.map +0 -1
  85. package/dist/jobTypes/jobTypes.type.js.map +0 -1
  86. package/dist/middleware/index.d.ts.map +0 -1
  87. package/dist/middleware/index.js.map +0 -1
  88. package/dist/middleware/middleware.core.d.ts.map +0 -1
  89. package/dist/middleware/middleware.core.js.map +0 -1
  90. package/dist/middleware/middleware.type.d.ts.map +0 -1
  91. package/dist/middleware/middleware.type.js.map +0 -1
  92. package/dist/processor/index.d.ts.map +0 -1
  93. package/dist/processor/index.js.map +0 -1
  94. package/dist/processor/processor.core.d.ts.map +0 -1
  95. package/dist/processor/processor.core.js.map +0 -1
  96. package/dist/processor/processor.type.d.ts.map +0 -1
  97. package/dist/processor/processor.type.js.map +0 -1
  98. package/dist/queue/index.d.ts.map +0 -1
  99. package/dist/queue/index.js.map +0 -1
  100. package/dist/queue/queue.core.d.ts.map +0 -1
  101. package/dist/queue/queue.core.js.map +0 -1
  102. package/dist/queue/queue.type.d.ts.map +0 -1
  103. package/dist/queue/queue.type.js.map +0 -1
  104. package/dist/queueEmitter/index.d.ts.map +0 -1
  105. package/dist/queueEmitter/index.js.map +0 -1
  106. package/dist/queueEmitter/queueEmitter.core.d.ts.map +0 -1
  107. package/dist/queueEmitter/queueEmitter.core.js.map +0 -1
  108. package/dist/queueEmitter/queueEmitter.type.d.ts.map +0 -1
  109. package/dist/queueEmitter/queueEmitter.type.js.map +0 -1
  110. package/dist/queueEvents/index.d.ts +0 -8
  111. package/dist/queueEvents/index.d.ts.map +0 -1
  112. package/dist/queueEvents/index.js +0 -7
  113. package/dist/queueEvents/index.js.map +0 -1
  114. package/dist/queueEvents/queueEvents.core.d.ts +0 -6
  115. package/dist/queueEvents/queueEvents.core.d.ts.map +0 -1
  116. package/dist/queueEvents/queueEvents.core.js +0 -12
  117. package/dist/queueEvents/queueEvents.core.js.map +0 -1
  118. package/dist/queueEvents/queueEvents.type.d.ts +0 -97
  119. package/dist/queueEvents/queueEvents.type.d.ts.map +0 -1
  120. package/dist/queueEvents/queueEvents.type.js +0 -2
  121. package/dist/queueEvents/queueEvents.type.js.map +0 -1
  122. package/dist/queueManager/index.d.ts.map +0 -1
  123. package/dist/queueManager/index.js.map +0 -1
  124. package/dist/queueManager/queueManager.core.d.ts.map +0 -1
  125. package/dist/queueManager/queueManager.core.js.map +0 -1
  126. package/dist/queueManager/queueManager.type.d.ts.map +0 -1
  127. package/dist/queueManager/queueManager.type.js.map +0 -1
  128. package/dist/queueRegistry/index.d.ts.map +0 -1
  129. package/dist/queueRegistry/index.js.map +0 -1
  130. package/dist/queueRegistry/queueRegistry.core.d.ts.map +0 -1
  131. package/dist/queueRegistry/queueRegistry.core.js.map +0 -1
  132. package/dist/queueRegistry/queueRegistry.type.d.ts.map +0 -1
  133. package/dist/queueRegistry/queueRegistry.type.js.map +0 -1
  134. package/dist/retryPolicy/index.d.ts.map +0 -1
  135. package/dist/retryPolicy/index.js.map +0 -1
  136. package/dist/retryPolicy/retryPolicy.core.d.ts.map +0 -1
  137. package/dist/retryPolicy/retryPolicy.core.js.map +0 -1
  138. package/dist/serializer/index.d.ts.map +0 -1
  139. package/dist/serializer/index.js.map +0 -1
  140. package/dist/serializer/serializer.core.d.ts.map +0 -1
  141. package/dist/serializer/serializer.core.js.map +0 -1
  142. package/dist/serializer/serializer.type.d.ts.map +0 -1
  143. package/dist/serializer/serializer.type.js.map +0 -1
  144. package/dist/worker/index.d.ts.map +0 -1
  145. package/dist/worker/index.js.map +0 -1
  146. package/dist/worker/worker.core.d.ts.map +0 -1
  147. package/dist/worker/worker.core.js.map +0 -1
  148. package/dist/worker/worker.type.d.ts.map +0 -1
  149. package/dist/worker/worker.type.js.map +0 -1
@@ -5,6 +5,6 @@
5
5
  * and managing job queues.
6
6
  */
7
7
  export { createQueue, isQueue } from "./queue.core.js";
8
- export type { Queue, QueueOptions, QueueStats, QueueEventMap, } from "./queue.type.js";
8
+ export type { Queue, QueueLogger, QueueOptions, QueueStats, QueueEventMap, } from "./queue.type.js";
9
9
  export type { QueueEventEmitter } from "../queueEmitter/queueEmitter.type.js";
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,3 @@
1
- import { QueueClosedError, QueueDisposedError } from "@zudojs/errors";
2
1
  import { createInMemoryQueue } from "../inMemoryQueue/index.js";
3
2
  /**
4
3
  * Creates a new queue.
@@ -18,6 +17,7 @@ export function isQueue(value) {
18
17
  "add" in value &&
19
18
  "process" in value &&
20
19
  "getJob" in value &&
21
- "getNextJob" in value);
20
+ "getNextJob" in value &&
21
+ "claimNextJob" in value);
22
22
  }
23
23
  //# sourceMappingURL=queue.core.js.map
@@ -5,10 +5,26 @@ import type { Processor } from "../processor/processor.type.js";
5
5
  import type { Serializer } from "../serializer/serializer.type.js";
6
6
  import type { QueueMiddleware } from "../middleware/middleware.type.js";
7
7
  import type { QueueEventEmitter } from "../queueEmitter/queueEmitter.type.js";
8
+ import type { DeadLetterJob, DeadLetterStore } from "../deadLetter/deadLetter.type.js";
9
+ /**
10
+ * Somewhere for a job to write a log line.
11
+ *
12
+ * Structurally compatible with `@zudojs/logger` and with `console`.
13
+ */
14
+ export interface QueueLogger {
15
+ info(message: string, data?: Record<string, unknown>): void;
16
+ }
8
17
  /**
9
18
  * Options for creating a queue.
10
19
  */
11
20
  export interface QueueOptions {
21
+ /**
22
+ * Receives lines a processor writes with `context.log()`.
23
+ *
24
+ * Without one, `JobContext.log` is a documented method that discards
25
+ * everything handed to it.
26
+ */
27
+ readonly logger?: QueueLogger;
12
28
  /** Queue concurrency limit. */
13
29
  readonly concurrency?: number;
14
30
  /** Default job options. */
@@ -21,6 +37,48 @@ export interface QueueOptions {
21
37
  readonly pollInterval?: number;
22
38
  /** Event emitter for queue lifecycle events. */
23
39
  readonly eventEmitter?: QueueEventEmitter;
40
+ /** Store that receives jobs which exhausted their attempts. */
41
+ readonly deadLetterStore?: DeadLetterStore<never>;
42
+ /**
43
+ * Whether `add()` rejects while the queue is paused.
44
+ *
45
+ * Defaults to `true`. Set to `false` for the conventional
46
+ * producer/consumer split, where pausing stops consumption only and
47
+ * producers may keep enqueuing.
48
+ */
49
+ readonly pauseRejectsAdd?: boolean;
50
+ /**
51
+ * Whether job payloads are round-tripped through the serializer on
52
+ * `add()`. Defaults to `true`, which isolates stored payloads from
53
+ * later caller mutation and rejects non-serializable payloads up
54
+ * front. Set to `false` to store payloads by reference.
55
+ */
56
+ readonly serializePayloads?: boolean;
57
+ /**
58
+ * Number of terminal (completed, failed, dead-lettered) jobs retained
59
+ * before the oldest are evicted. Defaults to 1000. Without a bound the
60
+ * queue grows for the life of the process.
61
+ */
62
+ readonly retainSettledJobs?: number;
63
+ /**
64
+ * How long `close()` waits for in-flight jobs before aborting them,
65
+ * in milliseconds. Defaults to 30000.
66
+ */
67
+ readonly closeTimeout?: number;
68
+ /**
69
+ * How long a job may sit in `active` without a live consumer before it
70
+ * is reclaimed, in milliseconds. Defaults to `0` (disabled).
71
+ *
72
+ * Guards against a consumer that claimed a job and then died, which
73
+ * would otherwise leave the job `active` with nothing able to retry
74
+ * it.
75
+ */
76
+ readonly stalledAfter?: number;
77
+ /**
78
+ * How many times a job may stall before it is dead-lettered instead of
79
+ * reclaimed again. Defaults to 3.
80
+ */
81
+ readonly maxStalledCount?: number;
24
82
  }
25
83
  /**
26
84
  * Statistics for a queue.
@@ -38,6 +96,16 @@ export interface QueueStats {
38
96
  readonly delayed: number;
39
97
  /** Number of retrying jobs. */
40
98
  readonly retrying: number;
99
+ /** Total jobs dispatched over the queue's lifetime. */
100
+ readonly processed: number;
101
+ /** Total jobs that completed successfully. */
102
+ readonly succeeded: number;
103
+ /** Total jobs that failed terminally. */
104
+ readonly errored: number;
105
+ /** Total retry attempts scheduled. */
106
+ readonly retried: number;
107
+ /** Total jobs moved to the dead letter store. */
108
+ readonly deadLettered: number;
41
109
  }
42
110
  /**
43
111
  * A named stream of jobs.
@@ -51,8 +119,42 @@ export interface Queue<TData = unknown> {
51
119
  process(name: string, processor: Processor<TData>): void;
52
120
  /** Get a job by ID. */
53
121
  getJob(jobId: JobId): Promise<Job<TData> | null>;
54
- /** Get the next available job for processing. */
122
+ /**
123
+ * Peeks at the job that would be processed next, without claiming it.
124
+ *
125
+ * Consumers that intend to run the job must use {@link claimNextJob};
126
+ * peeking leaves the job `waiting`, so two consumers polling with this
127
+ * method would both run it.
128
+ */
55
129
  getNextJob(): Promise<Job<TData> | null>;
130
+ /**
131
+ * Atomically claims the next runnable job, marking it `active`.
132
+ *
133
+ * Returns `null` when nothing is runnable — including when a waiting
134
+ * job has no registered processor.
135
+ */
136
+ claimNextJob(): Promise<Job<TData> | null>;
137
+ /**
138
+ * Returns a claimed job to the waiting pool.
139
+ *
140
+ * Used when a consumer claims a job it turns out it cannot run, so the
141
+ * job is retried by another consumer rather than stranded in `active`.
142
+ */
143
+ releaseJob(jobId: JobId): Promise<boolean>;
144
+ /**
145
+ * Runs an already-claimed job through the queue's processing pipeline.
146
+ *
147
+ * The queue owns job state, so consumers must dispatch through this
148
+ * rather than invoking a processor themselves — doing so would leave
149
+ * the job stuck in `active` and skip retry, dead-lettering and
150
+ * middleware.
151
+ */
152
+ runJob(job: Job<TData>, options?: {
153
+ middleware?: readonly QueueMiddleware[];
154
+ signal?: AbortSignal;
155
+ /** Fallback timeout for a job that carries none of its own. */
156
+ timeoutMs?: number;
157
+ }): Promise<void>;
56
158
  /** Get a registered processor by job name. */
57
159
  getProcessor(name: string): Processor<TData> | undefined;
58
160
  /** Get queue statistics. */
@@ -63,7 +165,11 @@ export interface Queue<TData = unknown> {
63
165
  resume(): Promise<void>;
64
166
  /** Check if the queue is paused. */
65
167
  isPaused(): boolean;
66
- /** Close the queue. */
168
+ /** Check if the queue has been closed. */
169
+ isDisposed(): boolean;
170
+ /** Get the jobs that exhausted their attempts. */
171
+ getDeadLetterJobs(): Promise<readonly DeadLetterJob<TData>[]>;
172
+ /** Close the queue, draining in-flight jobs first. */
67
173
  close(): Promise<void>;
68
174
  }
69
175
  /**
@@ -2,20 +2,40 @@ import type { QueueEventEmitter } from "./queueEmitter.type.js";
2
2
  import type { QueueEventMap } from "../queue/queue.type.js";
3
3
  type EventName = keyof QueueEventMap;
4
4
  type Handler<T extends EventName> = (data: QueueEventMap[T]) => void;
5
+ /**
6
+ * Options for the in-memory queue event emitter.
7
+ */
8
+ export interface QueueEventEmitterOptions {
9
+ /**
10
+ * Invoked when a handler throws. Defaults to reporting the error on the
11
+ * next tick so it surfaces in normal error reporting without unwinding
12
+ * the emitting code path.
13
+ */
14
+ readonly onHandlerError?: (error: unknown, event: EventName) => void;
15
+ }
5
16
  /**
6
17
  * In-memory queue event emitter.
7
18
  *
8
- * Stores handlers in memory and emits events synchronously.
19
+ * Stores handlers in memory and emits events synchronously. A handler
20
+ * that throws is isolated: the remaining handlers still run, and the
21
+ * failure never propagates back into queue processing, where it would be
22
+ * misreported as a job failure.
9
23
  */
10
24
  export declare class InMemoryQueueEventEmitter implements QueueEventEmitter {
11
25
  private readonly handlers;
26
+ private readonly onHandlerError;
27
+ constructor(options?: QueueEventEmitterOptions);
12
28
  emit<K extends EventName>(event: K, data: QueueEventMap[K]): void;
13
29
  on<K extends EventName>(event: K, handler: Handler<K>): () => void;
30
+ /**
31
+ * Removes every registered handler.
32
+ */
33
+ removeAllListeners(): void;
14
34
  }
15
35
  /**
16
36
  * Creates an in-memory queue event emitter.
17
37
  */
18
- export declare function createInMemoryQueueEventEmitter(): QueueEventEmitter;
38
+ export declare function createInMemoryQueueEventEmitter(options?: QueueEventEmitterOptions): QueueEventEmitter;
19
39
  /**
20
40
  * Creates a no-op queue event emitter.
21
41
  */
@@ -1,16 +1,37 @@
1
1
  /**
2
2
  * In-memory queue event emitter.
3
3
  *
4
- * Stores handlers in memory and emits events synchronously.
4
+ * Stores handlers in memory and emits events synchronously. A handler
5
+ * that throws is isolated: the remaining handlers still run, and the
6
+ * failure never propagates back into queue processing, where it would be
7
+ * misreported as a job failure.
5
8
  */
6
9
  export class InMemoryQueueEventEmitter {
7
10
  handlers = new Map();
11
+ onHandlerError;
12
+ constructor(options = {}) {
13
+ this.onHandlerError =
14
+ options.onHandlerError ??
15
+ ((error, event) => {
16
+ queueMicrotask(() => {
17
+ console.error(`[@zudojs/queue] Listener for "${event}" threw.`, error);
18
+ });
19
+ });
20
+ }
8
21
  emit(event, data) {
9
22
  const handlers = this.handlers.get(event);
10
- if (handlers) {
11
- for (const handler of handlers) {
23
+ if (!handlers) {
24
+ return;
25
+ }
26
+ // Snapshot so a handler that subscribes or unsubscribes during
27
+ // dispatch cannot mutate the set being iterated.
28
+ for (const handler of [...handlers]) {
29
+ try {
12
30
  handler(data);
13
31
  }
32
+ catch (error) {
33
+ this.onHandlerError(error, event);
34
+ }
14
35
  }
15
36
  }
16
37
  on(event, handler) {
@@ -19,17 +40,36 @@ export class InMemoryQueueEventEmitter {
19
40
  handlers = new Set();
20
41
  this.handlers.set(event, handlers);
21
42
  }
22
- handlers.add(handler);
43
+ const registered = handler;
44
+ handlers.add(registered);
45
+ let removed = false;
23
46
  return () => {
24
- handlers.delete(handler);
47
+ if (removed) {
48
+ return;
49
+ }
50
+ removed = true;
51
+ const current = this.handlers.get(event);
52
+ if (!current) {
53
+ return;
54
+ }
55
+ current.delete(registered);
56
+ if (current.size === 0) {
57
+ this.handlers.delete(event);
58
+ }
25
59
  };
26
60
  }
61
+ /**
62
+ * Removes every registered handler.
63
+ */
64
+ removeAllListeners() {
65
+ this.handlers.clear();
66
+ }
27
67
  }
28
68
  /**
29
69
  * Creates an in-memory queue event emitter.
30
70
  */
31
- export function createInMemoryQueueEventEmitter() {
32
- return new InMemoryQueueEventEmitter();
71
+ export function createInMemoryQueueEventEmitter(options) {
72
+ return new InMemoryQueueEventEmitter(options);
33
73
  }
34
74
  /**
35
75
  * Creates a no-op queue event emitter.
@@ -1,3 +1,4 @@
1
+ import { QueueError } from "@zudojs/errors";
1
2
  import { createQueue } from "../queue/queue.core.js";
2
3
  /**
3
4
  * Creates a new QueueManager.
@@ -5,12 +6,23 @@ import { createQueue } from "../queue/queue.core.js";
5
6
  export function createQueueManager() {
6
7
  const queues = new Map();
7
8
  return {
9
+ /**
10
+ * Returns the queue with this name, creating it on first request.
11
+ *
12
+ * `options` apply only when the queue is created. Passing different
13
+ * options for an existing queue throws rather than silently
14
+ * returning a queue configured some other way.
15
+ */
8
16
  getQueue(name, options) {
9
- let queue = queues.get(name);
10
- if (!queue) {
11
- queue = createQueue(name, options);
12
- queues.set(name, queue);
17
+ const existing = queues.get(name);
18
+ if (existing) {
19
+ if (options !== undefined) {
20
+ throw new QueueError(`Queue "${name}" already exists; options are only applied when a queue is created.`, { queueName: name });
21
+ }
22
+ return existing;
13
23
  }
24
+ const queue = createQueue(name, options);
25
+ queues.set(name, queue);
14
26
  return queue;
15
27
  },
16
28
  getExistingQueue(name) {
@@ -22,10 +34,17 @@ export function createQueueManager() {
22
34
  getQueueNames() {
23
35
  return Array.from(queues.keys());
24
36
  },
37
+ /**
38
+ * Closes every queue, clearing the registry even if some fail.
39
+ */
25
40
  async closeAll() {
26
41
  const closePromises = Array.from(queues.values()).map((queue) => queue.close());
27
- await Promise.all(closePromises);
28
42
  queues.clear();
43
+ const results = await Promise.allSettled(closePromises);
44
+ const failure = results.find((result) => result.status === "rejected");
45
+ if (failure && failure.status === "rejected") {
46
+ throw failure.reason;
47
+ }
29
48
  },
30
49
  };
31
50
  }
@@ -28,11 +28,26 @@ export function createQueueRegistry() {
28
28
  return Array.from(infoMap.values());
29
29
  },
30
30
  unregister(name) {
31
+ const queue = queues.get(name);
31
32
  const deleted = queues.delete(name);
32
33
  infoMap.delete(name);
34
+ // Dropping the reference without closing would leave the queue's
35
+ // poll timer running with nothing able to stop it.
36
+ if (queue) {
37
+ void queue.close().catch(() => { });
38
+ }
33
39
  return deleted;
34
40
  },
41
+ async closeAll() {
42
+ const pending = Array.from(queues.values()).map((queue) => queue.close());
43
+ queues.clear();
44
+ infoMap.clear();
45
+ await Promise.allSettled(pending);
46
+ },
35
47
  clear() {
48
+ for (const queue of queues.values()) {
49
+ void queue.close().catch(() => { });
50
+ }
36
51
  queues.clear();
37
52
  infoMap.clear();
38
53
  },
@@ -15,6 +15,8 @@ export interface QueueInfo {
15
15
  * Registry for queue instances.
16
16
  */
17
17
  export interface QueueRegistry {
18
+ /** Close every registered queue and clear the registry. */
19
+ closeAll(): Promise<void>;
18
20
  /** Register a queue. */
19
21
  register<TData>(queue: Queue<TData>): void;
20
22
  /** Get a queue by name. */
@@ -1,9 +1,44 @@
1
- import { BackoffType } from "../jobTypes/jobTypes.type.js";
1
+ import type { BackoffStrategy } from "../jobTypes/jobTypes.type.js";
2
2
  import type { BackoffOptions } from "../jobOptions/jobOptions.type.js";
3
+ /**
4
+ * Largest delay Node's timer subsystem accepts.
5
+ *
6
+ * A `setTimeout` delay above this overflows a signed 32-bit integer; Node
7
+ * warns and clamps it to `1`, turning a long backoff into an immediate
8
+ * retry. Every computed delay is clamped to this ceiling instead.
9
+ */
10
+ export declare const MAX_TIMER_DELAY = 2147483647;
11
+ /**
12
+ * Backoff applied to a retryable job that declares no backoff of its own.
13
+ *
14
+ * Without this a job configured with `attempts > 1` and no `backoff` would
15
+ * retry with a zero delay, hammering whatever dependency just failed.
16
+ */
17
+ export declare const DEFAULT_RETRY_BACKOFF: BackoffOptions;
18
+ /**
19
+ * Applies the configured jitter strategy to a computed delay.
20
+ *
21
+ * `full` spreads retries uniformly across `[0, delay]`; `equal` keeps half
22
+ * the delay fixed and randomises the other half. Both break up the
23
+ * synchronised retry waves that a purely deterministic backoff produces
24
+ * when many jobs fail against the same dependency at the same moment.
25
+ */
26
+ export declare function applyJitter(delay: number, jitter: BackoffOptions["jitter"], random?: () => number): number;
3
27
  /**
4
28
  * Calculates the delay for the next retry attempt.
29
+ *
30
+ * The result is always a finite, non-negative number no greater than
31
+ * {@link MAX_TIMER_DELAY}. Jitter is applied only when the backoff opts
32
+ * into it, so a backoff without `jitter` stays deterministic.
5
33
  */
6
34
  export declare function calculateRetryDelay(attempt: number, backoff?: BackoffOptions): number;
35
+ /**
36
+ * Resolves the backoff to use for a job.
37
+ *
38
+ * Falls back to {@link DEFAULT_RETRY_BACKOFF} so that a retryable job
39
+ * never retries with a zero delay.
40
+ */
41
+ export declare function resolveBackoff(backoff?: BackoffOptions): BackoffOptions;
7
42
  /**
8
43
  * Checks if a job should be retried based on its state.
9
44
  */
@@ -11,19 +46,23 @@ export declare function shouldRetry(attempt: number, maxAttempts: number): boole
11
46
  /**
12
47
  * Creates a backoff options object.
13
48
  */
14
- export declare function createBackoffOptions(type: BackoffType, delay: number, options?: {
49
+ export declare function createBackoffOptions(type: BackoffStrategy, delay: number, options?: {
15
50
  maxDelay?: number;
16
51
  multiplier?: number;
52
+ jitter?: BackoffOptions["jitter"];
17
53
  }): BackoffOptions;
18
54
  /**
19
55
  * Creates a fixed backoff options.
20
56
  */
21
- export declare function createFixedBackoff(delay: number): BackoffOptions;
57
+ export declare function createFixedBackoff(delay: number, options?: {
58
+ jitter?: BackoffOptions["jitter"];
59
+ }): BackoffOptions;
22
60
  /**
23
61
  * Creates an exponential backoff options.
24
62
  */
25
63
  export declare function createExponentialBackoff(delay: number, options?: {
26
64
  maxDelay?: number;
27
65
  multiplier?: number;
66
+ jitter?: BackoffOptions["jitter"];
28
67
  }): BackoffOptions;
29
68
  //# sourceMappingURL=retryPolicy.core.d.ts.map
@@ -1,23 +1,91 @@
1
1
  import { BackoffType } from "../jobTypes/jobTypes.type.js";
2
+ /**
3
+ * Largest delay Node's timer subsystem accepts.
4
+ *
5
+ * A `setTimeout` delay above this overflows a signed 32-bit integer; Node
6
+ * warns and clamps it to `1`, turning a long backoff into an immediate
7
+ * retry. Every computed delay is clamped to this ceiling instead.
8
+ */
9
+ export const MAX_TIMER_DELAY = 2_147_483_647;
10
+ /**
11
+ * Backoff applied to a retryable job that declares no backoff of its own.
12
+ *
13
+ * Without this a job configured with `attempts > 1` and no `backoff` would
14
+ * retry with a zero delay, hammering whatever dependency just failed.
15
+ */
16
+ export const DEFAULT_RETRY_BACKOFF = Object.freeze({
17
+ type: BackoffType.EXPONENTIAL,
18
+ delay: 1_000,
19
+ maxDelay: 30_000,
20
+ multiplier: 2,
21
+ jitter: "full",
22
+ });
23
+ /**
24
+ * Clamps a delay to the range a timer can actually represent.
25
+ */
26
+ function clampDelay(delay) {
27
+ if (!Number.isFinite(delay) || delay < 0) {
28
+ return 0;
29
+ }
30
+ return Math.min(delay, MAX_TIMER_DELAY);
31
+ }
32
+ /**
33
+ * Applies the configured jitter strategy to a computed delay.
34
+ *
35
+ * `full` spreads retries uniformly across `[0, delay]`; `equal` keeps half
36
+ * the delay fixed and randomises the other half. Both break up the
37
+ * synchronised retry waves that a purely deterministic backoff produces
38
+ * when many jobs fail against the same dependency at the same moment.
39
+ */
40
+ export function applyJitter(delay, jitter, random = Math.random) {
41
+ if (delay <= 0) {
42
+ return 0;
43
+ }
44
+ switch (jitter) {
45
+ case "full":
46
+ return Math.round(random() * delay);
47
+ case "equal":
48
+ return Math.round(delay / 2 + random() * (delay / 2));
49
+ default:
50
+ return delay;
51
+ }
52
+ }
2
53
  /**
3
54
  * Calculates the delay for the next retry attempt.
55
+ *
56
+ * The result is always a finite, non-negative number no greater than
57
+ * {@link MAX_TIMER_DELAY}. Jitter is applied only when the backoff opts
58
+ * into it, so a backoff without `jitter` stays deterministic.
4
59
  */
5
60
  export function calculateRetryDelay(attempt, backoff) {
6
61
  if (!backoff) {
7
62
  return 0;
8
63
  }
9
- const { type, delay, maxDelay, multiplier } = backoff;
10
- switch (type) {
11
- case BackoffType.FIXED:
12
- return delay;
13
- case BackoffType.EXPONENTIAL: {
14
- const multiplierValue = multiplier ?? 2;
15
- const calculatedDelay = delay * Math.pow(multiplierValue, attempt - 1);
16
- return maxDelay ? Math.min(calculatedDelay, maxDelay) : calculatedDelay;
64
+ const { type, delay, maxDelay, multiplier, jitter } = backoff;
65
+ const base = () => {
66
+ switch (type) {
67
+ case BackoffType.FIXED:
68
+ return delay;
69
+ case BackoffType.EXPONENTIAL: {
70
+ const multiplierValue = multiplier ?? 2;
71
+ const exponent = Math.max(0, attempt - 1);
72
+ const calculatedDelay = delay * Math.pow(multiplierValue, exponent);
73
+ return maxDelay ? Math.min(calculatedDelay, maxDelay) : calculatedDelay;
74
+ }
75
+ default:
76
+ return delay;
17
77
  }
18
- default:
19
- return delay;
20
- }
78
+ };
79
+ return clampDelay(applyJitter(clampDelay(base()), jitter));
80
+ }
81
+ /**
82
+ * Resolves the backoff to use for a job.
83
+ *
84
+ * Falls back to {@link DEFAULT_RETRY_BACKOFF} so that a retryable job
85
+ * never retries with a zero delay.
86
+ */
87
+ export function resolveBackoff(backoff) {
88
+ return backoff ?? DEFAULT_RETRY_BACKOFF;
21
89
  }
22
90
  /**
23
91
  * Checks if a job should be retried based on its state.
@@ -34,13 +102,14 @@ export function createBackoffOptions(type, delay, options) {
34
102
  delay,
35
103
  maxDelay: options?.maxDelay,
36
104
  multiplier: options?.multiplier,
105
+ jitter: options?.jitter,
37
106
  };
38
107
  }
39
108
  /**
40
109
  * Creates a fixed backoff options.
41
110
  */
42
- export function createFixedBackoff(delay) {
43
- return createBackoffOptions(BackoffType.FIXED, delay);
111
+ export function createFixedBackoff(delay, options) {
112
+ return createBackoffOptions(BackoffType.FIXED, delay, options);
44
113
  }
45
114
  /**
46
115
  * Creates an exponential backoff options.
@@ -10,8 +10,9 @@ export declare const JsonSerializer: Serializer;
10
10
  * Creates a serializer with custom options.
11
11
  */
12
12
  export declare function createJsonSerializer(options?: {
13
- replacer?: (key: string, value: unknown) => unknown;
13
+ /** Indentation width. Any value turns on pretty-printing. */
14
14
  space?: number;
15
+ /** Preserve BigInt, Date, Map, Set and Uint8Array across the round trip. */
15
16
  preserveTypes?: boolean;
16
17
  }): Serializer;
17
18
  /** No-op serializer that passes data through unchanged. */
@@ -2,6 +2,10 @@ import type { Queue } from "../queue/queue.type.js";
2
2
  import type { Worker, WorkerOptions } from "./worker.type.js";
3
3
  /**
4
4
  * Creates a new Worker.
5
+ *
6
+ * The worker claims each job before running it, so a job is never picked
7
+ * up twice — by this worker on its next poll, or by another worker on the
8
+ * same queue.
5
9
  */
6
10
  export declare function createWorker<TData>(id: string, queue: Queue<TData>, options?: WorkerOptions): Worker<TData>;
7
11
  /**