@zudojs/queue 1.0.0 → 1.2.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/README.md +58 -3
- package/dist/contextCarrier/contextCarrier.core.d.ts +31 -0
- package/dist/contextCarrier/contextCarrier.core.js +60 -0
- package/dist/contextCarrier/contextCarrier.type.d.ts +41 -0
- package/dist/contextCarrier/contextCarrier.type.js +7 -0
- package/dist/contextCarrier/index.d.ts +11 -0
- package/dist/contextCarrier/index.js +10 -0
- package/dist/inMemoryQueue/inMemoryQueue.core.d.ts +11 -0
- package/dist/inMemoryQueue/inMemoryQueue.core.js +28 -3
- package/dist/inMemoryQueue/inMemoryQueue.processing.d.ts +7 -0
- package/dist/inMemoryQueue/inMemoryQueue.processing.js +11 -3
- package/dist/inMemoryQueue/inMemoryQueue.settle.d.ts +24 -0
- package/dist/inMemoryQueue/inMemoryQueue.settle.js +39 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/queue/queue.report.d.ts +21 -0
- package/dist/queue/queue.report.js +26 -0
- package/dist/queue/queue.type.d.ts +30 -0
- package/dist/queueEmitter/queueEmitter.core.js +2 -1
- package/dist/worker/worker.core.d.ts +5 -0
- package/dist/worker/worker.core.js +72 -27
- package/dist/worker/worker.type.d.ts +6 -3
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Background job and asynchronous task infrastructure with in-memory and adapter-based queue implementations.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-queue](https://zudojs.oyinlola.site/docs/packages-queue) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-queue.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -43,9 +49,6 @@ await queue.add(
|
|
|
43
49
|
// The queue polls in the background; give it a tick before reading counts.
|
|
44
50
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
45
51
|
|
|
46
|
-
// The queue polls in the background; give it a tick before reading counts.
|
|
47
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
48
|
-
|
|
49
52
|
// Jobs that exhaust their attempts land here rather than vanishing.
|
|
50
53
|
console.log((await queue.getDeadLetterJobs()).length);
|
|
51
54
|
console.log(await queue.getStats());
|
|
@@ -69,6 +72,58 @@ await queue.close();
|
|
|
69
72
|
- Bounded retention of settled jobs, so a long-lived queue does not grow without
|
|
70
73
|
limit
|
|
71
74
|
|
|
75
|
+
## Workers, timeouts and context
|
|
76
|
+
|
|
77
|
+
**One consumer at a time.** `queue.process()` registers a processor and, by
|
|
78
|
+
default, the queue's own poller runs jobs. Creating a `Worker` for the queue
|
|
79
|
+
turns that poller off as a consumer (`queue.setAutoProcess(false)`), so the
|
|
80
|
+
worker's `middleware`, `timeoutMs` and `concurrency` apply to every job, and
|
|
81
|
+
`worker.stop()` really stops consumption. Pass `autoProcess: false` to keep the
|
|
82
|
+
queue from consuming before any worker exists.
|
|
83
|
+
|
|
84
|
+
**Timeouts are cooperative.** A timeout aborts `context.signal`; honour it. The
|
|
85
|
+
job's concurrency slot and its retry wait up to `timeoutGraceMs` (5000 ms by
|
|
86
|
+
default) for the processor to settle, so a retry never runs beside the attempt
|
|
87
|
+
it replaces. A processor still running after the grace period is abandoned.
|
|
88
|
+
|
|
89
|
+
**Carrying context across the queue.** AsyncLocalStorage does not follow a job
|
|
90
|
+
into the poller or worker that runs it. A `QueueContextCarrier` captures a value
|
|
91
|
+
at `add()` into job metadata and restores it around the middleware and the
|
|
92
|
+
processor:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
96
|
+
import { createInMemoryQueue, createQueueName } from "@zudojs/queue";
|
|
97
|
+
import type { QueueContextCarrier } from "@zudojs/queue";
|
|
98
|
+
|
|
99
|
+
const tenants = new AsyncLocalStorage<{ tenantId: string }>();
|
|
100
|
+
|
|
101
|
+
const tenantCarrier: QueueContextCarrier<string> = {
|
|
102
|
+
key: "tenantId",
|
|
103
|
+
capture: () => tenants.getStore()?.tenantId,
|
|
104
|
+
restore: (tenantId, run) => tenants.run({ tenantId }, run),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const jobs = createInMemoryQueue<{ id: number }>(createQueueName("reports"), {
|
|
108
|
+
contextCarriers: [tenantCarrier],
|
|
109
|
+
});
|
|
110
|
+
jobs.process("build", async () => {
|
|
111
|
+
console.log("building for", tenants.getStore()?.tenantId); // "acme"
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await tenants.run({ tenantId: "acme" }, () => jobs.add("build", { id: 1 }));
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
116
|
+
await jobs.close();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Keep captured values small and serializable (ids, not live objects). With
|
|
120
|
+
`@zudojs/tenancy`, capture the tenant id and restore it with the tenant context
|
|
121
|
+
storage's `run`.
|
|
122
|
+
|
|
123
|
+
Errors with no caller to receive them (a worker's failing poll, a throwing event
|
|
124
|
+
listener) go to `logger.error` when a logger is configured, and otherwise to
|
|
125
|
+
`process.emitWarning`, never to `console`.
|
|
126
|
+
|
|
72
127
|
## Use Cases
|
|
73
128
|
|
|
74
129
|
- Background email sending
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context capture at `add()` and restoration around processing.
|
|
3
|
+
*
|
|
4
|
+
* @module contextCarrier/contextCarrier.core
|
|
5
|
+
*/
|
|
6
|
+
import type { Job } from "../job/job.type.js";
|
|
7
|
+
import type { QueueContextCarrier } from "./contextCarrier.type.js";
|
|
8
|
+
/**
|
|
9
|
+
* Job metadata key under which captured context values are stored, as a
|
|
10
|
+
* `{ [carrier.key]: value }` record.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CONTEXT_METADATA_KEY = "zudo:context";
|
|
13
|
+
/**
|
|
14
|
+
* Captures every carrier's value into a metadata record.
|
|
15
|
+
*
|
|
16
|
+
* @param carriers - The queue's context carriers.
|
|
17
|
+
* @param metadata - The job's own metadata, if any.
|
|
18
|
+
* @returns The metadata with a {@link CONTEXT_METADATA_KEY} entry added, or
|
|
19
|
+
* the input unchanged when no carrier captured anything.
|
|
20
|
+
*/
|
|
21
|
+
export declare function captureContext(carriers: readonly QueueContextCarrier[] | undefined, metadata: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Runs `fn` inside every context the job carries, first carrier outermost.
|
|
24
|
+
*
|
|
25
|
+
* @param carriers - The queue's context carriers.
|
|
26
|
+
* @param job - The job about to run.
|
|
27
|
+
* @param fn - The work to run inside the restored context.
|
|
28
|
+
* @returns Whatever `fn` resolves to.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runWithContext<T>(carriers: readonly QueueContextCarrier[] | undefined, job: Job<unknown>, fn: () => Promise<T>): Promise<T>;
|
|
31
|
+
//# sourceMappingURL=contextCarrier.core.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context capture at `add()` and restoration around processing.
|
|
3
|
+
*
|
|
4
|
+
* @module contextCarrier/contextCarrier.core
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Job metadata key under which captured context values are stored, as a
|
|
8
|
+
* `{ [carrier.key]: value }` record.
|
|
9
|
+
*/
|
|
10
|
+
export const CONTEXT_METADATA_KEY = "zudo:context";
|
|
11
|
+
/**
|
|
12
|
+
* Captures every carrier's value into a metadata record.
|
|
13
|
+
*
|
|
14
|
+
* @param carriers - The queue's context carriers.
|
|
15
|
+
* @param metadata - The job's own metadata, if any.
|
|
16
|
+
* @returns The metadata with a {@link CONTEXT_METADATA_KEY} entry added, or
|
|
17
|
+
* the input unchanged when no carrier captured anything.
|
|
18
|
+
*/
|
|
19
|
+
export function captureContext(carriers, metadata) {
|
|
20
|
+
if (!carriers || carriers.length === 0)
|
|
21
|
+
return metadata;
|
|
22
|
+
const captured = {};
|
|
23
|
+
let any = false;
|
|
24
|
+
for (const carrier of carriers) {
|
|
25
|
+
const value = carrier.capture();
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
captured[carrier.key] = value;
|
|
29
|
+
any = true;
|
|
30
|
+
}
|
|
31
|
+
if (!any)
|
|
32
|
+
return metadata;
|
|
33
|
+
return { ...metadata, [CONTEXT_METADATA_KEY]: Object.freeze(captured) };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Runs `fn` inside every context the job carries, first carrier outermost.
|
|
37
|
+
*
|
|
38
|
+
* @param carriers - The queue's context carriers.
|
|
39
|
+
* @param job - The job about to run.
|
|
40
|
+
* @param fn - The work to run inside the restored context.
|
|
41
|
+
* @returns Whatever `fn` resolves to.
|
|
42
|
+
*/
|
|
43
|
+
export function runWithContext(carriers, job, fn) {
|
|
44
|
+
const raw = job.metadata?.[CONTEXT_METADATA_KEY];
|
|
45
|
+
if (!carriers || carriers.length === 0)
|
|
46
|
+
return fn();
|
|
47
|
+
if (typeof raw !== "object" || raw === null)
|
|
48
|
+
return fn();
|
|
49
|
+
const stored = raw;
|
|
50
|
+
let run = fn;
|
|
51
|
+
for (let i = carriers.length - 1; i >= 0; i--) {
|
|
52
|
+
const carrier = carriers[i];
|
|
53
|
+
if (!Object.hasOwn(stored, carrier.key))
|
|
54
|
+
continue;
|
|
55
|
+
const inner = run;
|
|
56
|
+
run = () => carrier.restore(stored[carrier.key], inner);
|
|
57
|
+
}
|
|
58
|
+
return run();
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=contextCarrier.core.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context carrier contract.
|
|
3
|
+
*
|
|
4
|
+
* @module contextCarrier/contextCarrier.type
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Captures one piece of ambient context when a job is added and restores it
|
|
8
|
+
* while the job runs.
|
|
9
|
+
*
|
|
10
|
+
* AsyncLocalStorage does not follow a job from the request that enqueued it
|
|
11
|
+
* into the poller or worker that runs it, so without a carrier every job runs
|
|
12
|
+
* with no tenant, no correlation id and no trace. Keep the captured value
|
|
13
|
+
* small and serializable (an id, not a live object): a broker-backed queue
|
|
14
|
+
* has to store it with the job.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const tenantCarrier: QueueContextCarrier<string> = {
|
|
19
|
+
* key: "tenantId",
|
|
20
|
+
* capture: () => tenantStorage.get()?.tenant?.id,
|
|
21
|
+
* restore: (tenantId, run) =>
|
|
22
|
+
* tenantStorage.run(contextFor(tenantId), run),
|
|
23
|
+
* };
|
|
24
|
+
* createInMemoryQueue("emails", { contextCarriers: [tenantCarrier] });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export interface QueueContextCarrier<TValue = unknown> {
|
|
28
|
+
/** Unique name under which the value is stored with the job. */
|
|
29
|
+
readonly key: string;
|
|
30
|
+
/**
|
|
31
|
+
* Reads the value from the caller's context at `add()`. Returning
|
|
32
|
+
* `undefined` stores nothing.
|
|
33
|
+
*/
|
|
34
|
+
capture(): TValue | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Runs `run` inside the restored context. Called only for jobs that
|
|
37
|
+
* carry a value for this carrier's `key`.
|
|
38
|
+
*/
|
|
39
|
+
restore<T>(value: TValue, run: () => Promise<T>): Promise<T>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=contextCarrier.type.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/queue/contextCarrier
|
|
3
|
+
*
|
|
4
|
+
* Carries ambient execution context (tenant, correlation id, trace ids)
|
|
5
|
+
* across the queue boundary: captured into job metadata at `add()` and
|
|
6
|
+
* restored around the processor, so a background job runs in the context of
|
|
7
|
+
* the request that enqueued it.
|
|
8
|
+
*/
|
|
9
|
+
export type { QueueContextCarrier } from "./contextCarrier.type.js";
|
|
10
|
+
export { CONTEXT_METADATA_KEY, captureContext, runWithContext, } from "./contextCarrier.core.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/queue/contextCarrier
|
|
3
|
+
*
|
|
4
|
+
* Carries ambient execution context (tenant, correlation id, trace ids)
|
|
5
|
+
* across the queue boundary: captured into job metadata at `add()` and
|
|
6
|
+
* restored around the processor, so a background job runs in the context of
|
|
7
|
+
* the request that enqueued it.
|
|
8
|
+
*/
|
|
9
|
+
export { CONTEXT_METADATA_KEY, captureContext, runWithContext, } from "./contextCarrier.core.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -20,6 +20,8 @@ export declare class InMemoryQueue<TData = unknown> implements Queue<TData> {
|
|
|
20
20
|
private readonly middleware;
|
|
21
21
|
private paused;
|
|
22
22
|
private disposed;
|
|
23
|
+
/** Whether the internal poller claims jobs; see `setAutoProcess`. */
|
|
24
|
+
private autoProcess;
|
|
23
25
|
private activeCount;
|
|
24
26
|
private pollTimer;
|
|
25
27
|
private readonly scheduledTimers;
|
|
@@ -35,6 +37,15 @@ export declare class InMemoryQueue<TData = unknown> implements Queue<TData> {
|
|
|
35
37
|
private emptySince;
|
|
36
38
|
private backoffMs;
|
|
37
39
|
constructor(name: QueueName, options?: QueueOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Turns the internal poller on or off as a consumer.
|
|
42
|
+
*
|
|
43
|
+
* While off, the poller still promotes scheduled jobs and reclaims stalled
|
|
44
|
+
* ones, but claims nothing: an external `Worker` is the only consumer, so
|
|
45
|
+
* stopping it really stops consumption and its middleware, timeout and
|
|
46
|
+
* concurrency apply to every job.
|
|
47
|
+
*/
|
|
48
|
+
setAutoProcess(enabled: boolean): void;
|
|
38
49
|
add(jobName: string, data: TData, options?: JobOptions): Promise<Job<TData>>;
|
|
39
50
|
process(name: string, processor: Processor<TData>): void;
|
|
40
51
|
getJob(jobId: JobId): Promise<Job<TData> | null>;
|
|
@@ -7,6 +7,7 @@ import { JsonSerializer } from "../serializer/serializer.core.js";
|
|
|
7
7
|
import { createInMemoryDeadLetterStore } from "../deadLetter/deadLetter.core.js";
|
|
8
8
|
import { createNoopQueueEventEmitter } from "../queueEmitter/queueEmitter.core.js";
|
|
9
9
|
import { processJob } from "./inMemoryQueue.processing.js";
|
|
10
|
+
import { captureContext } from "../contextCarrier/contextCarrier.core.js";
|
|
10
11
|
import { scheduleJob, promoteDueScheduledJobs, } from "./inMemoryQueue.scheduling.js";
|
|
11
12
|
/** Terminal states a job never leaves. */
|
|
12
13
|
const TERMINAL_STATES = new Set([
|
|
@@ -34,6 +35,8 @@ export class InMemoryQueue {
|
|
|
34
35
|
middleware;
|
|
35
36
|
paused = false;
|
|
36
37
|
disposed = false;
|
|
38
|
+
/** Whether the internal poller claims jobs; see `setAutoProcess`. */
|
|
39
|
+
autoProcess;
|
|
37
40
|
activeCount = 0;
|
|
38
41
|
pollTimer = null;
|
|
39
42
|
scheduledTimers = new Map();
|
|
@@ -62,6 +65,18 @@ export class InMemoryQueue {
|
|
|
62
65
|
this.emitter = options?.eventEmitter ?? createNoopQueueEventEmitter();
|
|
63
66
|
this.deadLetterStore =
|
|
64
67
|
this.options.deadLetterStore ?? createInMemoryDeadLetterStore();
|
|
68
|
+
this.autoProcess = this.options.autoProcess ?? true;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Turns the internal poller on or off as a consumer.
|
|
72
|
+
*
|
|
73
|
+
* While off, the poller still promotes scheduled jobs and reclaims stalled
|
|
74
|
+
* ones, but claims nothing: an external `Worker` is the only consumer, so
|
|
75
|
+
* stopping it really stops consumption and its middleware, timeout and
|
|
76
|
+
* concurrency apply to every job.
|
|
77
|
+
*/
|
|
78
|
+
setAutoProcess(enabled) {
|
|
79
|
+
this.autoProcess = enabled;
|
|
65
80
|
}
|
|
66
81
|
async add(jobName, data, options) {
|
|
67
82
|
if (this.disposed)
|
|
@@ -74,7 +89,9 @@ export class InMemoryQueue {
|
|
|
74
89
|
queueName: this.name,
|
|
75
90
|
});
|
|
76
91
|
}
|
|
77
|
-
const
|
|
92
|
+
const merged = { ...this.options.defaultJobOptions, ...options };
|
|
93
|
+
const metadata = captureContext(this.options.contextCarriers, merged.metadata);
|
|
94
|
+
const mergedOptions = metadata === merged.metadata ? merged : { ...merged, metadata };
|
|
78
95
|
if (mergedOptions.deduplicationKey) {
|
|
79
96
|
const existing = this.deduplicationIndex.get(mergedOptions.deduplicationKey);
|
|
80
97
|
if (existing && this.jobs.has(existing)) {
|
|
@@ -402,6 +419,12 @@ export class InMemoryQueue {
|
|
|
402
419
|
await processJob(job, processor, {
|
|
403
420
|
timeoutMs: job.timeoutMs ?? options?.timeoutMs,
|
|
404
421
|
abortController,
|
|
422
|
+
...(this.options.timeoutGraceMs !== undefined
|
|
423
|
+
? { timeoutGraceMs: this.options.timeoutGraceMs }
|
|
424
|
+
: {}),
|
|
425
|
+
...(this.options.contextCarriers
|
|
426
|
+
? { contextCarriers: this.options.contextCarriers }
|
|
427
|
+
: {}),
|
|
405
428
|
}, {
|
|
406
429
|
jobs: this.jobs,
|
|
407
430
|
emitter: this.emitter,
|
|
@@ -413,6 +436,9 @@ export class InMemoryQueue {
|
|
|
413
436
|
registerRetryTimer: (jobId, timer) => {
|
|
414
437
|
this.retryTimers.set(jobId, timer);
|
|
415
438
|
},
|
|
439
|
+
deregisterRetryTimer: (jobId) => {
|
|
440
|
+
this.retryTimers.delete(jobId);
|
|
441
|
+
},
|
|
416
442
|
onSettled: (settled) => this.recordSettled(settled),
|
|
417
443
|
isDisposed: () => this.disposed,
|
|
418
444
|
...(this.options.logger ? { logger: this.options.logger } : {}),
|
|
@@ -427,7 +453,6 @@ export class InMemoryQueue {
|
|
|
427
453
|
finally {
|
|
428
454
|
this.activeCount--;
|
|
429
455
|
this.inFlight.delete(job.id);
|
|
430
|
-
this.retryTimers.delete(job.id);
|
|
431
456
|
}
|
|
432
457
|
}
|
|
433
458
|
async processTick() {
|
|
@@ -435,7 +460,7 @@ export class InMemoryQueue {
|
|
|
435
460
|
return;
|
|
436
461
|
const concurrency = Math.max(1, this.options.concurrency ?? 1);
|
|
437
462
|
let processed = 0;
|
|
438
|
-
while (this.activeCount < concurrency) {
|
|
463
|
+
while (this.autoProcess && this.activeCount < concurrency) {
|
|
439
464
|
// `claimNextJob` only returns jobs that have a registered
|
|
440
465
|
// processor and moves them out of `waiting`, so this loop always
|
|
441
466
|
// terminates. Returning an unrunnable job here is what previously
|
|
@@ -5,6 +5,7 @@ import type { QueueMiddleware } from "../middleware/middleware.type.js";
|
|
|
5
5
|
import type { QueueEventEmitter } from "../queueEmitter/queueEmitter.type.js";
|
|
6
6
|
import type { DeadLetterStore } from "../deadLetter/deadLetter.type.js";
|
|
7
7
|
import type { QueueLogger } from "../queue/queue.type.js";
|
|
8
|
+
import type { QueueContextCarrier } from "../contextCarrier/contextCarrier.type.js";
|
|
8
9
|
/**
|
|
9
10
|
* Mutable throughput counters.
|
|
10
11
|
*
|
|
@@ -32,6 +33,8 @@ export interface ProcessJobDependencies<TData> {
|
|
|
32
33
|
* queue can clear it on close instead of leaking it.
|
|
33
34
|
*/
|
|
34
35
|
readonly registerRetryTimer: (jobId: JobId, timer: ReturnType<typeof setTimeout>) => void;
|
|
36
|
+
/** Forgets a retry timer once it has fired. */
|
|
37
|
+
readonly deregisterRetryTimer?: (jobId: JobId) => void;
|
|
35
38
|
/** Invoked whenever a job reaches a terminal state. */
|
|
36
39
|
readonly onSettled?: (job: Job<TData>) => void;
|
|
37
40
|
/** Whether the owning queue has been disposed. */
|
|
@@ -52,6 +55,10 @@ export interface ProcessJobDependencies<TData> {
|
|
|
52
55
|
export declare function processJob<TData>(job: Job<TData>, processor: Processor<TData>, options: {
|
|
53
56
|
timeoutMs?: number;
|
|
54
57
|
abortController?: AbortController;
|
|
58
|
+
/** See `QueueOptions.timeoutGraceMs`. */
|
|
59
|
+
timeoutGraceMs?: number;
|
|
60
|
+
/** See `QueueOptions.contextCarriers`. */
|
|
61
|
+
contextCarriers?: readonly QueueContextCarrier[];
|
|
55
62
|
}, deps: ProcessJobDependencies<TData>): Promise<void>;
|
|
56
63
|
/**
|
|
57
64
|
* Handle job failure with retry logic.
|
|
@@ -5,6 +5,8 @@ import { createJobContext } from "../jobContext/jobContext.core.js";
|
|
|
5
5
|
import { createMiddlewareChain, createTimeoutMiddleware, } from "../middleware/middleware.core.js";
|
|
6
6
|
import { calculateRetryDelay, resolveBackoff, shouldRetry, } from "../retryPolicy/retryPolicy.core.js";
|
|
7
7
|
import { moveToDeadLetter } from "../deadLetter/deadLetter.core.js";
|
|
8
|
+
import { runWithContext } from "../contextCarrier/contextCarrier.core.js";
|
|
9
|
+
import { DEFAULT_TIMEOUT_GRACE_MS, settleWithin, } from "./inMemoryQueue.settle.js";
|
|
8
10
|
import { JobMaxAttemptsError } from "@zudojs/errors";
|
|
9
11
|
/**
|
|
10
12
|
* Narrows a processor's return value to a `JobResult`.
|
|
@@ -57,14 +59,18 @@ export async function processJob(job, processor, options, deps) {
|
|
|
57
59
|
timeoutMiddleware,
|
|
58
60
|
...deps.middleware,
|
|
59
61
|
]);
|
|
62
|
+
// The processor promise itself, so a failure (a timeout above all) can
|
|
63
|
+
// wait for it to stop before the slot and the retry are released.
|
|
64
|
+
let running;
|
|
60
65
|
try {
|
|
61
|
-
const result = await middlewareChain({
|
|
66
|
+
const result = await runWithContext(options.contextCarriers, updatedJob, () => middlewareChain({
|
|
62
67
|
job: updatedJob,
|
|
63
68
|
context,
|
|
64
69
|
next: async () => {
|
|
65
|
-
|
|
70
|
+
running = Promise.resolve(processor(updatedJob, context));
|
|
71
|
+
return running;
|
|
66
72
|
},
|
|
67
|
-
});
|
|
73
|
+
}));
|
|
68
74
|
if (isJobResult(result) && !result.success) {
|
|
69
75
|
await handleJobFailure(updatedJob, result.error ?? "Job failed", deps);
|
|
70
76
|
}
|
|
@@ -85,6 +91,7 @@ export async function processJob(job, processor, options, deps) {
|
|
|
85
91
|
}
|
|
86
92
|
catch (error) {
|
|
87
93
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
94
|
+
await settleWithin(running, options.timeoutGraceMs ?? DEFAULT_TIMEOUT_GRACE_MS);
|
|
88
95
|
await handleJobFailure(updatedJob, errorMessage, deps);
|
|
89
96
|
}
|
|
90
97
|
finally {
|
|
@@ -122,6 +129,7 @@ export async function handleJobFailure(job, errorMessage, deps) {
|
|
|
122
129
|
const backoff = resolveBackoff(incrementedJob.backoff);
|
|
123
130
|
const delay = calculateRetryDelay(incrementedJob.attempt, backoff);
|
|
124
131
|
const timer = setTimeout(() => {
|
|
132
|
+
deps.deregisterRetryTimer?.(job.id);
|
|
125
133
|
if (deps.isDisposed()) {
|
|
126
134
|
return;
|
|
127
135
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waiting for a timed-out processor to actually stop.
|
|
3
|
+
*
|
|
4
|
+
* The timeout middleware rejects as soon as the timer fires, but the
|
|
5
|
+
* processor promise it raced keeps running until it notices the aborted
|
|
6
|
+
* signal. Freeing the concurrency slot and scheduling the retry at that
|
|
7
|
+
* moment let the retry run beside the original attempt.
|
|
8
|
+
*
|
|
9
|
+
* @module inMemoryQueue/inMemoryQueue.settle
|
|
10
|
+
*/
|
|
11
|
+
/** Default grace for a timed-out processor to settle, in milliseconds. */
|
|
12
|
+
export declare const DEFAULT_TIMEOUT_GRACE_MS = 5000;
|
|
13
|
+
/**
|
|
14
|
+
* Resolves once `pending` settles or `graceMs` elapses, whichever is first.
|
|
15
|
+
*
|
|
16
|
+
* Never rejects: the processor's own outcome has already been superseded by
|
|
17
|
+
* the failure being handled.
|
|
18
|
+
*
|
|
19
|
+
* @param pending - The processor promise, if the processor was started.
|
|
20
|
+
* @param graceMs - The longest to wait.
|
|
21
|
+
* @returns True when the processor settled within the grace period.
|
|
22
|
+
*/
|
|
23
|
+
export declare function settleWithin(pending: Promise<unknown> | undefined, graceMs: number): Promise<boolean>;
|
|
24
|
+
//# sourceMappingURL=inMemoryQueue.settle.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waiting for a timed-out processor to actually stop.
|
|
3
|
+
*
|
|
4
|
+
* The timeout middleware rejects as soon as the timer fires, but the
|
|
5
|
+
* processor promise it raced keeps running until it notices the aborted
|
|
6
|
+
* signal. Freeing the concurrency slot and scheduling the retry at that
|
|
7
|
+
* moment let the retry run beside the original attempt.
|
|
8
|
+
*
|
|
9
|
+
* @module inMemoryQueue/inMemoryQueue.settle
|
|
10
|
+
*/
|
|
11
|
+
/** Default grace for a timed-out processor to settle, in milliseconds. */
|
|
12
|
+
export const DEFAULT_TIMEOUT_GRACE_MS = 5_000;
|
|
13
|
+
/**
|
|
14
|
+
* Resolves once `pending` settles or `graceMs` elapses, whichever is first.
|
|
15
|
+
*
|
|
16
|
+
* Never rejects: the processor's own outcome has already been superseded by
|
|
17
|
+
* the failure being handled.
|
|
18
|
+
*
|
|
19
|
+
* @param pending - The processor promise, if the processor was started.
|
|
20
|
+
* @param graceMs - The longest to wait.
|
|
21
|
+
* @returns True when the processor settled within the grace period.
|
|
22
|
+
*/
|
|
23
|
+
export async function settleWithin(pending, graceMs) {
|
|
24
|
+
if (pending === undefined)
|
|
25
|
+
return true;
|
|
26
|
+
let timer;
|
|
27
|
+
const settled = pending.then(() => true, () => true);
|
|
28
|
+
const expired = new Promise((resolve) => {
|
|
29
|
+
timer = setTimeout(() => resolve(false), Math.max(0, graceMs));
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
return await Promise.race([settled, expired]);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (timer !== undefined)
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=inMemoryQueue.settle.js.map
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -39,4 +39,6 @@ export * from "./worker/index.js";
|
|
|
39
39
|
export * from "./inMemoryQueue/index.js";
|
|
40
40
|
// Dead letter
|
|
41
41
|
export * from "./deadLetter/index.js";
|
|
42
|
+
// Context propagation across the queue boundary
|
|
43
|
+
export * from "./contextCarrier/index.js";
|
|
42
44
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last-resort reporting for failures nobody else can receive.
|
|
3
|
+
*
|
|
4
|
+
* A worker's poll failure or a throwing event listener has no caller to
|
|
5
|
+
* reject. They used to go to `console.error`, bypassing structured logging
|
|
6
|
+
* and redaction. They now go to the configured logger's `error`, and without
|
|
7
|
+
* one to `process.emitWarning`, which Node prints once and applications can
|
|
8
|
+
* intercept with `process.on("warning")`.
|
|
9
|
+
*
|
|
10
|
+
* @module queue/queue.report
|
|
11
|
+
*/
|
|
12
|
+
import type { QueueLogger } from "./queue.type.js";
|
|
13
|
+
/**
|
|
14
|
+
* Report an error that has no caller to receive it.
|
|
15
|
+
*
|
|
16
|
+
* @param message - What failed.
|
|
17
|
+
* @param error - The failure.
|
|
18
|
+
* @param logger - The configured logger, if any.
|
|
19
|
+
*/
|
|
20
|
+
export declare function reportQueueError(message: string, error: unknown, logger?: QueueLogger): void;
|
|
21
|
+
//# sourceMappingURL=queue.report.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last-resort reporting for failures nobody else can receive.
|
|
3
|
+
*
|
|
4
|
+
* A worker's poll failure or a throwing event listener has no caller to
|
|
5
|
+
* reject. They used to go to `console.error`, bypassing structured logging
|
|
6
|
+
* and redaction. They now go to the configured logger's `error`, and without
|
|
7
|
+
* one to `process.emitWarning`, which Node prints once and applications can
|
|
8
|
+
* intercept with `process.on("warning")`.
|
|
9
|
+
*
|
|
10
|
+
* @module queue/queue.report
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Report an error that has no caller to receive it.
|
|
14
|
+
*
|
|
15
|
+
* @param message - What failed.
|
|
16
|
+
* @param error - The failure.
|
|
17
|
+
* @param logger - The configured logger, if any.
|
|
18
|
+
*/
|
|
19
|
+
export function reportQueueError(message, error, logger) {
|
|
20
|
+
if (logger?.error) {
|
|
21
|
+
logger.error(message, { error });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
process.emitWarning(error instanceof Error ? error : new Error(String(error)), { type: "ZudoQueueWarning", detail: message });
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=queue.report.js.map
|
|
@@ -5,6 +5,7 @@ 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 { QueueContextCarrier } from "../contextCarrier/contextCarrier.type.js";
|
|
8
9
|
import type { DeadLetterJob, DeadLetterStore } from "../deadLetter/deadLetter.type.js";
|
|
9
10
|
/**
|
|
10
11
|
* Somewhere for a job to write a log line.
|
|
@@ -13,6 +14,8 @@ import type { DeadLetterJob, DeadLetterStore } from "../deadLetter/deadLetter.ty
|
|
|
13
14
|
*/
|
|
14
15
|
export interface QueueLogger {
|
|
15
16
|
info(message: string, data?: Record<string, unknown>): void;
|
|
17
|
+
/** Receives failures that have no caller to reject (poll errors). */
|
|
18
|
+
error?(message: string, data?: Record<string, unknown>): void;
|
|
16
19
|
}
|
|
17
20
|
/**
|
|
18
21
|
* Options for creating a queue.
|
|
@@ -79,6 +82,28 @@ export interface QueueOptions {
|
|
|
79
82
|
* reclaimed again. Defaults to 3.
|
|
80
83
|
*/
|
|
81
84
|
readonly maxStalledCount?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the queue's own poller claims and runs jobs. Defaults to
|
|
87
|
+
* `true`. Creating a `Worker` for the queue turns it off, so the worker —
|
|
88
|
+
* with its middleware, timeout and concurrency — is the only consumer.
|
|
89
|
+
*/
|
|
90
|
+
readonly autoProcess?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* After a job times out, how long its concurrency slot and its retry wait
|
|
93
|
+
* for the processor to actually settle, in milliseconds. Defaults to 5000.
|
|
94
|
+
*
|
|
95
|
+
* A timeout aborts `context.signal`; a processor that ignores the signal
|
|
96
|
+
* keeps running. Without the wait, the retry started beside it and one
|
|
97
|
+
* job ran several times at once. Past this grace the processor is
|
|
98
|
+
* abandoned and the job fails anyway, so processors must honour the
|
|
99
|
+
* signal.
|
|
100
|
+
*/
|
|
101
|
+
readonly timeoutGraceMs?: number;
|
|
102
|
+
/**
|
|
103
|
+
* Context carried from `add()` into the processor (tenant, correlation
|
|
104
|
+
* id, trace ids). See {@link QueueContextCarrier}.
|
|
105
|
+
*/
|
|
106
|
+
readonly contextCarriers?: readonly QueueContextCarrier[];
|
|
82
107
|
}
|
|
83
108
|
/**
|
|
84
109
|
* Statistics for a queue.
|
|
@@ -171,6 +196,11 @@ export interface Queue<TData = unknown> {
|
|
|
171
196
|
getDeadLetterJobs(): Promise<readonly DeadLetterJob<TData>[]>;
|
|
172
197
|
/** Close the queue, draining in-flight jobs first. */
|
|
173
198
|
close(): Promise<void>;
|
|
199
|
+
/**
|
|
200
|
+
* Turns the queue's own poller on or off as a consumer. `createWorker`
|
|
201
|
+
* turns it off so a worker is never racing the queue for jobs.
|
|
202
|
+
*/
|
|
203
|
+
setAutoProcess?(enabled: boolean): void;
|
|
174
204
|
}
|
|
175
205
|
/**
|
|
176
206
|
* Event types emitted by a queue.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reportQueueError } from "../queue/queue.report.js";
|
|
1
2
|
/**
|
|
2
3
|
* In-memory queue event emitter.
|
|
3
4
|
*
|
|
@@ -14,7 +15,7 @@ export class InMemoryQueueEventEmitter {
|
|
|
14
15
|
options.onHandlerError ??
|
|
15
16
|
((error, event) => {
|
|
16
17
|
queueMicrotask(() => {
|
|
17
|
-
|
|
18
|
+
reportQueueError(`[@zudojs/queue] Listener for "${event}" threw.`, error);
|
|
18
19
|
});
|
|
19
20
|
});
|
|
20
21
|
}
|
|
@@ -6,6 +6,11 @@ import type { Worker, WorkerOptions } from "./worker.type.js";
|
|
|
6
6
|
* The worker claims each job before running it, so a job is never picked
|
|
7
7
|
* up twice — by this worker on its next poll, or by another worker on the
|
|
8
8
|
* same queue.
|
|
9
|
+
*
|
|
10
|
+
* Creating a worker turns the queue's own poller off as a consumer
|
|
11
|
+
* (`queue.setAutoProcess(false)`), so the worker — with its middleware,
|
|
12
|
+
* timeout and concurrency — is the only thing running jobs, and `stop()`
|
|
13
|
+
* really stops consumption.
|
|
9
14
|
*/
|
|
10
15
|
export declare function createWorker<TData>(id: string, queue: Queue<TData>, options?: WorkerOptions): Worker<TData>;
|
|
11
16
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { JobState as JobStateEnum, WorkerState, } from "../jobTypes/jobTypes.type.js";
|
|
2
2
|
import { WorkerLifecycleError } from "@zudojs/errors";
|
|
3
|
+
import { reportQueueError } from "../queue/queue.report.js";
|
|
3
4
|
/** How long `stop()` waits for in-flight jobs before forcing a stop. */
|
|
4
5
|
const DEFAULT_DRAIN_TIMEOUT_MS = 30_000;
|
|
5
6
|
/**
|
|
@@ -8,6 +9,11 @@ const DEFAULT_DRAIN_TIMEOUT_MS = 30_000;
|
|
|
8
9
|
* The worker claims each job before running it, so a job is never picked
|
|
9
10
|
* up twice — by this worker on its next poll, or by another worker on the
|
|
10
11
|
* same queue.
|
|
12
|
+
*
|
|
13
|
+
* Creating a worker turns the queue's own poller off as a consumer
|
|
14
|
+
* (`queue.setAutoProcess(false)`), so the worker — with its middleware,
|
|
15
|
+
* timeout and concurrency — is the only thing running jobs, and `stop()`
|
|
16
|
+
* really stops consumption.
|
|
11
17
|
*/
|
|
12
18
|
export function createWorker(id, queue, options) {
|
|
13
19
|
let state = WorkerState.CREATED;
|
|
@@ -17,17 +23,29 @@ export function createWorker(id, queue, options) {
|
|
|
17
23
|
const drainTimeout = options?.drainTimeout ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
18
24
|
let pollTimer = null;
|
|
19
25
|
let activeJobs = 0;
|
|
26
|
+
let polling = false;
|
|
20
27
|
let abortController = null;
|
|
28
|
+
queue.setAutoProcess?.(false);
|
|
21
29
|
const onError = options?.onError ??
|
|
22
30
|
((error) => {
|
|
23
31
|
queueMicrotask(() => {
|
|
24
|
-
|
|
32
|
+
reportQueueError(`[@zudojs/queue] Worker "${id}" poll failed.`, error, options?.logger);
|
|
25
33
|
});
|
|
26
34
|
});
|
|
35
|
+
/**
|
|
36
|
+
* Arms the next poll. At most one timer is ever armed: a delayed poll
|
|
37
|
+
* already pending is left alone, while an immediate poll (capacity just
|
|
38
|
+
* freed up, or a job was just dispatched) supersedes it.
|
|
39
|
+
*/
|
|
27
40
|
const scheduleNextPoll = (delay) => {
|
|
28
41
|
if (state !== WorkerState.RUNNING) {
|
|
29
42
|
return;
|
|
30
43
|
}
|
|
44
|
+
if (pollTimer !== null) {
|
|
45
|
+
if (delay > 0)
|
|
46
|
+
return;
|
|
47
|
+
clearTimeout(pollTimer);
|
|
48
|
+
}
|
|
31
49
|
pollTimer = setTimeout(runPoll, delay);
|
|
32
50
|
pollTimer.unref?.();
|
|
33
51
|
};
|
|
@@ -37,34 +55,20 @@ export function createWorker(id, queue, options) {
|
|
|
37
55
|
* fatal, would take down the whole application.
|
|
38
56
|
*/
|
|
39
57
|
const runPoll = () => {
|
|
58
|
+
pollTimer = null;
|
|
40
59
|
void poll().catch((error) => {
|
|
41
60
|
onError(error);
|
|
42
61
|
scheduleNextPoll(pollInterval);
|
|
43
62
|
});
|
|
44
63
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const job = await queue.claimNextJob();
|
|
54
|
-
if (!job) {
|
|
55
|
-
scheduleNextPoll(pollInterval);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const proc = queue.getProcessor(job.name);
|
|
59
|
-
if (!proc) {
|
|
60
|
-
// Claimed but unrunnable: release it rather than stranding it in
|
|
61
|
-
// `active` where nothing would ever pick it up again.
|
|
62
|
-
await queue.releaseJob(job.id);
|
|
63
|
-
scheduleNextPoll(pollInterval);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
activeJobs++;
|
|
67
|
-
stats.processed++;
|
|
64
|
+
/**
|
|
65
|
+
* Runs one claimed job to completion and frees its concurrency slot.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately not awaited by `poll`: awaiting it there serialised the
|
|
68
|
+
* worker, so `concurrency` was reported by `getStats()` and honoured by
|
|
69
|
+
* nothing.
|
|
70
|
+
*/
|
|
71
|
+
const runClaimedJob = async (job) => {
|
|
68
72
|
try {
|
|
69
73
|
// Dispatch through the queue rather than invoking the processor
|
|
70
74
|
// directly. The queue owns job state, retry, dead-lettering and
|
|
@@ -91,10 +95,46 @@ export function createWorker(id, queue, options) {
|
|
|
91
95
|
}
|
|
92
96
|
finally {
|
|
93
97
|
activeJobs--;
|
|
94
|
-
//
|
|
95
|
-
//
|
|
98
|
+
// A slot just opened: poll again immediately, yielding to the event
|
|
99
|
+
// loop first so a saturated queue cannot starve timers.
|
|
100
|
+
scheduleNextPoll(0);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const poll = async () => {
|
|
104
|
+
if (polling ||
|
|
105
|
+
state !== WorkerState.RUNNING ||
|
|
106
|
+
abortController?.signal.aborted) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
polling = true;
|
|
110
|
+
try {
|
|
111
|
+
if (activeJobs >= concurrency) {
|
|
112
|
+
scheduleNextPoll(pollInterval);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const job = await queue.claimNextJob();
|
|
116
|
+
if (!job) {
|
|
117
|
+
scheduleNextPoll(pollInterval);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const proc = queue.getProcessor(job.name);
|
|
121
|
+
if (!proc) {
|
|
122
|
+
// Claimed but unrunnable: release it rather than stranding it in
|
|
123
|
+
// `active` where nothing would ever pick it up again.
|
|
124
|
+
await queue.releaseJob(job.id);
|
|
125
|
+
scheduleNextPoll(pollInterval);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
activeJobs++;
|
|
129
|
+
stats.processed++;
|
|
130
|
+
void runClaimedJob(job);
|
|
131
|
+
// Capacity may remain: look for more work now, not after this job
|
|
132
|
+
// settles.
|
|
96
133
|
scheduleNextPoll(0);
|
|
97
134
|
}
|
|
135
|
+
finally {
|
|
136
|
+
polling = false;
|
|
137
|
+
}
|
|
98
138
|
};
|
|
99
139
|
const clearPollTimer = () => {
|
|
100
140
|
if (pollTimer) {
|
|
@@ -138,13 +178,18 @@ export function createWorker(id, queue, options) {
|
|
|
138
178
|
}
|
|
139
179
|
state = WorkerState.DRAINING;
|
|
140
180
|
clearPollTimer();
|
|
141
|
-
|
|
181
|
+
// Graceful means graceful: in-flight jobs get `drainTimeout` to
|
|
182
|
+
// finish on their own. Aborting them up front — as this once did —
|
|
183
|
+
// made `stop()` indistinguishable from `forceStop()` for any
|
|
184
|
+
// processor that honours its signal, and turned every routine
|
|
185
|
+
// shutdown into a batch of failed jobs.
|
|
142
186
|
const deadline = Date.now() + Math.max(0, drainTimeout);
|
|
143
187
|
while (activeJobs > 0 && Date.now() < deadline) {
|
|
144
188
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
145
189
|
}
|
|
146
190
|
if (activeJobs > 0) {
|
|
147
191
|
onError(new WorkerLifecycleError(`Worker "${id}" still had ${activeJobs} job(s) in flight after ${drainTimeout}ms; forcing stop.`, { workerId: id }));
|
|
192
|
+
abortController?.abort();
|
|
148
193
|
}
|
|
149
194
|
clearPollTimer();
|
|
150
195
|
state = WorkerState.STOPPED;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Queue } from "../queue/queue.type.js";
|
|
1
|
+
import type { Queue, QueueLogger } from "../queue/queue.type.js";
|
|
2
2
|
import type { QueueMiddleware } from "../middleware/middleware.type.js";
|
|
3
3
|
import type { WorkerState } from "../jobTypes/jobTypes.type.js";
|
|
4
4
|
/**
|
|
@@ -25,10 +25,13 @@ export interface WorkerOptions {
|
|
|
25
25
|
readonly drainTimeout?: number;
|
|
26
26
|
/**
|
|
27
27
|
* Invoked for errors raised outside a job — a failing poll, a job that
|
|
28
|
-
* threw, or a drain that timed out. Defaults to
|
|
29
|
-
*
|
|
28
|
+
* threw, or a drain that timed out. Defaults to `logger.error`, or to
|
|
29
|
+
* `process.emitWarning` without a logger. Poll errors are never left as
|
|
30
|
+
* unhandled rejections.
|
|
30
31
|
*/
|
|
31
32
|
readonly onError?: (error: unknown) => void;
|
|
33
|
+
/** Receives errors when no `onError` is given. */
|
|
34
|
+
readonly logger?: QueueLogger;
|
|
32
35
|
}
|
|
33
36
|
/**
|
|
34
37
|
* Worker lifecycle states.
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/queue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Background job and asynchronous task infrastructure with in-memory and adapter-based queue implementations.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -21,9 +25,9 @@
|
|
|
21
25
|
"!dist/.tsbuildinfo"
|
|
22
26
|
],
|
|
23
27
|
"dependencies": {
|
|
24
|
-
"@zudojs/errors": "1.
|
|
25
|
-
"@zudojs/constants": "1.
|
|
26
|
-
"@zudojs/serialization": "1.
|
|
28
|
+
"@zudojs/errors": "1.1.0",
|
|
29
|
+
"@zudojs/constants": "1.1.0",
|
|
30
|
+
"@zudojs/serialization": "1.1.0"
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
29
33
|
"typescript": "7.0.2",
|