@zudojs/queue 1.5.1 → 1.6.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.
- package/dist/contextCarrier/contextCarrier.core.d.ts +0 -1
- package/dist/contextCarrier/contextCarrier.core.js +0 -1
- package/dist/contextCarrier/contextCarrier.type.d.ts +0 -1
- package/dist/contextCarrier/contextCarrier.type.js +0 -1
- package/dist/contextCarrier/index.d.ts +0 -1
- package/dist/contextCarrier/index.js +0 -1
- package/dist/deadLetter/deadLetter.core.d.ts +0 -1
- package/dist/deadLetter/deadLetter.core.js +0 -1
- package/dist/deadLetter/deadLetter.type.d.ts +0 -1
- package/dist/deadLetter/deadLetter.type.js +0 -1
- package/dist/deadLetter/index.d.ts +0 -1
- package/dist/deadLetter/index.js +0 -1
- package/dist/inMemoryQueue/inMemoryQueue.core.d.ts +0 -1
- package/dist/inMemoryQueue/inMemoryQueue.core.js +1 -1
- package/dist/inMemoryQueue/inMemoryQueue.factory.d.ts +0 -1
- package/dist/inMemoryQueue/inMemoryQueue.factory.js +0 -1
- package/dist/inMemoryQueue/inMemoryQueue.processing.d.ts +19 -3
- package/dist/inMemoryQueue/inMemoryQueue.processing.js +37 -15
- package/dist/inMemoryQueue/inMemoryQueue.scheduling.d.ts +0 -1
- package/dist/inMemoryQueue/inMemoryQueue.scheduling.js +0 -1
- package/dist/inMemoryQueue/inMemoryQueue.settle.d.ts +0 -1
- package/dist/inMemoryQueue/inMemoryQueue.settle.js +0 -1
- package/dist/inMemoryQueue/index.d.ts +0 -1
- package/dist/inMemoryQueue/index.js +0 -1
- package/dist/inMemoryQueue/polling/inMemoryQueue.poller.d.ts +0 -1
- package/dist/inMemoryQueue/polling/inMemoryQueue.poller.js +0 -1
- package/dist/inMemoryQueue/polling/inMemoryQueue.select.d.ts +5 -1
- package/dist/inMemoryQueue/polling/inMemoryQueue.select.js +33 -10
- package/dist/inMemoryQueue/polling/index.d.ts +0 -1
- package/dist/inMemoryQueue/polling/index.js +0 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/job/index.d.ts +0 -1
- package/dist/job/index.js +0 -1
- package/dist/job/job.core.d.ts +0 -1
- package/dist/job/job.core.js +0 -1
- package/dist/job/job.type.d.ts +0 -1
- package/dist/job/job.type.js +0 -1
- package/dist/jobContext/index.d.ts +0 -1
- package/dist/jobContext/index.js +0 -1
- package/dist/jobContext/jobContext.core.d.ts +0 -1
- package/dist/jobContext/jobContext.core.js +0 -1
- package/dist/jobContext/jobContext.type.d.ts +0 -1
- package/dist/jobContext/jobContext.type.js +0 -1
- package/dist/jobFailure/index.d.ts +8 -0
- package/dist/jobFailure/index.js +7 -0
- package/dist/jobFailure/jobFailure.unrecoverable.d.ts +51 -0
- package/dist/jobFailure/jobFailure.unrecoverable.js +65 -0
- package/dist/jobOptions/index.d.ts +0 -1
- package/dist/jobOptions/index.js +0 -1
- package/dist/jobOptions/jobOptions.core.d.ts +0 -1
- package/dist/jobOptions/jobOptions.core.js +0 -1
- package/dist/jobOptions/jobOptions.type.d.ts +0 -1
- package/dist/jobOptions/jobOptions.type.js +0 -1
- package/dist/jobResult/index.d.ts +0 -1
- package/dist/jobResult/index.js +0 -1
- package/dist/jobResult/jobResult.core.d.ts +8 -2
- package/dist/jobResult/jobResult.core.js +7 -2
- package/dist/jobResult/jobResult.type.d.ts +6 -1
- package/dist/jobResult/jobResult.type.js +0 -1
- package/dist/jobTypes/index.d.ts +0 -1
- package/dist/jobTypes/index.js +0 -1
- package/dist/jobTypes/jobTypes.type.d.ts +0 -1
- package/dist/jobTypes/jobTypes.type.js +0 -1
- package/dist/middleware/index.d.ts +0 -1
- package/dist/middleware/index.js +0 -1
- package/dist/middleware/middleware.core.d.ts +0 -1
- package/dist/middleware/middleware.core.js +0 -1
- package/dist/middleware/middleware.type.d.ts +0 -1
- package/dist/middleware/middleware.type.js +0 -1
- package/dist/processor/index.d.ts +0 -1
- package/dist/processor/index.js +0 -1
- package/dist/processor/processor.core.d.ts +0 -1
- package/dist/processor/processor.core.js +0 -1
- package/dist/processor/processor.type.d.ts +0 -1
- package/dist/processor/processor.type.js +0 -1
- package/dist/queue/index.d.ts +0 -1
- package/dist/queue/index.js +0 -1
- package/dist/queue/queue.core.d.ts +0 -1
- package/dist/queue/queue.core.js +0 -1
- package/dist/queue/queue.report.d.ts +0 -1
- package/dist/queue/queue.report.js +0 -1
- package/dist/queue/queue.type.d.ts +6 -1
- package/dist/queue/queue.type.js +0 -1
- package/dist/queueEmitter/index.d.ts +0 -1
- package/dist/queueEmitter/index.js +0 -1
- package/dist/queueEmitter/queueEmitter.core.d.ts +0 -1
- package/dist/queueEmitter/queueEmitter.core.js +0 -1
- package/dist/queueEmitter/queueEmitter.type.d.ts +0 -1
- package/dist/queueEmitter/queueEmitter.type.js +0 -1
- package/dist/queueErrors/index.d.ts +7 -0
- package/dist/queueErrors/index.js +6 -0
- package/dist/queueErrors/queueError.base.d.ts +13 -0
- package/dist/queueErrors/queueError.base.js +12 -0
- package/dist/queueManager/index.d.ts +0 -1
- package/dist/queueManager/index.js +0 -1
- package/dist/queueManager/queueManager.core.d.ts +0 -1
- package/dist/queueManager/queueManager.core.js +0 -1
- package/dist/queueManager/queueManager.type.d.ts +0 -1
- package/dist/queueManager/queueManager.type.js +0 -1
- package/dist/queueRegistry/index.d.ts +0 -1
- package/dist/queueRegistry/index.js +0 -1
- package/dist/queueRegistry/queueRegistry.core.d.ts +0 -1
- package/dist/queueRegistry/queueRegistry.core.js +0 -1
- package/dist/queueRegistry/queueRegistry.type.d.ts +0 -1
- package/dist/queueRegistry/queueRegistry.type.js +0 -1
- package/dist/retryPolicy/index.d.ts +1 -2
- package/dist/retryPolicy/index.js +1 -2
- package/dist/retryPolicy/retryPolicy.core.d.ts +7 -2
- package/dist/retryPolicy/retryPolicy.core.js +8 -3
- package/dist/serializer/index.d.ts +0 -1
- package/dist/serializer/index.js +0 -1
- package/dist/serializer/serializer.core.d.ts +0 -1
- package/dist/serializer/serializer.core.js +0 -1
- package/dist/serializer/serializer.type.d.ts +0 -1
- package/dist/serializer/serializer.type.js +0 -1
- package/dist/worker/index.d.ts +0 -1
- package/dist/worker/index.js +0 -1
- package/dist/worker/worker.core.d.ts +0 -1
- package/dist/worker/worker.core.js +0 -1
- package/dist/worker/worker.type.d.ts +0 -1
- package/dist/worker/worker.type.js +0 -1
- package/package.json +4 -4
|
@@ -33,4 +33,3 @@ export declare function captureContext(carriers: readonly QueueContextCarrier[]
|
|
|
33
33
|
* @returns Whatever `fn` resolves to.
|
|
34
34
|
*/
|
|
35
35
|
export declare function runWithContext<T>(carriers: readonly QueueContextCarrier[] | undefined, job: Job<unknown>, fn: () => Promise<T>): Promise<T>;
|
|
36
|
-
//# sourceMappingURL=contextCarrier.core.d.ts.map
|
|
@@ -35,4 +35,3 @@ export declare function createInMemoryDeadLetterStore<TData = unknown>(options?:
|
|
|
35
35
|
export declare function moveToDeadLetter<TData>(store: DeadLetterStore<TData>, job: Job<TData>, error: Error, options?: {
|
|
36
36
|
reason?: string;
|
|
37
37
|
}): Promise<void>;
|
|
38
|
-
//# sourceMappingURL=deadLetter.core.d.ts.map
|
|
@@ -7,4 +7,3 @@
|
|
|
7
7
|
export { DEFAULT_DEAD_LETTER_JOBS, createInMemoryDeadLetterStore, moveToDeadLetter, } from "./deadLetter.core.js";
|
|
8
8
|
export type { InMemoryDeadLetterStoreOptions } from "./deadLetter.core.js";
|
|
9
9
|
export type { DeadLetterJob, DeadLetterStore } from "./deadLetter.type.js";
|
|
10
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/deadLetter/index.js
CHANGED
|
@@ -489,6 +489,7 @@ export class InMemoryQueue {
|
|
|
489
489
|
onJobReady: () => this.jobReady(),
|
|
490
490
|
isDisposed: () => this.disposed,
|
|
491
491
|
...(this.options.logger ? { logger: this.options.logger } : {}),
|
|
492
|
+
...(this.options.random ? { random: this.options.random } : {}),
|
|
492
493
|
});
|
|
493
494
|
}
|
|
494
495
|
catch (error) {
|
|
@@ -631,4 +632,3 @@ export class InMemoryQueue {
|
|
|
631
632
|
}
|
|
632
633
|
}
|
|
633
634
|
}
|
|
634
|
-
//# sourceMappingURL=inMemoryQueue.core.js.map
|
|
@@ -46,6 +46,21 @@ export interface ProcessJobDependencies<TData> {
|
|
|
46
46
|
* when the queue was configured without a logger.
|
|
47
47
|
*/
|
|
48
48
|
readonly logger?: QueueLogger;
|
|
49
|
+
/** See `QueueOptions.random`. */
|
|
50
|
+
readonly random?: () => number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* How an attempt failed, beyond its message.
|
|
54
|
+
*/
|
|
55
|
+
export interface JobFailureDetails {
|
|
56
|
+
/** The value the processor threw or the result it returned, if any. */
|
|
57
|
+
readonly error?: unknown;
|
|
58
|
+
/**
|
|
59
|
+
* The failure is permanent: dead-letter the job now rather than retry.
|
|
60
|
+
* Set when the thrown error was marked with `markUnrecoverable` or the
|
|
61
|
+
* returned `JobResult` carries `unrecoverable: true`.
|
|
62
|
+
*/
|
|
63
|
+
readonly unrecoverable?: boolean;
|
|
49
64
|
}
|
|
50
65
|
/**
|
|
51
66
|
* Process a single job with middleware, retry, and failure handling.
|
|
@@ -67,7 +82,8 @@ export declare function processJob<TData>(job: Job<TData>, processor: Processor<
|
|
|
67
82
|
*
|
|
68
83
|
* A failure that will be retried is reported as `job:retrying` and counted
|
|
69
84
|
* separately; only a terminal failure increments `failedCount`, so the
|
|
70
|
-
* counters describe outcomes rather than attempts.
|
|
85
|
+
* counters describe outcomes rather than attempts. An unrecoverable failure
|
|
86
|
+
* skips the remaining attempts and is dead-lettered with the processor's
|
|
87
|
+
* own error.
|
|
71
88
|
*/
|
|
72
|
-
export declare function handleJobFailure<TData>(job: Job<TData>, errorMessage: string, deps: ProcessJobDependencies<TData
|
|
73
|
-
//# sourceMappingURL=inMemoryQueue.processing.d.ts.map
|
|
89
|
+
export declare function handleJobFailure<TData>(job: Job<TData>, errorMessage: string, deps: ProcessJobDependencies<TData>, details?: JobFailureDetails): Promise<void>;
|
|
@@ -7,6 +7,7 @@ import { calculateRetryDelay, resolveBackoff, shouldRetry, } from "../retryPolic
|
|
|
7
7
|
import { moveToDeadLetter } from "../deadLetter/deadLetter.core.js";
|
|
8
8
|
import { runWithContext } from "../contextCarrier/contextCarrier.core.js";
|
|
9
9
|
import { DEFAULT_TIMEOUT_GRACE_MS, settleWithin, } from "./inMemoryQueue.settle.js";
|
|
10
|
+
import { isUnrecoverableJobError } from "../jobFailure/jobFailure.unrecoverable.js";
|
|
10
11
|
import { JobMaxAttemptsError } from "@zudojs/errors";
|
|
11
12
|
/**
|
|
12
13
|
* Narrows a processor's return value to a `JobResult`.
|
|
@@ -77,7 +78,9 @@ export async function processJob(job, processor, options, deps) {
|
|
|
77
78
|
},
|
|
78
79
|
}));
|
|
79
80
|
if (isJobResult(result) && !result.success) {
|
|
80
|
-
await handleJobFailure(updatedJob, result.error ?? "Job failed", deps
|
|
81
|
+
await handleJobFailure(updatedJob, result.error ?? "Job failed", deps, {
|
|
82
|
+
unrecoverable: result.unrecoverable === true,
|
|
83
|
+
});
|
|
81
84
|
}
|
|
82
85
|
else {
|
|
83
86
|
const completedJob = updateJobState(updatedJob, JobStateEnum.COMPLETED, {
|
|
@@ -100,7 +103,10 @@ export async function processJob(job, processor, options, deps) {
|
|
|
100
103
|
if (abortController.signal.aborted && !timedOut) {
|
|
101
104
|
emitter.emit("job:cancelled", { job: updatedJob });
|
|
102
105
|
}
|
|
103
|
-
await handleJobFailure(updatedJob, errorMessage, deps
|
|
106
|
+
await handleJobFailure(updatedJob, errorMessage, deps, {
|
|
107
|
+
error,
|
|
108
|
+
unrecoverable: isUnrecoverableJobError(error),
|
|
109
|
+
});
|
|
104
110
|
}
|
|
105
111
|
finally {
|
|
106
112
|
counters.processedCount++;
|
|
@@ -111,22 +117,23 @@ export async function processJob(job, processor, options, deps) {
|
|
|
111
117
|
*
|
|
112
118
|
* A failure that will be retried is reported as `job:retrying` and counted
|
|
113
119
|
* separately; only a terminal failure increments `failedCount`, so the
|
|
114
|
-
* counters describe outcomes rather than attempts.
|
|
120
|
+
* counters describe outcomes rather than attempts. An unrecoverable failure
|
|
121
|
+
* skips the remaining attempts and is dead-lettered with the processor's
|
|
122
|
+
* own error.
|
|
115
123
|
*/
|
|
116
|
-
export async function handleJobFailure(job, errorMessage, deps) {
|
|
124
|
+
export async function handleJobFailure(job, errorMessage, deps, details = {}) {
|
|
117
125
|
const { jobs, emitter, counters, deadLetterStore } = deps;
|
|
126
|
+
const thrown = details.error instanceof Error ? details.error : new Error(errorMessage);
|
|
118
127
|
const failedJob = updateJobState(job, JobStateEnum.FAILED, {
|
|
119
128
|
error: errorMessage,
|
|
120
129
|
failedAt: new Date().toISOString(),
|
|
121
130
|
});
|
|
122
131
|
jobs.set(job.id, failedJob);
|
|
123
|
-
emitter.emit("job:failed", {
|
|
124
|
-
job: failedJob,
|
|
125
|
-
error: new Error(errorMessage),
|
|
126
|
-
});
|
|
132
|
+
emitter.emit("job:failed", { job: failedJob, error: thrown });
|
|
127
133
|
const incrementedJob = incrementJobAttempt(failedJob);
|
|
128
134
|
jobs.set(job.id, incrementedJob);
|
|
129
|
-
if (
|
|
135
|
+
if (details.unrecoverable !== true &&
|
|
136
|
+
shouldRetry(incrementedJob.attempt, incrementedJob.maxAttempts)) {
|
|
130
137
|
const retryingJob = updateJobState(incrementedJob, JobStateEnum.RETRYING);
|
|
131
138
|
jobs.set(job.id, retryingJob);
|
|
132
139
|
counters.retriedCount++;
|
|
@@ -135,7 +142,7 @@ export async function handleJobFailure(job, errorMessage, deps) {
|
|
|
135
142
|
attempt: incrementedJob.attempt,
|
|
136
143
|
});
|
|
137
144
|
const backoff = resolveBackoff(incrementedJob.backoff);
|
|
138
|
-
const delay = calculateRetryDelay(incrementedJob.attempt, backoff);
|
|
145
|
+
const delay = calculateRetryDelay(incrementedJob.attempt, backoff, deps.random);
|
|
139
146
|
const timer = setTimeout(() => {
|
|
140
147
|
deps.deregisterRetryTimer?.(job.id);
|
|
141
148
|
if (deps.isDisposed()) {
|
|
@@ -159,9 +166,11 @@ export async function handleJobFailure(job, errorMessage, deps) {
|
|
|
159
166
|
}
|
|
160
167
|
counters.failedCount++;
|
|
161
168
|
counters.deadLetteredCount++;
|
|
162
|
-
const
|
|
169
|
+
const deadLetterError = details.unrecoverable === true
|
|
170
|
+
? thrown
|
|
171
|
+
: describeExhaustedAttempts(incrementedJob, errorMessage);
|
|
163
172
|
try {
|
|
164
|
-
await moveToDeadLetter(deadLetterStore, incrementedJob,
|
|
173
|
+
await moveToDeadLetter(deadLetterStore, incrementedJob, deadLetterError, {
|
|
165
174
|
reason: errorMessage,
|
|
166
175
|
});
|
|
167
176
|
}
|
|
@@ -169,13 +178,26 @@ export async function handleJobFailure(job, errorMessage, deps) {
|
|
|
169
178
|
// A dead letter store that rejects must not mask the original
|
|
170
179
|
// failure or strand the job in `failed`.
|
|
171
180
|
}
|
|
172
|
-
const deadLetterJob = updateJobState(incrementedJob, JobStateEnum.DEAD_LETTER, { error:
|
|
181
|
+
const deadLetterJob = updateJobState(incrementedJob, JobStateEnum.DEAD_LETTER, { error: deadLetterError.message });
|
|
173
182
|
jobs.set(job.id, deadLetterJob);
|
|
174
183
|
emitter.emit("job:dead-lettered", {
|
|
175
184
|
job: deadLetterJob,
|
|
176
|
-
error:
|
|
185
|
+
error: deadLetterError,
|
|
177
186
|
reason: errorMessage,
|
|
178
187
|
});
|
|
179
188
|
deps.onSettled?.(deadLetterJob);
|
|
180
189
|
}
|
|
181
|
-
|
|
190
|
+
/**
|
|
191
|
+
* The error recorded for a job whose attempts ran out.
|
|
192
|
+
*
|
|
193
|
+
* The message names the last failure, so a dead-letter entry says what
|
|
194
|
+
* went wrong ("... Last error: card declined") rather than only that the
|
|
195
|
+
* attempts were used up; the bare reason is still on `reason`.
|
|
196
|
+
*/
|
|
197
|
+
function describeExhaustedAttempts(job, lastError) {
|
|
198
|
+
const error = new JobMaxAttemptsError(job.id, job.attempt, job.maxAttempts, {
|
|
199
|
+
queueName: job.queueName,
|
|
200
|
+
});
|
|
201
|
+
error.message = `${error.message} Last error: ${lastError}`;
|
|
202
|
+
return error;
|
|
203
|
+
}
|
|
@@ -26,4 +26,3 @@ export declare function promoteDueScheduledJobs<TData>(jobs: Map<string, Job<TDa
|
|
|
26
26
|
* timer belonging to a promoted job.
|
|
27
27
|
*/
|
|
28
28
|
export declare function scheduleDelayedJobs<TData>(jobs: Map<string, Job<TData>>): void;
|
|
29
|
-
//# sourceMappingURL=inMemoryQueue.scheduling.d.ts.map
|
|
@@ -21,4 +21,3 @@ export declare const DEFAULT_TIMEOUT_GRACE_MS = 5000;
|
|
|
21
21
|
* @returns True when the processor settled within the grace period.
|
|
22
22
|
*/
|
|
23
23
|
export declare function settleWithin(pending: Promise<unknown> | undefined, graceMs: number): Promise<boolean>;
|
|
24
|
-
//# sourceMappingURL=inMemoryQueue.settle.d.ts.map
|
|
@@ -19,8 +19,12 @@ export declare function runnableAt(job: Job<unknown>): number;
|
|
|
19
19
|
* first ({@link runnableAt}); among equals, the one added first. The
|
|
20
20
|
* incumbent is tracked by reference rather than by a sentinel priority, so
|
|
21
21
|
* negative priorities are selectable like any other.
|
|
22
|
+
*
|
|
23
|
+
* One pass over the job map per selection. A job that cannot beat the
|
|
24
|
+
* incumbent on priority is skipped before its timestamps are consulted, and
|
|
25
|
+
* timestamps are parsed once per record, so the pass is a state check per
|
|
26
|
+
* job rather than a date parse per job.
|
|
22
27
|
*/
|
|
23
28
|
export declare function selectNextJob<TData>(jobs: Iterable<Job<TData>>, now: number, predicate?: (job: Job<TData>) => boolean): Job<TData> | null;
|
|
24
29
|
/** Whether any job a consumer can run is still owed a run. */
|
|
25
30
|
export declare function hasPendingWork<TData>(jobs: Iterable<Job<TData>>, isConsumable: (job: Job<TData>) => boolean): boolean;
|
|
26
|
-
//# sourceMappingURL=inMemoryQueue.select.d.ts.map
|
|
@@ -11,6 +11,27 @@ const PENDING_STATES = new Set([
|
|
|
11
11
|
JobStateEnum.RETRYING,
|
|
12
12
|
JobStateEnum.ACTIVE,
|
|
13
13
|
]);
|
|
14
|
+
/**
|
|
15
|
+
* Job records are immutable and replaced on every state change, so a parsed
|
|
16
|
+
* timestamp stays valid for as long as the record itself is reachable. The
|
|
17
|
+
* cache turns the per-poll cost of a waiting job from an ISO-8601 parse (or
|
|
18
|
+
* two, for a delayed job) into a map lookup.
|
|
19
|
+
*/
|
|
20
|
+
const jobTimes = new WeakMap();
|
|
21
|
+
function timesOf(job) {
|
|
22
|
+
const cached = jobTimes.get(job);
|
|
23
|
+
if (cached !== undefined)
|
|
24
|
+
return cached;
|
|
25
|
+
const created = new Date(job.createdAt).getTime();
|
|
26
|
+
const scheduled = job.scheduledAt
|
|
27
|
+
? new Date(job.scheduledAt).getTime()
|
|
28
|
+
: Number.NaN;
|
|
29
|
+
const times = Number.isNaN(scheduled)
|
|
30
|
+
? { runnableAt: created, dueAt: Number.NEGATIVE_INFINITY }
|
|
31
|
+
: { runnableAt: Math.max(created, scheduled), dueAt: scheduled };
|
|
32
|
+
jobTimes.set(job, times);
|
|
33
|
+
return times;
|
|
34
|
+
}
|
|
14
35
|
/**
|
|
15
36
|
* When a job became runnable, in epoch milliseconds: its creation, or for a
|
|
16
37
|
* delayed job the moment its delay elapsed.
|
|
@@ -19,11 +40,7 @@ const PENDING_STATES = new Set([
|
|
|
19
40
|
* that had been ready and waiting while it was still delayed.
|
|
20
41
|
*/
|
|
21
42
|
export function runnableAt(job) {
|
|
22
|
-
|
|
23
|
-
if (!job.scheduledAt)
|
|
24
|
-
return created;
|
|
25
|
-
const scheduled = new Date(job.scheduledAt).getTime();
|
|
26
|
-
return Number.isNaN(scheduled) ? created : Math.max(created, scheduled);
|
|
43
|
+
return timesOf(job).runnableAt;
|
|
27
44
|
}
|
|
28
45
|
/**
|
|
29
46
|
* Selects the job that runs next among those that are waiting and due.
|
|
@@ -32,6 +49,11 @@ export function runnableAt(job) {
|
|
|
32
49
|
* first ({@link runnableAt}); among equals, the one added first. The
|
|
33
50
|
* incumbent is tracked by reference rather than by a sentinel priority, so
|
|
34
51
|
* negative priorities are selectable like any other.
|
|
52
|
+
*
|
|
53
|
+
* One pass over the job map per selection. A job that cannot beat the
|
|
54
|
+
* incumbent on priority is skipped before its timestamps are consulted, and
|
|
55
|
+
* timestamps are parsed once per record, so the pass is a state check per
|
|
56
|
+
* job rather than a date parse per job.
|
|
35
57
|
*/
|
|
36
58
|
export function selectNextJob(jobs, now, predicate) {
|
|
37
59
|
let next = null;
|
|
@@ -39,16 +61,18 @@ export function selectNextJob(jobs, now, predicate) {
|
|
|
39
61
|
for (const job of jobs) {
|
|
40
62
|
if (job.state !== JobStateEnum.WAITING)
|
|
41
63
|
continue;
|
|
42
|
-
if (
|
|
64
|
+
if (next !== null && job.priority < next.priority)
|
|
65
|
+
continue;
|
|
66
|
+
const times = timesOf(job);
|
|
67
|
+
if (times.dueAt > now)
|
|
43
68
|
continue;
|
|
44
69
|
if (predicate && !predicate(job))
|
|
45
70
|
continue;
|
|
46
|
-
const at = runnableAt(job);
|
|
47
71
|
if (next === null ||
|
|
48
72
|
job.priority > next.priority ||
|
|
49
|
-
|
|
73
|
+
times.runnableAt < nextAt) {
|
|
50
74
|
next = job;
|
|
51
|
-
nextAt =
|
|
75
|
+
nextAt = times.runnableAt;
|
|
52
76
|
}
|
|
53
77
|
}
|
|
54
78
|
return next;
|
|
@@ -61,4 +85,3 @@ export function hasPendingWork(jobs, isConsumable) {
|
|
|
61
85
|
}
|
|
62
86
|
return false;
|
|
63
87
|
}
|
|
64
|
-
//# sourceMappingURL=inMemoryQueue.select.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -24,4 +24,5 @@ export * from "./worker/index.js";
|
|
|
24
24
|
export * from "./inMemoryQueue/index.js";
|
|
25
25
|
export * from "./deadLetter/index.js";
|
|
26
26
|
export * from "./contextCarrier/index.js";
|
|
27
|
-
|
|
27
|
+
export * from "./jobFailure/index.js";
|
|
28
|
+
export * from "./queueErrors/index.js";
|
package/dist/index.js
CHANGED
|
@@ -41,4 +41,7 @@ export * from "./inMemoryQueue/index.js";
|
|
|
41
41
|
export * from "./deadLetter/index.js";
|
|
42
42
|
// Context propagation across the queue boundary
|
|
43
43
|
export * from "./contextCarrier/index.js";
|
|
44
|
-
|
|
44
|
+
// Permanent failures
|
|
45
|
+
export * from "./jobFailure/index.js";
|
|
46
|
+
// Error classes (owned by @zudojs/errors)
|
|
47
|
+
export * from "./queueErrors/index.js";
|
package/dist/job/index.d.ts
CHANGED
package/dist/job/index.js
CHANGED
package/dist/job/job.core.d.ts
CHANGED
package/dist/job/job.core.js
CHANGED
package/dist/job/job.type.d.ts
CHANGED
package/dist/job/job.type.js
CHANGED
package/dist/jobContext/index.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permanent job failures: marking an error so the job is dead-lettered
|
|
3
|
+
* without further attempts.
|
|
4
|
+
*
|
|
5
|
+
* @module jobFailure
|
|
6
|
+
*/
|
|
7
|
+
export { markUnrecoverable, createUnrecoverableJobError, isUnrecoverableJobError, } from "./jobFailure.unrecoverable.js";
|
|
8
|
+
export type { UnrecoverableJobErrorOptions } from "./jobFailure.unrecoverable.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marking a job failure as permanent.
|
|
3
|
+
*
|
|
4
|
+
* By default every failure is retried until `attempts` is exhausted. Some
|
|
5
|
+
* failures are known to be permanent the moment they happen — a payload
|
|
6
|
+
* that fails validation, a record that no longer exists — and retrying them
|
|
7
|
+
* only burns the remaining attempts and delays the dead-letter entry an
|
|
8
|
+
* operator needs to see. A processor marks such a failure unrecoverable and
|
|
9
|
+
* the queue dead-letters the job at once.
|
|
10
|
+
*
|
|
11
|
+
* @module jobFailure/unrecoverable
|
|
12
|
+
*/
|
|
13
|
+
import { JobError } from "@zudojs/errors";
|
|
14
|
+
/**
|
|
15
|
+
* Options for {@link createUnrecoverableJobError}.
|
|
16
|
+
*/
|
|
17
|
+
export interface UnrecoverableJobErrorOptions {
|
|
18
|
+
readonly queueName?: string;
|
|
19
|
+
readonly jobId?: string;
|
|
20
|
+
readonly cause?: unknown;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Marks an error so the job that throws it is dead-lettered without
|
|
24
|
+
* further attempts. Works on any error, including one from a library the
|
|
25
|
+
* processor calls into.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* queue.process("charge", async (job) => {
|
|
30
|
+
* const card = await cards.find(job.data.cardId);
|
|
31
|
+
* if (!card) throw markUnrecoverable(new Error("card no longer exists"));
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function markUnrecoverable<TError extends object>(error: TError): TError;
|
|
36
|
+
/**
|
|
37
|
+
* Creates a `JobError` already marked unrecoverable.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* throw createUnrecoverableJobError("payload failed validation", {
|
|
42
|
+
* jobId: job.id,
|
|
43
|
+
* queueName: job.queueName,
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function createUnrecoverableJobError(message: string, options?: UnrecoverableJobErrorOptions): JobError;
|
|
48
|
+
/**
|
|
49
|
+
* Whether a thrown value was marked with {@link markUnrecoverable}.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isUnrecoverableJobError(value: unknown): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marking a job failure as permanent.
|
|
3
|
+
*
|
|
4
|
+
* By default every failure is retried until `attempts` is exhausted. Some
|
|
5
|
+
* failures are known to be permanent the moment they happen — a payload
|
|
6
|
+
* that fails validation, a record that no longer exists — and retrying them
|
|
7
|
+
* only burns the remaining attempts and delays the dead-letter entry an
|
|
8
|
+
* operator needs to see. A processor marks such a failure unrecoverable and
|
|
9
|
+
* the queue dead-letters the job at once.
|
|
10
|
+
*
|
|
11
|
+
* @module jobFailure/unrecoverable
|
|
12
|
+
*/
|
|
13
|
+
import { JobError } from "@zudojs/errors";
|
|
14
|
+
/** Brand carried by an error whose job must not be retried. */
|
|
15
|
+
const UNRECOVERABLE = Symbol.for("@zudojs/queue.unrecoverable");
|
|
16
|
+
/**
|
|
17
|
+
* Marks an error so the job that throws it is dead-lettered without
|
|
18
|
+
* further attempts. Works on any error, including one from a library the
|
|
19
|
+
* processor calls into.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* queue.process("charge", async (job) => {
|
|
24
|
+
* const card = await cards.find(job.data.cardId);
|
|
25
|
+
* if (!card) throw markUnrecoverable(new Error("card no longer exists"));
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function markUnrecoverable(error) {
|
|
30
|
+
if (!Object.prototype.hasOwnProperty.call(error, UNRECOVERABLE)) {
|
|
31
|
+
Object.defineProperty(error, UNRECOVERABLE, {
|
|
32
|
+
value: true,
|
|
33
|
+
enumerable: false,
|
|
34
|
+
configurable: false,
|
|
35
|
+
writable: false,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates a `JobError` already marked unrecoverable.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* throw createUnrecoverableJobError("payload failed validation", {
|
|
46
|
+
* jobId: job.id,
|
|
47
|
+
* queueName: job.queueName,
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function createUnrecoverableJobError(message, options = {}) {
|
|
52
|
+
return markUnrecoverable(new JobError(message, {
|
|
53
|
+
queueName: options.queueName,
|
|
54
|
+
jobId: options.jobId,
|
|
55
|
+
cause: options.cause,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether a thrown value was marked with {@link markUnrecoverable}.
|
|
60
|
+
*/
|
|
61
|
+
export function isUnrecoverableJobError(value) {
|
|
62
|
+
return (typeof value === "object" &&
|
|
63
|
+
value !== null &&
|
|
64
|
+
value[UNRECOVERABLE] === true);
|
|
65
|
+
}
|