@zudojs/queue 1.2.0 → 1.4.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 +70 -2
- package/dist/contextCarrier/contextCarrier.core.d.ts +6 -1
- package/dist/contextCarrier/contextCarrier.core.js +23 -3
- package/dist/deadLetter/deadLetter.core.d.ts +26 -1
- package/dist/deadLetter/deadLetter.core.js +27 -1
- package/dist/deadLetter/index.d.ts +2 -1
- package/dist/deadLetter/index.js +1 -1
- package/dist/inMemoryQueue/inMemoryQueue.core.d.ts +42 -12
- package/dist/inMemoryQueue/inMemoryQueue.core.js +140 -95
- package/dist/inMemoryQueue/inMemoryQueue.processing.d.ts +2 -0
- package/dist/inMemoryQueue/inMemoryQueue.processing.js +11 -2
- package/dist/inMemoryQueue/inMemoryQueue.scheduling.d.ts +6 -3
- package/dist/inMemoryQueue/inMemoryQueue.scheduling.js +8 -3
- package/dist/inMemoryQueue/polling/inMemoryQueue.poller.d.ts +64 -0
- package/dist/inMemoryQueue/polling/inMemoryQueue.poller.js +132 -0
- package/dist/inMemoryQueue/polling/inMemoryQueue.select.d.ts +26 -0
- package/dist/inMemoryQueue/polling/inMemoryQueue.select.js +64 -0
- package/dist/inMemoryQueue/polling/index.d.ts +10 -0
- package/dist/inMemoryQueue/polling/index.js +10 -0
- package/dist/job/job.type.d.ts +5 -1
- package/dist/jobContext/jobContext.core.js +1 -0
- package/dist/jobContext/jobContext.type.d.ts +7 -0
- package/dist/queue/queue.type.d.ts +44 -3
- package/dist/queueEmitter/queueEmitter.core.d.ts +20 -1
- package/dist/queueEmitter/queueEmitter.core.js +20 -1
- package/dist/serializer/serializer.core.d.ts +27 -4
- package/dist/serializer/serializer.core.js +31 -12
- package/dist/serializer/serializer.type.d.ts +6 -0
- package/dist/worker/worker.core.js +44 -2
- package/dist/worker/worker.type.d.ts +11 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -69,8 +69,72 @@ await queue.close();
|
|
|
69
69
|
- Draining shutdown with a bounded `closeTimeout`, then cooperative abort
|
|
70
70
|
- Stalled-job reclaim (`stalledAfter`, `maxStalledCount`) for consumers that die
|
|
71
71
|
mid-job
|
|
72
|
-
- Bounded retention of settled jobs, so a long-lived
|
|
73
|
-
limit
|
|
72
|
+
- Bounded retention of settled jobs and of dead-lettered jobs, so a long-lived
|
|
73
|
+
queue does not grow without limit
|
|
74
|
+
|
|
75
|
+
## Polling and process lifetime
|
|
76
|
+
|
|
77
|
+
The queue's own consumer polls every `pollInterval` milliseconds when you set
|
|
78
|
+
one; unset, it starts at 50 ms and backs off to 2000 ms while idle. It never
|
|
79
|
+
waits out that interval when work arrives: `add()`, a delayed job coming due, a
|
|
80
|
+
retry's backoff elapsing, `resume()` and a finished job all wake it at once. A
|
|
81
|
+
`Worker` is woken the same way through `queue.onJobReady`, so its
|
|
82
|
+
`pollInterval` only bounds how often an idle worker re-checks.
|
|
83
|
+
|
|
84
|
+
Pending work keeps the Node.js process alive. While the queue has waiting,
|
|
85
|
+
delayed, retrying or running jobs that one of its processors can run, a script
|
|
86
|
+
whose only work is the queue does not exit before the jobs run. A started
|
|
87
|
+
`Worker` keeps the process alive until `stop()` or `forceStop()`. An idle queue,
|
|
88
|
+
a paused one, work that no processor handles, and a closed queue never hold the
|
|
89
|
+
process open. Pass `keepAlive: false` (to the queue or the worker) for the old
|
|
90
|
+
behaviour, where every timer was unreferenced.
|
|
91
|
+
|
|
92
|
+
## Payloads
|
|
93
|
+
|
|
94
|
+
Payloads round-trip through the serializer on `add()`, so the stored job is a
|
|
95
|
+
copy. The default `JsonSerializer` preserves `Date`, `BigInt`, `Map`, `Set`,
|
|
96
|
+
`Uint8Array` and `Error`, so a field typed `Date` arrives as a `Date`:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const reminders = createInMemoryQueue<{ at: Date }>(createQueueName("reminders"));
|
|
100
|
+
reminders.process("remind", async (job) => job.data.at.getTime()); // a real Date
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`createJsonSerializer({ preserveTypes: false })` gives plain JSON, where a
|
|
104
|
+
`Date` becomes its ISO string (type it as `string`). `PassthroughSerializer`
|
|
105
|
+
(or `serializePayloads: false`) stores payloads by reference, class instances
|
|
106
|
+
included, with no copy and no serialization.
|
|
107
|
+
|
|
108
|
+
## Ordering
|
|
109
|
+
|
|
110
|
+
Higher `priority` runs first. Within a priority, jobs run in the order they
|
|
111
|
+
became runnable: when they were added, or for a delayed job when its delay
|
|
112
|
+
elapsed, so a delayed job never jumps ahead of jobs that were already waiting
|
|
113
|
+
when it came due. A retried job keeps its original place in line.
|
|
114
|
+
|
|
115
|
+
## Attempts
|
|
116
|
+
|
|
117
|
+
`job.attempt` counts the attempts already made, so it is `0` while the first
|
|
118
|
+
attempt runs. `context.attemptNumber` is the 1-based number of the attempt in
|
|
119
|
+
progress (`job.attempt + 1`), the same convention as `ctx.attempt` in
|
|
120
|
+
`@zudojs/scheduler`:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
queue.process("sync", async (job, context) => {
|
|
124
|
+
context.log(`attempt ${context.attemptNumber} of ${job.maxAttempts}`);
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Events
|
|
129
|
+
|
|
130
|
+
`queue.events` works without configuration: a queue created without an
|
|
131
|
+
`eventEmitter` gets an in-memory one.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
queue.events?.on("job:completed", ({ job, result }) => {
|
|
135
|
+
console.log(job.id, result);
|
|
136
|
+
});
|
|
137
|
+
```
|
|
74
138
|
|
|
75
139
|
## Workers, timeouts and context
|
|
76
140
|
|
|
@@ -120,6 +184,10 @@ Keep captured values small and serializable (ids, not live objects). With
|
|
|
120
184
|
`@zudojs/tenancy`, capture the tenant id and restore it with the tenant context
|
|
121
185
|
storage's `run`.
|
|
122
186
|
|
|
187
|
+
The `zudo:context` metadata key is reserved: it is written only by the queue's
|
|
188
|
+
own carriers and is stripped from any `metadata` passed to `add()`, so an
|
|
189
|
+
enqueuer cannot choose the context its job runs under.
|
|
190
|
+
|
|
123
191
|
Errors with no caller to receive them (a worker's failing poll, a throwing event
|
|
124
192
|
listener) go to `logger.error` when a logger is configured, and otherwise to
|
|
125
193
|
`process.emitWarning`, never to `console`.
|
|
@@ -13,10 +13,15 @@ export declare const CONTEXT_METADATA_KEY = "zudo:context";
|
|
|
13
13
|
/**
|
|
14
14
|
* Captures every carrier's value into a metadata record.
|
|
15
15
|
*
|
|
16
|
+
* {@link CONTEXT_METADATA_KEY} is owned by the queue: a value the caller put
|
|
17
|
+
* under that key is always discarded, whether or not a carrier captured
|
|
18
|
+
* anything, so an enqueuer cannot forge the tenant, correlation id or trace
|
|
19
|
+
* the processor runs under.
|
|
20
|
+
*
|
|
16
21
|
* @param carriers - The queue's context carriers.
|
|
17
22
|
* @param metadata - The job's own metadata, if any.
|
|
18
23
|
* @returns The metadata with a {@link CONTEXT_METADATA_KEY} entry added, or
|
|
19
|
-
*
|
|
24
|
+
* without one when no carrier captured anything.
|
|
20
25
|
*/
|
|
21
26
|
export declare function captureContext(carriers: readonly QueueContextCarrier[] | undefined, metadata: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
22
27
|
/**
|
|
@@ -8,17 +8,37 @@
|
|
|
8
8
|
* `{ [carrier.key]: value }` record.
|
|
9
9
|
*/
|
|
10
10
|
export const CONTEXT_METADATA_KEY = "zudo:context";
|
|
11
|
+
/**
|
|
12
|
+
* Strips the reserved context key from caller-supplied metadata.
|
|
13
|
+
*
|
|
14
|
+
* The key is owned by the queue on every path: whatever the enqueuer put
|
|
15
|
+
* there is dropped, so `add()` metadata can never supply a context record of
|
|
16
|
+
* its own. Returns the input untouched when there is nothing to strip, so the
|
|
17
|
+
* common case still allocates nothing.
|
|
18
|
+
*/
|
|
19
|
+
function withoutStoredContext(metadata) {
|
|
20
|
+
if (!metadata || !Object.hasOwn(metadata, CONTEXT_METADATA_KEY)) {
|
|
21
|
+
return metadata;
|
|
22
|
+
}
|
|
23
|
+
const { [CONTEXT_METADATA_KEY]: _discarded, ...rest } = metadata;
|
|
24
|
+
return rest;
|
|
25
|
+
}
|
|
11
26
|
/**
|
|
12
27
|
* Captures every carrier's value into a metadata record.
|
|
13
28
|
*
|
|
29
|
+
* {@link CONTEXT_METADATA_KEY} is owned by the queue: a value the caller put
|
|
30
|
+
* under that key is always discarded, whether or not a carrier captured
|
|
31
|
+
* anything, so an enqueuer cannot forge the tenant, correlation id or trace
|
|
32
|
+
* the processor runs under.
|
|
33
|
+
*
|
|
14
34
|
* @param carriers - The queue's context carriers.
|
|
15
35
|
* @param metadata - The job's own metadata, if any.
|
|
16
36
|
* @returns The metadata with a {@link CONTEXT_METADATA_KEY} entry added, or
|
|
17
|
-
*
|
|
37
|
+
* without one when no carrier captured anything.
|
|
18
38
|
*/
|
|
19
39
|
export function captureContext(carriers, metadata) {
|
|
20
40
|
if (!carriers || carriers.length === 0)
|
|
21
|
-
return metadata;
|
|
41
|
+
return withoutStoredContext(metadata);
|
|
22
42
|
const captured = {};
|
|
23
43
|
let any = false;
|
|
24
44
|
for (const carrier of carriers) {
|
|
@@ -29,7 +49,7 @@ export function captureContext(carriers, metadata) {
|
|
|
29
49
|
any = true;
|
|
30
50
|
}
|
|
31
51
|
if (!any)
|
|
32
|
-
return metadata;
|
|
52
|
+
return withoutStoredContext(metadata);
|
|
33
53
|
return { ...metadata, [CONTEXT_METADATA_KEY]: Object.freeze(captured) };
|
|
34
54
|
}
|
|
35
55
|
/**
|
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
import type { Job } from "../job/job.type.js";
|
|
2
2
|
import type { DeadLetterStore } from "./deadLetter.type.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dead-lettered jobs retained by the in-memory store when no cap is given.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the queue's own `DEFAULT_RETAINED_JOBS`: the dead letter store
|
|
7
|
+
* keeps a full copy of every failed job — payload, metadata and error — so an
|
|
8
|
+
* unbounded one grows for the life of the process at whatever the failure
|
|
9
|
+
* rate happens to be.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_DEAD_LETTER_JOBS = 1000;
|
|
12
|
+
/**
|
|
13
|
+
* Options for {@link createInMemoryDeadLetterStore}.
|
|
14
|
+
*/
|
|
15
|
+
export interface InMemoryDeadLetterStoreOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Maximum number of dead-lettered jobs retained before the oldest are
|
|
18
|
+
* evicted. Defaults to {@link DEFAULT_DEAD_LETTER_JOBS}. Pass
|
|
19
|
+
* `Number.POSITIVE_INFINITY` for the previous unbounded behaviour.
|
|
20
|
+
*/
|
|
21
|
+
readonly maxEntries?: number;
|
|
22
|
+
}
|
|
3
23
|
/**
|
|
4
24
|
* Creates an in-memory dead letter store.
|
|
25
|
+
*
|
|
26
|
+
* Retention is bounded: past `maxEntries` the oldest entry is evicted, so a
|
|
27
|
+
* long-lived queue with a steady failure rate does not grow without limit.
|
|
28
|
+
*
|
|
29
|
+
* @param options - Retention options.
|
|
5
30
|
*/
|
|
6
|
-
export declare function createInMemoryDeadLetterStore<TData = unknown>(): DeadLetterStore<TData>;
|
|
31
|
+
export declare function createInMemoryDeadLetterStore<TData = unknown>(options?: InMemoryDeadLetterStoreOptions): DeadLetterStore<TData>;
|
|
7
32
|
/**
|
|
8
33
|
* Moves a failed job to the dead letter store.
|
|
9
34
|
*/
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import { toQueueError } from "@zudojs/errors";
|
|
2
|
+
/**
|
|
3
|
+
* Dead-lettered jobs retained by the in-memory store when no cap is given.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the queue's own `DEFAULT_RETAINED_JOBS`: the dead letter store
|
|
6
|
+
* keeps a full copy of every failed job — payload, metadata and error — so an
|
|
7
|
+
* unbounded one grows for the life of the process at whatever the failure
|
|
8
|
+
* rate happens to be.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_DEAD_LETTER_JOBS = 1_000;
|
|
2
11
|
/**
|
|
3
12
|
* Creates an in-memory dead letter store.
|
|
13
|
+
*
|
|
14
|
+
* Retention is bounded: past `maxEntries` the oldest entry is evicted, so a
|
|
15
|
+
* long-lived queue with a steady failure rate does not grow without limit.
|
|
16
|
+
*
|
|
17
|
+
* @param options - Retention options.
|
|
4
18
|
*/
|
|
5
|
-
export function createInMemoryDeadLetterStore() {
|
|
19
|
+
export function createInMemoryDeadLetterStore(options) {
|
|
6
20
|
const store = new Map();
|
|
21
|
+
const configured = options?.maxEntries ?? DEFAULT_DEAD_LETTER_JOBS;
|
|
22
|
+
const maxEntries = configured > 0 ? configured : DEFAULT_DEAD_LETTER_JOBS;
|
|
7
23
|
return {
|
|
8
24
|
async add(deadLetterJob) {
|
|
25
|
+
// Re-inserting an existing id must not count as a new entry, so
|
|
26
|
+
// delete first: a Map keeps its original insertion order otherwise,
|
|
27
|
+
// which would also evict the wrong entry.
|
|
28
|
+
store.delete(deadLetterJob.job.id);
|
|
9
29
|
store.set(deadLetterJob.job.id, deadLetterJob);
|
|
30
|
+
while (store.size > maxEntries) {
|
|
31
|
+
const oldest = store.keys().next().value;
|
|
32
|
+
if (oldest === undefined)
|
|
33
|
+
break;
|
|
34
|
+
store.delete(oldest);
|
|
35
|
+
}
|
|
10
36
|
},
|
|
11
37
|
async get(jobId) {
|
|
12
38
|
return store.get(jobId) ?? null;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Provides storage and management for jobs that have
|
|
5
5
|
* exceeded their retry attempts.
|
|
6
6
|
*/
|
|
7
|
-
export { createInMemoryDeadLetterStore, moveToDeadLetter, } from "./deadLetter.core.js";
|
|
7
|
+
export { DEFAULT_DEAD_LETTER_JOBS, createInMemoryDeadLetterStore, moveToDeadLetter, } from "./deadLetter.core.js";
|
|
8
|
+
export type { InMemoryDeadLetterStoreOptions } from "./deadLetter.core.js";
|
|
8
9
|
export type { DeadLetterJob, DeadLetterStore } from "./deadLetter.type.js";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/deadLetter/index.js
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Provides storage and management for jobs that have
|
|
5
5
|
* exceeded their retry attempts.
|
|
6
6
|
*/
|
|
7
|
-
export { createInMemoryDeadLetterStore, moveToDeadLetter, } from "./deadLetter.core.js";
|
|
7
|
+
export { DEFAULT_DEAD_LETTER_JOBS, createInMemoryDeadLetterStore, moveToDeadLetter, } from "./deadLetter.core.js";
|
|
8
8
|
//# sourceMappingURL=index.js.map
|
|
@@ -4,6 +4,7 @@ import type { Queue, QueueOptions, QueueStats } from "../queue/queue.type.js";
|
|
|
4
4
|
import type { Processor } from "../processor/processor.type.js";
|
|
5
5
|
import type { JobOptions } from "../jobOptions/jobOptions.type.js";
|
|
6
6
|
import type { QueueMiddleware } from "../middleware/middleware.type.js";
|
|
7
|
+
import type { QueueEventEmitter } from "../queueEmitter/queueEmitter.type.js";
|
|
7
8
|
import type { DeadLetterJob } from "../deadLetter/deadLetter.type.js";
|
|
8
9
|
/**
|
|
9
10
|
* In-memory queue implementation.
|
|
@@ -23,7 +24,9 @@ export declare class InMemoryQueue<TData = unknown> implements Queue<TData> {
|
|
|
23
24
|
/** Whether the internal poller claims jobs; see `setAutoProcess`. */
|
|
24
25
|
private autoProcess;
|
|
25
26
|
private activeCount;
|
|
26
|
-
private
|
|
27
|
+
private readonly poller;
|
|
28
|
+
/** Consumers to tell when a job may have become runnable. */
|
|
29
|
+
private readonly readyListeners;
|
|
27
30
|
private readonly scheduledTimers;
|
|
28
31
|
private readonly retryTimers;
|
|
29
32
|
private readonly inFlight;
|
|
@@ -32,11 +35,23 @@ export declare class InMemoryQueue<TData = unknown> implements Queue<TData> {
|
|
|
32
35
|
private readonly stalledCounts;
|
|
33
36
|
private readonly deduplicationIndex;
|
|
34
37
|
private readonly deadLetterStore;
|
|
38
|
+
/**
|
|
39
|
+
* Whether this queue created its own dead letter store. A store handed in
|
|
40
|
+
* by the caller outlives the queue and is theirs to clear.
|
|
41
|
+
*/
|
|
42
|
+
private readonly ownsDeadLetterStore;
|
|
35
43
|
private readonly emitter;
|
|
36
44
|
private readonly counters;
|
|
37
|
-
private emptySince;
|
|
38
|
-
private backoffMs;
|
|
39
45
|
constructor(name: QueueName, options?: QueueOptions);
|
|
46
|
+
/**
|
|
47
|
+
* Subscribes to "a job may have become runnable": one was added, released
|
|
48
|
+
* or reclaimed, a delay or retry backoff elapsed, or the queue resumed.
|
|
49
|
+
*
|
|
50
|
+
* A `Worker` uses this to claim immediately instead of on its next poll.
|
|
51
|
+
*
|
|
52
|
+
* @returns A function that unsubscribes.
|
|
53
|
+
*/
|
|
54
|
+
onJobReady(listener: () => void): () => void;
|
|
40
55
|
/**
|
|
41
56
|
* Turns the internal poller on or off as a consumer.
|
|
42
57
|
*
|
|
@@ -46,6 +61,14 @@ export declare class InMemoryQueue<TData = unknown> implements Queue<TData> {
|
|
|
46
61
|
* concurrency apply to every job.
|
|
47
62
|
*/
|
|
48
63
|
setAutoProcess(enabled: boolean): void;
|
|
64
|
+
/**
|
|
65
|
+
* The emitter this queue publishes lifecycle events on.
|
|
66
|
+
*
|
|
67
|
+
* An in-memory emitter when the queue was created without one, so
|
|
68
|
+
* `queue.events.on(...)` works out of the box and a worker can report its
|
|
69
|
+
* lifecycle unconditionally.
|
|
70
|
+
*/
|
|
71
|
+
get events(): QueueEventEmitter;
|
|
49
72
|
add(jobName: string, data: TData, options?: JobOptions): Promise<Job<TData>>;
|
|
50
73
|
process(name: string, processor: Processor<TData>): void;
|
|
51
74
|
getJob(jobId: JobId): Promise<Job<TData> | null>;
|
|
@@ -100,17 +123,18 @@ export declare class InMemoryQueue<TData = unknown> implements Queue<TData> {
|
|
|
100
123
|
*/
|
|
101
124
|
private encodePayload;
|
|
102
125
|
/**
|
|
103
|
-
* Selects the
|
|
104
|
-
*
|
|
105
|
-
* Ties on priority are broken by creation time, oldest first. The
|
|
106
|
-
* incumbent is tracked by reference rather than by a sentinel priority,
|
|
107
|
-
* so jobs with negative priorities are selectable like any other.
|
|
126
|
+
* Selects the next job that is due and runnable; see {@link selectNextJob}
|
|
127
|
+
* for the ordering.
|
|
108
128
|
*/
|
|
109
129
|
private selectJob;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Whether the poll loop should hold the process open: while the queue is
|
|
132
|
+
* consuming and has work one of its processors can run. Work nothing can
|
|
133
|
+
* consume, a paused queue and `keepAlive: false` never do.
|
|
134
|
+
*/
|
|
135
|
+
private shouldKeepAlive;
|
|
136
|
+
/** Wakes the poll loop and tells consumers a job may be runnable. */
|
|
137
|
+
private jobReady;
|
|
114
138
|
/**
|
|
115
139
|
* Runs an already-claimed job through this queue's processing pipeline.
|
|
116
140
|
*
|
|
@@ -125,6 +149,12 @@ export declare class InMemoryQueue<TData = unknown> implements Queue<TData> {
|
|
|
125
149
|
/** Fallback timeout for a job that carries none of its own. */
|
|
126
150
|
timeoutMs?: number;
|
|
127
151
|
}): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* One poll: promotes due delayed jobs, claims runnable jobs up to the
|
|
154
|
+
* concurrency limit, and reclaims stalled ones.
|
|
155
|
+
*
|
|
156
|
+
* @returns Whether any job was dispatched.
|
|
157
|
+
*/
|
|
128
158
|
private processTick;
|
|
129
159
|
/**
|
|
130
160
|
* Returns jobs stuck in `active` to the waiting pool.
|