@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which job runs next, and whether any job is still owed a run.
|
|
3
|
+
*
|
|
4
|
+
* @module inMemoryQueue/polling/inMemoryQueue.select
|
|
5
|
+
*/
|
|
6
|
+
import type { Job } from "../../job/job.type.js";
|
|
7
|
+
/**
|
|
8
|
+
* When a job became runnable, in epoch milliseconds: its creation, or for a
|
|
9
|
+
* delayed job the moment its delay elapsed.
|
|
10
|
+
*
|
|
11
|
+
* Ordering a due delayed job by `createdAt` let it jump ahead of every job
|
|
12
|
+
* that had been ready and waiting while it was still delayed.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runnableAt(job: Job<unknown>): number;
|
|
15
|
+
/**
|
|
16
|
+
* Selects the job that runs next among those that are waiting and due.
|
|
17
|
+
*
|
|
18
|
+
* Higher priority first; within a priority, the job that became runnable
|
|
19
|
+
* first ({@link runnableAt}); among equals, the one added first. The
|
|
20
|
+
* incumbent is tracked by reference rather than by a sentinel priority, so
|
|
21
|
+
* negative priorities are selectable like any other.
|
|
22
|
+
*/
|
|
23
|
+
export declare function selectNextJob<TData>(jobs: Iterable<Job<TData>>, now: number, predicate?: (job: Job<TData>) => boolean): Job<TData> | null;
|
|
24
|
+
/** Whether any job a consumer can run is still owed a run. */
|
|
25
|
+
export declare function hasPendingWork<TData>(jobs: Iterable<Job<TData>>, isConsumable: (job: Job<TData>) => boolean): boolean;
|
|
26
|
+
//# sourceMappingURL=inMemoryQueue.select.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which job runs next, and whether any job is still owed a run.
|
|
3
|
+
*
|
|
4
|
+
* @module inMemoryQueue/polling/inMemoryQueue.select
|
|
5
|
+
*/
|
|
6
|
+
import { JobState as JobStateEnum } from "../../jobTypes/jobTypes.type.js";
|
|
7
|
+
/** States in which a job still has a run ahead of it. */
|
|
8
|
+
const PENDING_STATES = new Set([
|
|
9
|
+
JobStateEnum.WAITING,
|
|
10
|
+
JobStateEnum.SCHEDULED,
|
|
11
|
+
JobStateEnum.RETRYING,
|
|
12
|
+
JobStateEnum.ACTIVE,
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* When a job became runnable, in epoch milliseconds: its creation, or for a
|
|
16
|
+
* delayed job the moment its delay elapsed.
|
|
17
|
+
*
|
|
18
|
+
* Ordering a due delayed job by `createdAt` let it jump ahead of every job
|
|
19
|
+
* that had been ready and waiting while it was still delayed.
|
|
20
|
+
*/
|
|
21
|
+
export function runnableAt(job) {
|
|
22
|
+
const created = new Date(job.createdAt).getTime();
|
|
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);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Selects the job that runs next among those that are waiting and due.
|
|
30
|
+
*
|
|
31
|
+
* Higher priority first; within a priority, the job that became runnable
|
|
32
|
+
* first ({@link runnableAt}); among equals, the one added first. The
|
|
33
|
+
* incumbent is tracked by reference rather than by a sentinel priority, so
|
|
34
|
+
* negative priorities are selectable like any other.
|
|
35
|
+
*/
|
|
36
|
+
export function selectNextJob(jobs, now, predicate) {
|
|
37
|
+
let next = null;
|
|
38
|
+
let nextAt = 0;
|
|
39
|
+
for (const job of jobs) {
|
|
40
|
+
if (job.state !== JobStateEnum.WAITING)
|
|
41
|
+
continue;
|
|
42
|
+
if (job.scheduledAt && new Date(job.scheduledAt).getTime() > now)
|
|
43
|
+
continue;
|
|
44
|
+
if (predicate && !predicate(job))
|
|
45
|
+
continue;
|
|
46
|
+
const at = runnableAt(job);
|
|
47
|
+
if (next === null ||
|
|
48
|
+
job.priority > next.priority ||
|
|
49
|
+
(job.priority === next.priority && at < nextAt)) {
|
|
50
|
+
next = job;
|
|
51
|
+
nextAt = at;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
/** Whether any job a consumer can run is still owed a run. */
|
|
57
|
+
export function hasPendingWork(jobs, isConsumable) {
|
|
58
|
+
for (const job of jobs) {
|
|
59
|
+
if (PENDING_STATES.has(job.state) && isConsumable(job))
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=inMemoryQueue.select.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/queue/inMemoryQueue/polling
|
|
3
|
+
*
|
|
4
|
+
* The in-memory queue's poll loop and job selection: a fixed or backing-off
|
|
5
|
+
* interval, immediate wake-ups when work arrives, keep-alive while work a
|
|
6
|
+
* consumer can run is pending, and runnable-time ordering.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./inMemoryQueue.poller.js";
|
|
9
|
+
export * from "./inMemoryQueue.select.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/queue/inMemoryQueue/polling
|
|
3
|
+
*
|
|
4
|
+
* The in-memory queue's poll loop and job selection: a fixed or backing-off
|
|
5
|
+
* interval, immediate wake-ups when work arrives, keep-alive while work a
|
|
6
|
+
* consumer can run is pending, and runnable-time ordering.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./inMemoryQueue.poller.js";
|
|
9
|
+
export * from "./inMemoryQueue.select.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
package/dist/job/job.type.d.ts
CHANGED
|
@@ -15,7 +15,11 @@ export interface Job<TData = unknown> {
|
|
|
15
15
|
readonly data: TData;
|
|
16
16
|
/** Current state of the job. */
|
|
17
17
|
readonly state: JobState;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Number of attempts already made, so `0` while the first attempt runs.
|
|
20
|
+
* Inside a processor, `context.attemptNumber` is the 1-based number of
|
|
21
|
+
* the attempt in progress (`attempt + 1`).
|
|
22
|
+
*/
|
|
19
23
|
readonly attempt: number;
|
|
20
24
|
/** Maximum number of attempts allowed. */
|
|
21
25
|
readonly maxAttempts: number;
|
|
@@ -6,6 +6,13 @@ import type { JobProgress } from "../jobResult/jobResult.type.js";
|
|
|
6
6
|
export interface JobContext<TData = unknown> {
|
|
7
7
|
/** The job being processed. */
|
|
8
8
|
readonly job: Job<TData>;
|
|
9
|
+
/**
|
|
10
|
+
* The attempt in progress, 1-based: `1` on the first run, `2` on the first
|
|
11
|
+
* retry. Always `job.attempt + 1`, since `job.attempt` counts the attempts
|
|
12
|
+
* already made. Matches `ctx.attempt` / `ctx.attemptNumber` in
|
|
13
|
+
* `@zudojs/scheduler`.
|
|
14
|
+
*/
|
|
15
|
+
readonly attemptNumber: number;
|
|
9
16
|
/** AbortSignal for cancellation support. */
|
|
10
17
|
readonly signal: AbortSignal;
|
|
11
18
|
/** Update job progress. */
|
|
@@ -32,13 +32,36 @@ export interface QueueOptions {
|
|
|
32
32
|
readonly concurrency?: number;
|
|
33
33
|
/** Default job options. */
|
|
34
34
|
readonly defaultJobOptions?: Partial<JobOptions>;
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Serializer for job payloads. Defaults to `JsonSerializer`, which
|
|
37
|
+
* round-trips `Date`, `BigInt`, `Map`, `Set`, `Uint8Array` and `Error`.
|
|
38
|
+
*/
|
|
36
39
|
readonly serializer?: Serializer;
|
|
37
40
|
/** Middleware for job processing. */
|
|
38
41
|
readonly middleware?: QueueMiddleware[];
|
|
39
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Milliseconds between polls of the queue's own consumer.
|
|
44
|
+
*
|
|
45
|
+
* When set, every poll uses it. When unset, polls start at 50 ms and back
|
|
46
|
+
* off to 2000 ms while the queue is idle. Either way the poller does not
|
|
47
|
+
* wait for its next poll when work arrives: `add()`, a delayed job coming
|
|
48
|
+
* due, a retry's backoff elapsing, `resume()` and a job finishing all wake
|
|
49
|
+
* it immediately.
|
|
50
|
+
*/
|
|
40
51
|
readonly pollInterval?: number;
|
|
41
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Whether pending work holds the Node.js process open. Defaults to `true`:
|
|
54
|
+
* while the queue has waiting, delayed, retrying or running jobs that one of
|
|
55
|
+
* its processors can run, its poll timer is referenced, so a script whose
|
|
56
|
+
* only work is the queue does not exit before the jobs run. An idle queue,
|
|
57
|
+
* a paused one, work no processor handles, and a closed queue never hold it.
|
|
58
|
+
* Set to `false` to leave every timer unreferenced.
|
|
59
|
+
*/
|
|
60
|
+
readonly keepAlive?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Event emitter for queue lifecycle events. Defaults to an in-memory
|
|
63
|
+
* emitter, reachable as `queue.events`.
|
|
64
|
+
*/
|
|
42
65
|
readonly eventEmitter?: QueueEventEmitter;
|
|
43
66
|
/** Store that receives jobs which exhausted their attempts. */
|
|
44
67
|
readonly deadLetterStore?: DeadLetterStore<never>;
|
|
@@ -138,6 +161,14 @@ export interface QueueStats {
|
|
|
138
161
|
export interface Queue<TData = unknown> {
|
|
139
162
|
/** Queue name. */
|
|
140
163
|
readonly name: QueueName;
|
|
164
|
+
/**
|
|
165
|
+
* The emitter this queue publishes lifecycle events on, when it has one.
|
|
166
|
+
*
|
|
167
|
+
* Exposed so a `Worker` can report its own lifecycle
|
|
168
|
+
* (`worker:started`, `worker:stopped`, `worker:error`) on the same
|
|
169
|
+
* emitter as the jobs it runs.
|
|
170
|
+
*/
|
|
171
|
+
readonly events?: QueueEventEmitter;
|
|
141
172
|
/** Add a job to the queue. */
|
|
142
173
|
add(name: string, data: TData, options?: JobOptions): Promise<Job<TData>>;
|
|
143
174
|
/** Process jobs with a processor. */
|
|
@@ -201,6 +232,15 @@ export interface Queue<TData = unknown> {
|
|
|
201
232
|
* turns it off so a worker is never racing the queue for jobs.
|
|
202
233
|
*/
|
|
203
234
|
setAutoProcess?(enabled: boolean): void;
|
|
235
|
+
/**
|
|
236
|
+
* Subscribes to "a job may have become runnable" (added, released,
|
|
237
|
+
* reclaimed, a delay or retry backoff elapsed, the queue resumed). A
|
|
238
|
+
* `Worker` uses it to claim at once instead of on its next poll; a queue
|
|
239
|
+
* without it is simply polled.
|
|
240
|
+
*
|
|
241
|
+
* @returns A function that unsubscribes.
|
|
242
|
+
*/
|
|
243
|
+
onJobReady?(listener: () => void): () => void;
|
|
204
244
|
}
|
|
205
245
|
/**
|
|
206
246
|
* Event types emitted by a queue.
|
|
@@ -228,6 +268,7 @@ export type QueueEventMap = {
|
|
|
228
268
|
job: Job;
|
|
229
269
|
attempt: number;
|
|
230
270
|
};
|
|
271
|
+
/** A running job was aborted from outside — a drain, a close, a cancel. */
|
|
231
272
|
"job:cancelled": {
|
|
232
273
|
job: Job;
|
|
233
274
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { QueueEventEmitter } from "./queueEmitter.type.js";
|
|
2
|
-
import type { QueueEventMap } from "../queue/queue.type.js";
|
|
2
|
+
import type { QueueEventMap, QueueLogger } from "../queue/queue.type.js";
|
|
3
3
|
type EventName = keyof QueueEventMap;
|
|
4
4
|
type Handler<T extends EventName> = (data: QueueEventMap[T]) => void;
|
|
5
5
|
/**
|
|
@@ -12,6 +12,12 @@ export interface QueueEventEmitterOptions {
|
|
|
12
12
|
* the emitting code path.
|
|
13
13
|
*/
|
|
14
14
|
readonly onHandlerError?: (error: unknown, event: EventName) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Receives a throwing listener's error when no `onHandlerError` is given.
|
|
17
|
+
* Without one the failure goes to `process.emitWarning`, bypassing
|
|
18
|
+
* structured logging and redaction.
|
|
19
|
+
*/
|
|
20
|
+
readonly logger?: QueueLogger;
|
|
15
21
|
}
|
|
16
22
|
/**
|
|
17
23
|
* In-memory queue event emitter.
|
|
@@ -24,7 +30,20 @@ export interface QueueEventEmitterOptions {
|
|
|
24
30
|
export declare class InMemoryQueueEventEmitter implements QueueEventEmitter {
|
|
25
31
|
private readonly handlers;
|
|
26
32
|
private readonly onHandlerError;
|
|
33
|
+
/** Whether `onHandlerError` was supplied, so `setLogger` leaves it alone. */
|
|
34
|
+
private readonly hasCustomHandlerError;
|
|
35
|
+
private logger;
|
|
27
36
|
constructor(options?: QueueEventEmitterOptions);
|
|
37
|
+
/**
|
|
38
|
+
* Adopts a logger for the default handler-error report.
|
|
39
|
+
*
|
|
40
|
+
* Called by a queue that was configured with a logger, since the emitter is
|
|
41
|
+
* built before the queue exists and cannot have been given it. A logger or
|
|
42
|
+
* an `onHandlerError` supplied at construction always wins.
|
|
43
|
+
*
|
|
44
|
+
* @param logger - Destination for a throwing listener's error.
|
|
45
|
+
*/
|
|
46
|
+
setLogger(logger: QueueLogger): void;
|
|
28
47
|
emit<K extends EventName>(event: K, data: QueueEventMap[K]): void;
|
|
29
48
|
on<K extends EventName>(event: K, handler: Handler<K>): () => void;
|
|
30
49
|
/**
|
|
@@ -10,15 +10,34 @@ import { reportQueueError } from "../queue/queue.report.js";
|
|
|
10
10
|
export class InMemoryQueueEventEmitter {
|
|
11
11
|
handlers = new Map();
|
|
12
12
|
onHandlerError;
|
|
13
|
+
/** Whether `onHandlerError` was supplied, so `setLogger` leaves it alone. */
|
|
14
|
+
hasCustomHandlerError;
|
|
15
|
+
logger;
|
|
13
16
|
constructor(options = {}) {
|
|
17
|
+
this.hasCustomHandlerError = options.onHandlerError !== undefined;
|
|
18
|
+
this.logger = options.logger;
|
|
14
19
|
this.onHandlerError =
|
|
15
20
|
options.onHandlerError ??
|
|
16
21
|
((error, event) => {
|
|
17
22
|
queueMicrotask(() => {
|
|
18
|
-
reportQueueError(`[@zudojs/queue] Listener for "${event}" threw.`, error);
|
|
23
|
+
reportQueueError(`[@zudojs/queue] Listener for "${event}" threw.`, error, this.logger);
|
|
19
24
|
});
|
|
20
25
|
});
|
|
21
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Adopts a logger for the default handler-error report.
|
|
29
|
+
*
|
|
30
|
+
* Called by a queue that was configured with a logger, since the emitter is
|
|
31
|
+
* built before the queue exists and cannot have been given it. A logger or
|
|
32
|
+
* an `onHandlerError` supplied at construction always wins.
|
|
33
|
+
*
|
|
34
|
+
* @param logger - Destination for a throwing listener's error.
|
|
35
|
+
*/
|
|
36
|
+
setLogger(logger) {
|
|
37
|
+
if (this.hasCustomHandlerError || this.logger)
|
|
38
|
+
return;
|
|
39
|
+
this.logger = logger;
|
|
40
|
+
}
|
|
22
41
|
emit(event, data) {
|
|
23
42
|
const handlers = this.handlers.get(event);
|
|
24
43
|
if (!handlers) {
|
|
@@ -2,19 +2,42 @@
|
|
|
2
2
|
* @zudojs/queue — Serializer
|
|
3
3
|
*
|
|
4
4
|
* Job payload serialization using @zudojs/serialization's JSONSerializer.
|
|
5
|
+
*
|
|
6
|
+
* The default preserves types. Plain JSON turned a `Date` into a string
|
|
7
|
+
* while `Queue<{ d: Date }>` still typed it as a `Date`, so a processor
|
|
8
|
+
* calling `job.data.d.getTime()` crashed; `BigInt` was rejected outright.
|
|
9
|
+
* With `preserveTypes`, `Date`, `BigInt`, `Map`, `Set`, `Uint8Array` and
|
|
10
|
+
* `Error` all come back as themselves, and output for plain JSON data is
|
|
11
|
+
* unchanged.
|
|
5
12
|
*/
|
|
6
13
|
import type { Serializer } from "./serializer.type.js";
|
|
7
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Default serializer: JSON that round-trips `Date`, `BigInt`, `Map`, `Set`,
|
|
16
|
+
* `Uint8Array` and `Error`, backed by @zudojs/serialization.
|
|
17
|
+
*/
|
|
8
18
|
export declare const JsonSerializer: Serializer;
|
|
9
19
|
/**
|
|
10
|
-
* Creates a serializer with custom options.
|
|
20
|
+
* Creates a JSON serializer with custom options.
|
|
11
21
|
*/
|
|
12
22
|
export declare function createJsonSerializer(options?: {
|
|
13
23
|
/** Indentation width. Any value turns on pretty-printing. */
|
|
14
24
|
space?: number;
|
|
15
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Preserve BigInt, Date, Map, Set, Uint8Array and Error across the round
|
|
27
|
+
* trip. Defaults to `true`, like `JsonSerializer`. With `false` a `Date`
|
|
28
|
+
* comes back as its ISO string, so type such payload fields as `string`.
|
|
29
|
+
*/
|
|
16
30
|
preserveTypes?: boolean;
|
|
17
31
|
}): Serializer;
|
|
18
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Serializer that keeps payloads as they are.
|
|
34
|
+
*
|
|
35
|
+
* It sets `passthrough`, so the in-memory queue stores payloads by reference
|
|
36
|
+
* (class instances, functions and all) instead of round-tripping them. Used
|
|
37
|
+
* standalone it must still honour the `Serializer` contract of producing a
|
|
38
|
+
* string: a string is returned untouched, anything else is JSON-encoded, and
|
|
39
|
+
* `deserialize` parses JSON where it can and returns the raw string where it
|
|
40
|
+
* cannot.
|
|
41
|
+
*/
|
|
19
42
|
export declare const PassthroughSerializer: Serializer;
|
|
20
43
|
//# sourceMappingURL=serializer.core.d.ts.map
|
|
@@ -2,48 +2,67 @@
|
|
|
2
2
|
* @zudojs/queue — Serializer
|
|
3
3
|
*
|
|
4
4
|
* Job payload serialization using @zudojs/serialization's JSONSerializer.
|
|
5
|
+
*
|
|
6
|
+
* The default preserves types. Plain JSON turned a `Date` into a string
|
|
7
|
+
* while `Queue<{ d: Date }>` still typed it as a `Date`, so a processor
|
|
8
|
+
* calling `job.data.d.getTime()` crashed; `BigInt` was rejected outright.
|
|
9
|
+
* With `preserveTypes`, `Date`, `BigInt`, `Map`, `Set`, `Uint8Array` and
|
|
10
|
+
* `Error` all come back as themselves, and output for plain JSON data is
|
|
11
|
+
* unchanged.
|
|
5
12
|
*/
|
|
6
13
|
import { JSONSerializer } from "@zudojs/serialization";
|
|
7
|
-
|
|
14
|
+
const inner = new JSONSerializer();
|
|
15
|
+
/**
|
|
16
|
+
* Default serializer: JSON that round-trips `Date`, `BigInt`, `Map`, `Set`,
|
|
17
|
+
* `Uint8Array` and `Error`, backed by @zudojs/serialization.
|
|
18
|
+
*/
|
|
8
19
|
export const JsonSerializer = Object.freeze({
|
|
9
20
|
serialize(data) {
|
|
10
|
-
return
|
|
21
|
+
return inner.serialize(data, { preserveTypes: true });
|
|
11
22
|
},
|
|
12
23
|
deserialize(data) {
|
|
13
|
-
return
|
|
24
|
+
return inner.deserialize(data, { preserveTypes: true });
|
|
14
25
|
},
|
|
15
26
|
});
|
|
16
27
|
/**
|
|
17
|
-
* Creates a serializer with custom options.
|
|
28
|
+
* Creates a JSON serializer with custom options.
|
|
18
29
|
*/
|
|
19
30
|
export function createJsonSerializer(options) {
|
|
20
|
-
const
|
|
31
|
+
const preserveTypes = options?.preserveTypes ?? true;
|
|
21
32
|
return Object.freeze({
|
|
22
33
|
serialize(data) {
|
|
23
34
|
return inner.serialize(data, {
|
|
24
35
|
pretty: options?.space !== undefined,
|
|
25
|
-
preserveTypes
|
|
36
|
+
preserveTypes,
|
|
26
37
|
indent: options?.space,
|
|
27
38
|
});
|
|
28
39
|
},
|
|
29
40
|
deserialize(data) {
|
|
30
|
-
return inner.deserialize(data, {
|
|
31
|
-
preserveTypes: options?.preserveTypes,
|
|
32
|
-
});
|
|
41
|
+
return inner.deserialize(data, { preserveTypes });
|
|
33
42
|
},
|
|
34
43
|
});
|
|
35
44
|
}
|
|
36
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Serializer that keeps payloads as they are.
|
|
47
|
+
*
|
|
48
|
+
* It sets `passthrough`, so the in-memory queue stores payloads by reference
|
|
49
|
+
* (class instances, functions and all) instead of round-tripping them. Used
|
|
50
|
+
* standalone it must still honour the `Serializer` contract of producing a
|
|
51
|
+
* string: a string is returned untouched, anything else is JSON-encoded, and
|
|
52
|
+
* `deserialize` parses JSON where it can and returns the raw string where it
|
|
53
|
+
* cannot.
|
|
54
|
+
*/
|
|
37
55
|
export const PassthroughSerializer = Object.freeze({
|
|
56
|
+
passthrough: true,
|
|
38
57
|
serialize(data) {
|
|
39
58
|
if (typeof data === "string") {
|
|
40
59
|
return data;
|
|
41
60
|
}
|
|
42
|
-
return
|
|
61
|
+
return inner.serialize(data);
|
|
43
62
|
},
|
|
44
63
|
deserialize(data) {
|
|
45
64
|
try {
|
|
46
|
-
return
|
|
65
|
+
return inner.deserialize(data);
|
|
47
66
|
}
|
|
48
67
|
catch {
|
|
49
68
|
return data;
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* Provides serialization and deserialization of job data.
|
|
5
5
|
*/
|
|
6
6
|
export interface Serializer {
|
|
7
|
+
/**
|
|
8
|
+
* Marks a serializer that stores values as given. The in-memory queue
|
|
9
|
+
* skips its round trip for such a serializer and keeps the payload by
|
|
10
|
+
* reference, exactly as with `serializePayloads: false`.
|
|
11
|
+
*/
|
|
12
|
+
readonly passthrough?: boolean;
|
|
7
13
|
/** Serialize data to a string. */
|
|
8
14
|
serialize<T>(data: T): string;
|
|
9
15
|
/** Deserialize a string to data. */
|
|
@@ -25,13 +25,32 @@ export function createWorker(id, queue, options) {
|
|
|
25
25
|
let activeJobs = 0;
|
|
26
26
|
let polling = false;
|
|
27
27
|
let abortController = null;
|
|
28
|
+
const keepAlive = options?.keepAlive ?? true;
|
|
29
|
+
let stopWatching;
|
|
28
30
|
queue.setAutoProcess?.(false);
|
|
29
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Reports the worker's own lifecycle on the queue's emitter.
|
|
33
|
+
*
|
|
34
|
+
* `worker:started`, `worker:stopped` and `worker:error` were part of
|
|
35
|
+
* `QueueEventMap` with nothing emitting them, so a consumer subscribing for
|
|
36
|
+
* readiness never heard from the worker.
|
|
37
|
+
*/
|
|
38
|
+
const emitLifecycle = (event) => {
|
|
39
|
+
queue.events?.emit(event, { workerId: id });
|
|
40
|
+
};
|
|
41
|
+
const reportWorkerError = options?.onError ??
|
|
30
42
|
((error) => {
|
|
31
43
|
queueMicrotask(() => {
|
|
32
44
|
reportQueueError(`[@zudojs/queue] Worker "${id}" poll failed.`, error, options?.logger);
|
|
33
45
|
});
|
|
34
46
|
});
|
|
47
|
+
const onError = (error) => {
|
|
48
|
+
queue.events?.emit("worker:error", {
|
|
49
|
+
workerId: id,
|
|
50
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
51
|
+
});
|
|
52
|
+
reportWorkerError(error);
|
|
53
|
+
};
|
|
35
54
|
/**
|
|
36
55
|
* Arms the next poll. At most one timer is ever armed: a delayed poll
|
|
37
56
|
* already pending is left alone, while an immediate poll (capacity just
|
|
@@ -47,7 +66,19 @@ export function createWorker(id, queue, options) {
|
|
|
47
66
|
clearTimeout(pollTimer);
|
|
48
67
|
}
|
|
49
68
|
pollTimer = setTimeout(runPoll, delay);
|
|
50
|
-
|
|
69
|
+
// A started worker is the process's reason to stay alive until it is
|
|
70
|
+
// stopped; an unreferenced timer let a script exit before any job ran.
|
|
71
|
+
if (!keepAlive)
|
|
72
|
+
pollTimer.unref?.();
|
|
73
|
+
};
|
|
74
|
+
/** Claims at once when the queue reports a runnable job, if a slot is free. */
|
|
75
|
+
const onJobReady = () => {
|
|
76
|
+
if (activeJobs < concurrency)
|
|
77
|
+
scheduleNextPoll(0);
|
|
78
|
+
};
|
|
79
|
+
const unwatch = () => {
|
|
80
|
+
stopWatching?.();
|
|
81
|
+
stopWatching = undefined;
|
|
51
82
|
};
|
|
52
83
|
/**
|
|
53
84
|
* Wraps `poll` so a rejection can never escape as an unhandled promise
|
|
@@ -157,6 +188,8 @@ export function createWorker(id, queue, options) {
|
|
|
157
188
|
abortController = new AbortController();
|
|
158
189
|
try {
|
|
159
190
|
state = WorkerState.RUNNING;
|
|
191
|
+
stopWatching = queue.onJobReady?.(onJobReady);
|
|
192
|
+
emitLifecycle("worker:started");
|
|
160
193
|
scheduleNextPoll(0);
|
|
161
194
|
}
|
|
162
195
|
catch (error) {
|
|
@@ -174,10 +207,12 @@ export function createWorker(id, queue, options) {
|
|
|
174
207
|
if (state !== WorkerState.RUNNING && state !== WorkerState.STARTING) {
|
|
175
208
|
// Still clear any timer armed before the state moved on.
|
|
176
209
|
clearPollTimer();
|
|
210
|
+
unwatch();
|
|
177
211
|
return;
|
|
178
212
|
}
|
|
179
213
|
state = WorkerState.DRAINING;
|
|
180
214
|
clearPollTimer();
|
|
215
|
+
unwatch();
|
|
181
216
|
// Graceful means graceful: in-flight jobs get `drainTimeout` to
|
|
182
217
|
// finish on their own. Aborting them up front — as this once did —
|
|
183
218
|
// made `stop()` indistinguishable from `forceStop()` for any
|
|
@@ -193,11 +228,18 @@ export function createWorker(id, queue, options) {
|
|
|
193
228
|
}
|
|
194
229
|
clearPollTimer();
|
|
195
230
|
state = WorkerState.STOPPED;
|
|
231
|
+
emitLifecycle("worker:stopped");
|
|
196
232
|
},
|
|
197
233
|
async forceStop() {
|
|
234
|
+
const wasLive = state !== WorkerState.CREATED && state !== WorkerState.STOPPED;
|
|
198
235
|
abortController?.abort();
|
|
199
236
|
clearPollTimer();
|
|
237
|
+
unwatch();
|
|
200
238
|
state = WorkerState.STOPPED;
|
|
239
|
+
// A worker that never started never stopped: reporting it would give a
|
|
240
|
+
// readiness listener a transition that did not happen.
|
|
241
|
+
if (wasLive)
|
|
242
|
+
emitLifecycle("worker:stopped");
|
|
201
243
|
},
|
|
202
244
|
isRunning() {
|
|
203
245
|
return state === WorkerState.RUNNING;
|
|
@@ -7,8 +7,18 @@ import type { WorkerState } from "../jobTypes/jobTypes.type.js";
|
|
|
7
7
|
export interface WorkerOptions {
|
|
8
8
|
/** Maximum number of jobs to process concurrently. */
|
|
9
9
|
readonly concurrency?: number;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Milliseconds between polls while idle. Defaults to 100. A queue that
|
|
12
|
+
* supports `onJobReady` (the in-memory queue does) wakes the worker as soon
|
|
13
|
+
* as a job becomes runnable, so this bounds only how often it re-checks.
|
|
14
|
+
*/
|
|
11
15
|
readonly pollInterval?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Whether a started worker holds the Node.js process open until `stop()`
|
|
18
|
+
* or `forceStop()`. Defaults to `true`; `false` leaves its timers
|
|
19
|
+
* unreferenced.
|
|
20
|
+
*/
|
|
21
|
+
readonly keepAlive?: boolean;
|
|
12
22
|
/**
|
|
13
23
|
* Default job timeout in milliseconds, applied to jobs that do not
|
|
14
24
|
* carry their own. Stall detection is a queue-level concern; configure
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/queue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Background job and asynchronous task infrastructure with in-memory and adapter-based queue implementations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"!dist/.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@zudojs/errors": "1.
|
|
29
|
-
"@zudojs/constants": "1.1.
|
|
30
|
-
"@zudojs/serialization": "1.
|
|
28
|
+
"@zudojs/errors": "1.3.0",
|
|
29
|
+
"@zudojs/constants": "1.1.2",
|
|
30
|
+
"@zudojs/serialization": "1.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"typescript": "7.0.2",
|
|
34
|
-
"vitest": "^
|
|
34
|
+
"vitest": "^5.0.1"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=24.0.0"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"background-jobs",
|
|
47
47
|
"worker"
|
|
48
48
|
],
|
|
49
|
-
"homepage": "https://
|
|
49
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-queue",
|
|
50
50
|
"bugs": {
|
|
51
51
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
52
52
|
},
|