bunqueue-client 0.1.5 → 0.1.7
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/CHANGELOG.md +119 -0
- package/README.md +46 -3
- package/dist/ack-batcher.d.ts +28 -0
- package/dist/ack-batcher.js +67 -0
- package/dist/backpressure.d.ts +22 -0
- package/dist/backpressure.js +39 -0
- package/dist/bunqueue/bunqueue.js +12 -2
- package/dist/connection-pool.d.ts +30 -0
- package/dist/connection-pool.js +60 -0
- package/dist/connection-types.d.ts +21 -1
- package/dist/connection.d.ts +12 -3
- package/dist/connection.js +39 -3
- package/dist/flow-types.d.ts +2 -1
- package/dist/flow.js +12 -3
- package/dist/index.d.ts +7 -2
- package/dist/index.js +3 -1
- package/dist/job.d.ts +2 -2
- package/dist/observability.d.ts +95 -0
- package/dist/observability.js +110 -0
- package/dist/queue-admin.js +16 -2
- package/dist/queue-control.d.ts +5 -0
- package/dist/queue-control.js +27 -9
- package/dist/queue-query.js +13 -21
- package/dist/queue.d.ts +13 -4
- package/dist/queue.js +14 -7
- package/dist/responses.d.ts +79 -0
- package/dist/responses.js +10 -0
- package/dist/worker-base.d.ts +15 -3
- package/dist/worker-base.js +15 -7
- package/dist/worker-types.d.ts +43 -1
- package/dist/worker.d.ts +5 -1
- package/dist/worker.js +59 -14
- package/package.json +3 -1
- package/src/ack-batcher.ts +76 -0
- package/src/backpressure.ts +40 -0
- package/src/bunqueue/bunqueue.ts +12 -1
- package/src/connection-pool.ts +71 -0
- package/src/connection-types.ts +23 -1
- package/src/connection.ts +42 -6
- package/src/flow-types.ts +2 -1
- package/src/flow.ts +24 -16
- package/src/index.ts +33 -2
- package/src/job.ts +3 -3
- package/src/observability.ts +158 -0
- package/src/queue-admin.ts +15 -2
- package/src/queue-control.ts +34 -11
- package/src/queue-query.ts +39 -29
- package/src/queue.ts +30 -11
- package/src/responses.ts +96 -0
- package/src/worker-base.ts +49 -11
- package/src/worker-types.ts +42 -1
- package/src/worker.ts +63 -16
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability primitives: a pluggable structured logger and a telemetry
|
|
3
|
+
* sink. Zero hard dependencies — wire your own OpenTelemetry / Prometheus /
|
|
4
|
+
* logging stack through these interfaces. Defaults are no-ops, so the SDK is
|
|
5
|
+
* silent unless you opt in.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
9
|
+
|
|
10
|
+
export interface Logger {
|
|
11
|
+
debug(message: string, meta?: Record<string, unknown>): void;
|
|
12
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
13
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
14
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Silent logger (the default). */
|
|
18
|
+
export const noopLogger: Logger = {
|
|
19
|
+
debug() {},
|
|
20
|
+
info() {},
|
|
21
|
+
warn() {},
|
|
22
|
+
error() {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const LEVELS: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A console-backed logger. Levels below `min` are dropped. Opt-in — pass it as
|
|
29
|
+
* `logger` when you want the SDK to log to the console.
|
|
30
|
+
*/
|
|
31
|
+
export function consoleLogger(min: LogLevel = 'info'): Logger {
|
|
32
|
+
const floor = LEVELS[min];
|
|
33
|
+
const line = (lvl: LogLevel, msg: string, meta?: Record<string, unknown>) => {
|
|
34
|
+
if (LEVELS[lvl] < floor) return;
|
|
35
|
+
const suffix = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
|
|
36
|
+
const sink = lvl === 'debug' ? console.log : console[lvl];
|
|
37
|
+
sink(`[bunqueue] ${lvl} ${msg}${suffix}`);
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
debug: (m, meta) => line('debug', m, meta),
|
|
41
|
+
info: (m, meta) => line('info', m, meta),
|
|
42
|
+
warn: (m, meta) => line('warn', m, meta),
|
|
43
|
+
error: (m, meta) => line('error', m, meta),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A single telemetry data point. A discriminated union keyed on `type`. */
|
|
48
|
+
export type TelemetryEvent =
|
|
49
|
+
| { type: 'command'; cmd: string; reqId: string; durationMs: number; ok: boolean }
|
|
50
|
+
| { type: 'command_timeout'; cmd: string; reqId: string }
|
|
51
|
+
| { type: 'connect'; host: string; port: number; generation: number; durationMs: number }
|
|
52
|
+
| { type: 'disconnect'; host: string; port: number; generation: number }
|
|
53
|
+
| { type: 'reconnect_scheduled'; host: string; port: number; attempt: number; delayMs: number }
|
|
54
|
+
| { type: 'auth'; ok: boolean }
|
|
55
|
+
| { type: 'backpressure'; inFlight: number; maxInFlight: number };
|
|
56
|
+
|
|
57
|
+
export type TelemetryHandler = (event: TelemetryEvent) => void;
|
|
58
|
+
|
|
59
|
+
/** Lifecycle telemetry types that are ALSO emitted as EventEmitter events. */
|
|
60
|
+
export const LIFECYCLE_EVENTS = ['connect', 'disconnect', 'reconnect_scheduled'] as const;
|
|
61
|
+
|
|
62
|
+
export interface Observability {
|
|
63
|
+
/** Structured logger; defaults to {@link noopLogger}. */
|
|
64
|
+
logger?: Logger;
|
|
65
|
+
/**
|
|
66
|
+
* Telemetry sink for metrics and tracing. Receives every command's latency,
|
|
67
|
+
* connect/disconnect/reconnect, auth and backpressure events. Bridge it to
|
|
68
|
+
* OpenTelemetry spans or Prometheus counters without the SDK depending on
|
|
69
|
+
* either.
|
|
70
|
+
*/
|
|
71
|
+
onTelemetry?: TelemetryHandler;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** High-resolution monotonic clock (Node/Bun/Deno all expose `performance`). */
|
|
75
|
+
export const nowMs = (): number => performance.now();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fans a telemetry event out to (1) the telemetry sink, (2) the logger, and
|
|
79
|
+
* (3) — for lifecycle events — the owning EventEmitter, so users can both
|
|
80
|
+
* scrape metrics and attach imperative `connection.on('connect', …)` handlers.
|
|
81
|
+
*/
|
|
82
|
+
export class Telemetry {
|
|
83
|
+
private readonly logger: Logger;
|
|
84
|
+
private readonly sink: TelemetryHandler | undefined;
|
|
85
|
+
private readonly emit: (event: string, payload: unknown) => void;
|
|
86
|
+
|
|
87
|
+
constructor(obs: Observability | undefined, emit: (event: string, payload: unknown) => void) {
|
|
88
|
+
// Wrap the user logger so a throwing logger can never break the transport
|
|
89
|
+
// hot path (dispatch runs inside socket 'data' handlers and connect).
|
|
90
|
+
const inner = obs?.logger ?? noopLogger;
|
|
91
|
+
this.logger =
|
|
92
|
+
inner === noopLogger
|
|
93
|
+
? inner
|
|
94
|
+
: {
|
|
95
|
+
debug: (m, meta) => Telemetry.safely(() => inner.debug(m, meta)),
|
|
96
|
+
info: (m, meta) => Telemetry.safely(() => inner.info(m, meta)),
|
|
97
|
+
warn: (m, meta) => Telemetry.safely(() => inner.warn(m, meta)),
|
|
98
|
+
error: (m, meta) => Telemetry.safely(() => inner.error(m, meta)),
|
|
99
|
+
};
|
|
100
|
+
this.sink = obs?.onTelemetry;
|
|
101
|
+
this.emit = emit;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Observability must never break the client: swallow consumer errors. */
|
|
105
|
+
private static safely(fn: () => void): void {
|
|
106
|
+
try {
|
|
107
|
+
fn();
|
|
108
|
+
} catch {
|
|
109
|
+
/* consumer logging/telemetry error — intentionally ignored */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get log(): Logger {
|
|
114
|
+
return this.logger;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Record a completed command's latency and outcome. */
|
|
118
|
+
command(cmd: string, reqId: string, startMs: number, ok: boolean): void {
|
|
119
|
+
this.dispatch({ type: 'command', cmd, reqId, durationMs: nowMs() - startMs, ok });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
timeout(cmd: string, reqId: string): void {
|
|
123
|
+
this.logger.warn('command timed out', { cmd, reqId });
|
|
124
|
+
this.dispatch({ type: 'command_timeout', cmd, reqId });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
connected(host: string, port: number, generation: number, startMs: number): void {
|
|
128
|
+
this.logger.info('connected', { host, port, generation });
|
|
129
|
+
this.dispatch({ type: 'connect', host, port, generation, durationMs: nowMs() - startMs });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
disconnected(host: string, port: number, generation: number): void {
|
|
133
|
+
this.logger.info('disconnected', { host, port, generation });
|
|
134
|
+
this.dispatch({ type: 'disconnect', host, port, generation });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
reconnectScheduled(host: string, port: number, attempt: number, delayMs: number): void {
|
|
138
|
+
this.logger.warn('reconnect scheduled', { host, port, attempt, delayMs });
|
|
139
|
+
this.dispatch({ type: 'reconnect_scheduled', host, port, attempt, delayMs });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
auth(ok: boolean): void {
|
|
143
|
+
this.dispatch({ type: 'auth', ok });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
backpressure(inFlight: number, maxInFlight: number): void {
|
|
147
|
+
this.dispatch({ type: 'backpressure', inFlight, maxInFlight });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private dispatch(event: TelemetryEvent): void {
|
|
151
|
+
// A throwing sink or lifecycle listener must never break the transport:
|
|
152
|
+
// dispatch runs inside socket 'data' handlers and the connect path.
|
|
153
|
+
Telemetry.safely(() => this.sink?.(event));
|
|
154
|
+
if ((LIFECYCLE_EVENTS as readonly string[]).includes(event.type)) {
|
|
155
|
+
Telemetry.safely(() => this.emit(event.type, event));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/queue-admin.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* monitoring and webhooks. Merged onto Queue.prototype by queue.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { CommandError } from './errors.js';
|
|
6
7
|
import { compact } from './frame.js';
|
|
7
8
|
import type { Queue } from './queue.js';
|
|
8
9
|
import { type JobOptions, jobPayload, wireJobOptions } from './types.js';
|
|
@@ -95,6 +96,10 @@ export const adminMethods = {
|
|
|
95
96
|
repeat: SchedulerOptions,
|
|
96
97
|
template: { name?: string; data?: unknown; opts?: JobOptions } = {}
|
|
97
98
|
): Promise<void> {
|
|
99
|
+
// Priority and deduplication of spawned jobs travel as TOP-LEVEL Cron
|
|
100
|
+
// fields (the handler reads cmd.priority/uniqueKey/dedup); inside
|
|
101
|
+
// jobOptions the server's CronJobOptions silently ignores them.
|
|
102
|
+
const dedup = template.opts?.deduplication;
|
|
98
103
|
await this.call(
|
|
99
104
|
compact({
|
|
100
105
|
cmd: 'Cron',
|
|
@@ -103,9 +108,14 @@ export const adminMethods = {
|
|
|
103
108
|
data: jobPayload(template.name ?? schedulerId, template.data ?? {}),
|
|
104
109
|
schedule: repeat.pattern,
|
|
105
110
|
repeatEvery: repeat.every,
|
|
111
|
+
priority: template.opts?.priority,
|
|
106
112
|
timezone: repeat.tz,
|
|
107
113
|
immediately: repeat.immediately,
|
|
108
114
|
maxLimit: repeat.limit,
|
|
115
|
+
uniqueKey: dedup?.id,
|
|
116
|
+
dedup: dedup
|
|
117
|
+
? compact({ ttl: dedup.ttl, extend: dedup.extend, replace: dedup.replace })
|
|
118
|
+
: undefined,
|
|
109
119
|
skipMissedOnRestart: repeat.skipMissedOnRestart,
|
|
110
120
|
skipIfNoWorker: repeat.skipIfNoWorker,
|
|
111
121
|
preventOverlap: repeat.preventOverlap,
|
|
@@ -122,8 +132,11 @@ export const adminMethods = {
|
|
|
122
132
|
try {
|
|
123
133
|
const response = await this.call({ cmd: 'CronGet', name: schedulerId });
|
|
124
134
|
return (response.cron ?? response.data ?? null) as Raw | null;
|
|
125
|
-
} catch {
|
|
126
|
-
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// Only 'Cron job not found' maps to null; connection loss, timeouts and
|
|
137
|
+
// real server errors must surface, not masquerade as a missing scheduler.
|
|
138
|
+
if (err instanceof CommandError && /not found/i.test(err.message)) return null;
|
|
139
|
+
throw err;
|
|
127
140
|
}
|
|
128
141
|
},
|
|
129
142
|
|
package/src/queue-control.ts
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
* mutations. Methods are merged onto Queue.prototype by queue.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { UnrecoverableError } from './errors.js';
|
|
6
7
|
import { compact } from './frame.js';
|
|
7
8
|
import type { Queue } from './queue.js';
|
|
9
|
+
import type { CountResponse, PausedResponse } from './responses.js';
|
|
8
10
|
import type { JobStateName } from './types.js';
|
|
11
|
+
import { MAX_STACK_LINES } from './worker-types.js';
|
|
9
12
|
|
|
10
13
|
type Ctx = Queue<unknown>;
|
|
11
14
|
|
|
@@ -19,8 +22,7 @@ export const controlMethods = {
|
|
|
19
22
|
},
|
|
20
23
|
|
|
21
24
|
async isPaused(this: Ctx): Promise<boolean> {
|
|
22
|
-
|
|
23
|
-
return response.paused === true;
|
|
25
|
+
return (await this.call<PausedResponse>({ cmd: 'IsPaused', queue: this.name })).paused === true;
|
|
24
26
|
},
|
|
25
27
|
|
|
26
28
|
async drain(this: Ctx): Promise<void> {
|
|
@@ -33,10 +35,10 @@ export const controlMethods = {
|
|
|
33
35
|
|
|
34
36
|
/** Remove old jobs; returns how many were removed. */
|
|
35
37
|
async clean(this: Ctx, graceMs: number, limit?: number, state?: JobStateName): Promise<number> {
|
|
36
|
-
const response = await this.call(
|
|
38
|
+
const response = await this.call<CountResponse>(
|
|
37
39
|
compact({ cmd: 'Clean', queue: this.name, grace: graceMs, limit, state }) as { cmd: string }
|
|
38
40
|
);
|
|
39
|
-
return
|
|
41
|
+
return response.count ?? 0;
|
|
40
42
|
},
|
|
41
43
|
|
|
42
44
|
async remove(this: Ctx, id: string): Promise<void> {
|
|
@@ -53,10 +55,10 @@ export const controlMethods = {
|
|
|
53
55
|
|
|
54
56
|
/** Promote delayed jobs to waiting; returns how many were promoted. */
|
|
55
57
|
async promoteJobs(this: Ctx, opts: { count?: number } = {}): Promise<number> {
|
|
56
|
-
const response = await this.call(
|
|
58
|
+
const response = await this.call<CountResponse>(
|
|
57
59
|
compact({ cmd: 'PromoteJobs', queue: this.name, count: opts.count }) as { cmd: string }
|
|
58
60
|
);
|
|
59
|
-
return
|
|
61
|
+
return response.count ?? 0;
|
|
60
62
|
},
|
|
61
63
|
|
|
62
64
|
/** BullMQ contract: failed → waiting. */
|
|
@@ -72,9 +74,11 @@ export const controlMethods = {
|
|
|
72
74
|
await this.call({ cmd: 'RetryCompleted', queue: this.name });
|
|
73
75
|
return;
|
|
74
76
|
}
|
|
75
|
-
// `count`
|
|
76
|
-
//
|
|
77
|
-
await this.call(
|
|
77
|
+
// `count` caps how many DLQ entries are retried (server >= 2.8.29). Older
|
|
78
|
+
// servers ignore the field and retry the whole DLQ — forward-compatible.
|
|
79
|
+
await this.call(
|
|
80
|
+
compact({ cmd: 'RetryDlq', queue: this.name, count: opts.count }) as { cmd: string }
|
|
81
|
+
);
|
|
78
82
|
},
|
|
79
83
|
|
|
80
84
|
async retryCompleted(this: Ctx, id?: string): Promise<void> {
|
|
@@ -129,14 +133,33 @@ export const controlMethods = {
|
|
|
129
133
|
await this.call(compact({ cmd: 'ACK', id, result: returnValue, token }) as { cmd: string });
|
|
130
134
|
},
|
|
131
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Explicit failure path: mirrors the worker's FAIL wire so the stacktrace
|
|
138
|
+
* and the UnrecoverableError "do not retry" intent are persisted (#111
|
|
139
|
+
* silent-loss class), not just the message.
|
|
140
|
+
*/
|
|
132
141
|
async moveJobToFailed(
|
|
133
142
|
this: Ctx,
|
|
134
143
|
id: string,
|
|
135
144
|
error: Error | string,
|
|
136
145
|
token?: string
|
|
137
146
|
): Promise<void> {
|
|
138
|
-
const
|
|
139
|
-
|
|
147
|
+
const err = typeof error === 'string' ? undefined : error;
|
|
148
|
+
const message = err ? err.message || err.name : (error as string);
|
|
149
|
+
// Keep the FIRST lines (message + throw site), like the worker path.
|
|
150
|
+
const stack = err
|
|
151
|
+
? (err.stack ?? err.message).split('\n').slice(0, MAX_STACK_LINES)
|
|
152
|
+
: undefined;
|
|
153
|
+
await this.call(
|
|
154
|
+
compact({
|
|
155
|
+
cmd: 'FAIL',
|
|
156
|
+
id,
|
|
157
|
+
error: message,
|
|
158
|
+
stack,
|
|
159
|
+
unrecoverable: err instanceof UnrecoverableError ? true : undefined,
|
|
160
|
+
token,
|
|
161
|
+
}) as { cmd: string }
|
|
162
|
+
);
|
|
140
163
|
},
|
|
141
164
|
};
|
|
142
165
|
|
package/src/queue-query.ts
CHANGED
|
@@ -7,12 +7,23 @@ import { CommandError, CommandTimeoutError } from './errors.js';
|
|
|
7
7
|
import { compact } from './frame.js';
|
|
8
8
|
import { Job } from './job.js';
|
|
9
9
|
import type { Queue } from './queue.js';
|
|
10
|
+
import type {
|
|
11
|
+
CountResponse,
|
|
12
|
+
DataResponse,
|
|
13
|
+
JobCountsResponse,
|
|
14
|
+
JobResponse,
|
|
15
|
+
JobsResponse,
|
|
16
|
+
ProgressResponse,
|
|
17
|
+
ResultResponse,
|
|
18
|
+
StateResponse,
|
|
19
|
+
WaitJobResponse,
|
|
20
|
+
} from './responses.js';
|
|
10
21
|
import type { JobCounts } from './types.js';
|
|
11
22
|
|
|
12
23
|
type Ctx = Queue<unknown>;
|
|
13
24
|
type Raw = Record<string, unknown>;
|
|
14
25
|
|
|
15
|
-
function unwrapValues<R>(response:
|
|
26
|
+
function unwrapValues<R>(response: { data?: unknown; values?: unknown }): Record<string, R> {
|
|
16
27
|
const data = (response.data ?? {}) as Raw;
|
|
17
28
|
return (data.values ?? response.values ?? data ?? {}) as Record<string, R>;
|
|
18
29
|
}
|
|
@@ -20,22 +31,22 @@ function unwrapValues<R>(response: Raw): Record<string, R> {
|
|
|
20
31
|
export const queryMethods = {
|
|
21
32
|
async getJob<T = unknown>(this: Ctx, id: string): Promise<Job<T> | null> {
|
|
22
33
|
try {
|
|
23
|
-
const response = await this.call({ cmd: 'GetJob', id });
|
|
24
|
-
|
|
25
|
-
return raw ? new Job<T>(raw, this.connection) : null;
|
|
34
|
+
const response = await this.call<JobResponse>({ cmd: 'GetJob', id });
|
|
35
|
+
return response.job ? new Job<T>(response.job, this.connection) : null;
|
|
26
36
|
} catch (err) {
|
|
27
|
-
|
|
37
|
+
// Only the server's 'Job not found' maps to null; connection loss,
|
|
38
|
+
// timeouts and other server errors must surface.
|
|
39
|
+
if (err instanceof CommandError && /not found/i.test(err.message)) return null;
|
|
28
40
|
throw err;
|
|
29
41
|
}
|
|
30
42
|
},
|
|
31
43
|
|
|
32
44
|
async getJobByCustomId<T = unknown>(this: Ctx, customId: string): Promise<Job<T> | null> {
|
|
33
45
|
try {
|
|
34
|
-
const response = await this.call({ cmd: 'GetJobByCustomId', customId });
|
|
35
|
-
|
|
36
|
-
return raw ? new Job<T>(raw, this.connection) : null;
|
|
46
|
+
const response = await this.call<JobResponse>({ cmd: 'GetJobByCustomId', customId });
|
|
47
|
+
return response.job ? new Job<T>(response.job, this.connection) : null;
|
|
37
48
|
} catch (err) {
|
|
38
|
-
if (err instanceof CommandError) return null;
|
|
49
|
+
if (err instanceof CommandError && /not found/i.test(err.message)) return null;
|
|
39
50
|
throw err;
|
|
40
51
|
}
|
|
41
52
|
},
|
|
@@ -52,7 +63,7 @@ export const queryMethods = {
|
|
|
52
63
|
): Promise<Job<T>[]> {
|
|
53
64
|
const start = opts.start ?? 0;
|
|
54
65
|
const end = opts.end !== undefined && opts.end >= 0 ? opts.end : 1000;
|
|
55
|
-
const response = await this.call(
|
|
66
|
+
const response = await this.call<JobsResponse>(
|
|
56
67
|
compact({
|
|
57
68
|
cmd: 'GetJobs',
|
|
58
69
|
queue: this.name,
|
|
@@ -61,8 +72,7 @@ export const queryMethods = {
|
|
|
61
72
|
limit: end - start,
|
|
62
73
|
}) as { cmd: string }
|
|
63
74
|
);
|
|
64
|
-
|
|
65
|
-
return jobs.map((raw) => new Job<T>(raw, this.connection));
|
|
75
|
+
return (response.jobs ?? []).map((raw) => new Job<T>(raw, this.connection));
|
|
66
76
|
},
|
|
67
77
|
|
|
68
78
|
getWaiting<T = unknown>(this: Ctx, start?: number, end?: number): Promise<Job<T>[]> {
|
|
@@ -96,25 +106,27 @@ export const queryMethods = {
|
|
|
96
106
|
},
|
|
97
107
|
|
|
98
108
|
async getJobState(this: Ctx, id: string): Promise<string> {
|
|
99
|
-
|
|
100
|
-
return String(response.state);
|
|
109
|
+
return (await this.call<StateResponse>({ cmd: 'GetState', id })).state;
|
|
101
110
|
},
|
|
102
111
|
|
|
103
112
|
async getResult<R = unknown>(this: Ctx, id: string): Promise<R> {
|
|
104
|
-
|
|
105
|
-
return response.result as R;
|
|
113
|
+
return (await this.call<ResultResponse<R>>({ cmd: 'GetResult', id })).result;
|
|
106
114
|
},
|
|
107
115
|
|
|
108
116
|
async getChildrenValues<R = unknown>(this: Ctx, id: string): Promise<Record<string, R>> {
|
|
109
|
-
return unwrapValues<R>(await this.call({ cmd: 'GetChildrenValues', id }));
|
|
117
|
+
return unwrapValues<R>(await this.call<DataResponse>({ cmd: 'GetChildrenValues', id }));
|
|
110
118
|
},
|
|
111
119
|
|
|
112
120
|
async getFailedChildrenValues(this: Ctx, id: string): Promise<Record<string, string>> {
|
|
113
|
-
return unwrapValues<string>(
|
|
121
|
+
return unwrapValues<string>(
|
|
122
|
+
await this.call<DataResponse>({ cmd: 'GetFailedChildrenValues', id })
|
|
123
|
+
);
|
|
114
124
|
},
|
|
115
125
|
|
|
116
126
|
async getIgnoredChildrenFailures(this: Ctx, id: string): Promise<Record<string, string>> {
|
|
117
|
-
return unwrapValues<string>(
|
|
127
|
+
return unwrapValues<string>(
|
|
128
|
+
await this.call<DataResponse>({ cmd: 'GetIgnoredChildrenFailures', id })
|
|
129
|
+
);
|
|
118
130
|
},
|
|
119
131
|
|
|
120
132
|
/** Detach a child job from its parent's dependency list. */
|
|
@@ -136,7 +148,10 @@ export const queryMethods = {
|
|
|
136
148
|
* will not complete), everything else throws CommandTimeoutError.
|
|
137
149
|
*/
|
|
138
150
|
async waitForJob<R = unknown>(this: Ctx, id: string, ttlMs = 30_000): Promise<R> {
|
|
139
|
-
const response = await this.call(
|
|
151
|
+
const response = await this.call<WaitJobResponse<R>>(
|
|
152
|
+
{ cmd: 'WaitJob', id, timeout: ttlMs },
|
|
153
|
+
ttlMs + 5000
|
|
154
|
+
);
|
|
140
155
|
if (response.completed !== true) {
|
|
141
156
|
let state: string | undefined;
|
|
142
157
|
try {
|
|
@@ -161,18 +176,14 @@ export const queryMethods = {
|
|
|
161
176
|
},
|
|
162
177
|
|
|
163
178
|
async getProgress(this: Ctx, id: string): Promise<{ progress: number; message: string | null }> {
|
|
164
|
-
const response = await this.call({ cmd: 'GetProgress', id });
|
|
165
|
-
return {
|
|
166
|
-
progress: Number(response.progress ?? 0),
|
|
167
|
-
message: (response.message as string | null) ?? null,
|
|
168
|
-
};
|
|
179
|
+
const response = await this.call<ProgressResponse>({ cmd: 'GetProgress', id });
|
|
180
|
+
return { progress: response.progress ?? 0, message: response.message ?? null };
|
|
169
181
|
},
|
|
170
182
|
|
|
171
183
|
// ------------------------------------------------------------------- counts
|
|
172
184
|
|
|
173
185
|
async getJobCounts(this: Ctx): Promise<JobCounts> {
|
|
174
|
-
|
|
175
|
-
return response.counts as JobCounts;
|
|
186
|
+
return (await this.call<JobCountsResponse>({ cmd: 'GetJobCounts', queue: this.name })).counts;
|
|
176
187
|
},
|
|
177
188
|
|
|
178
189
|
async getWaitingCount(this: Ctx): Promise<number> {
|
|
@@ -206,8 +217,7 @@ export const queryMethods = {
|
|
|
206
217
|
},
|
|
207
218
|
|
|
208
219
|
async count(this: Ctx): Promise<number> {
|
|
209
|
-
|
|
210
|
-
return Number(response.count ?? 0);
|
|
220
|
+
return (await this.call<CountResponse>({ cmd: 'Count', queue: this.name })).count ?? 0;
|
|
211
221
|
},
|
|
212
222
|
|
|
213
223
|
async getCountsPerPriority(this: Ctx): Promise<Record<string, number>> {
|
package/src/queue.ts
CHANGED
|
@@ -10,19 +10,29 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { Connection, type Response, type TlsOption } from './connection.js';
|
|
13
|
+
import { ConnectionPool } from './connection-pool.js';
|
|
14
|
+
import type { ConnectionLike } from './connection-types.js';
|
|
13
15
|
import { Job } from './job.js';
|
|
16
|
+
import type { Observability } from './observability.js';
|
|
14
17
|
import { adminMethods, type QueueAdminApi } from './queue-admin.js';
|
|
15
18
|
import { controlMethods, type QueueControlApi } from './queue-control.js';
|
|
16
19
|
import { type QueueQueryApi, queryMethods } from './queue-query.js';
|
|
17
20
|
import { type JobOptions, jobPayload, wireJobOptions } from './types.js';
|
|
18
21
|
|
|
19
|
-
export interface QueueOptions {
|
|
22
|
+
export interface QueueOptions extends Observability {
|
|
20
23
|
host?: string;
|
|
21
24
|
port?: number;
|
|
22
25
|
token?: string;
|
|
23
26
|
tls?: TlsOption;
|
|
24
27
|
connection?: Connection;
|
|
25
28
|
commandTimeoutMs?: number;
|
|
29
|
+
/** Max in-flight commands before backpressure kicks in (0 = unbounded). */
|
|
30
|
+
maxInFlight?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Fan producer commands across N connections (round-robin) for throughput.
|
|
33
|
+
* >1 builds a ConnectionPool; default 1 = a single connection.
|
|
34
|
+
*/
|
|
35
|
+
poolSize?: number;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
export interface BulkJobEntry<T = unknown> {
|
|
@@ -34,26 +44,35 @@ export interface BulkJobEntry<T = unknown> {
|
|
|
34
44
|
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: prototype-mixin composition — Object.assign at the bottom installs exactly the methods the merged interface declares
|
|
35
45
|
export class Queue<T = unknown> {
|
|
36
46
|
readonly name: string;
|
|
37
|
-
readonly connection:
|
|
47
|
+
readonly connection: ConnectionLike;
|
|
38
48
|
private readonly ownsConnection: boolean;
|
|
39
49
|
|
|
40
50
|
constructor(name: string, opts: QueueOptions = {}) {
|
|
41
51
|
this.name = name;
|
|
52
|
+
const connOptions = {
|
|
53
|
+
host: opts.host,
|
|
54
|
+
port: opts.port,
|
|
55
|
+
token: opts.token,
|
|
56
|
+
tls: opts.tls,
|
|
57
|
+
commandTimeoutMs: opts.commandTimeoutMs,
|
|
58
|
+
maxInFlight: opts.maxInFlight,
|
|
59
|
+
logger: opts.logger,
|
|
60
|
+
onTelemetry: opts.onTelemetry,
|
|
61
|
+
};
|
|
42
62
|
this.connection =
|
|
43
63
|
opts.connection ??
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
token: opts.token,
|
|
48
|
-
tls: opts.tls,
|
|
49
|
-
commandTimeoutMs: opts.commandTimeoutMs,
|
|
50
|
-
});
|
|
64
|
+
(opts.poolSize && opts.poolSize > 1
|
|
65
|
+
? new ConnectionPool(opts.poolSize, connOptions)
|
|
66
|
+
: new Connection(connOptions));
|
|
51
67
|
this.ownsConnection = opts.connection === undefined;
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
/** Send a raw command on this queue's connection (used by area modules). */
|
|
55
|
-
call
|
|
56
|
-
|
|
71
|
+
call<R = Response>(
|
|
72
|
+
command: Record<string, unknown> & { cmd: string },
|
|
73
|
+
timeoutMs?: number
|
|
74
|
+
): Promise<R> {
|
|
75
|
+
return this.connection.call<R>(command, timeoutMs);
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
// ------------------------------------------------------------------ produce
|
package/src/responses.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed server-response shapes, mirroring the server's response builders
|
|
3
|
+
* (src/domain/types/response.ts). `Connection.call<R>()` and `Queue.call<R>()`
|
|
4
|
+
* are generic over these, so call sites read `response.job` / `response.counts`
|
|
5
|
+
* with real types instead of casting `as Record<string, unknown>`.
|
|
6
|
+
*
|
|
7
|
+
* These narrow the loose transport `Response`; the runtime value is whatever
|
|
8
|
+
* the server sent, so only assert the shape a given command actually returns.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { JobRaw } from './job.js';
|
|
12
|
+
import type { JobCounts, JobStateName } from './types.js';
|
|
13
|
+
|
|
14
|
+
interface Ok {
|
|
15
|
+
ok: true;
|
|
16
|
+
reqId?: string;
|
|
17
|
+
// Index signature so these satisfy the loose transport `Response`
|
|
18
|
+
// (Record<string, unknown> & { ok }). Named fields still win for reads.
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** PUSH → id, or a bare ok ack. */
|
|
23
|
+
export interface OkResponse extends Ok {
|
|
24
|
+
id?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** PUSHB → ids. */
|
|
28
|
+
export interface BatchResponse extends Ok {
|
|
29
|
+
ids: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** GetJob → job (null when not found is surfaced as a CommandError instead). */
|
|
33
|
+
export interface JobResponse extends Ok {
|
|
34
|
+
job: JobRaw | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** PULL → job + lock token. */
|
|
38
|
+
export interface PulledJobResponse extends Ok {
|
|
39
|
+
job: JobRaw | null;
|
|
40
|
+
token: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** PULLB → jobs + lock tokens (same order). */
|
|
44
|
+
export interface PulledJobsResponse extends Ok {
|
|
45
|
+
jobs: JobRaw[];
|
|
46
|
+
tokens: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** GetJobs → jobs. */
|
|
50
|
+
export interface JobsResponse extends Ok {
|
|
51
|
+
jobs: JobRaw[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** GetState → state. */
|
|
55
|
+
export interface StateResponse extends Ok {
|
|
56
|
+
id: string;
|
|
57
|
+
state: JobStateName;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** GetResult → result. */
|
|
61
|
+
export interface ResultResponse<R = unknown> extends Ok {
|
|
62
|
+
result: R;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** WaitJob → completed flag + (on completion) result. */
|
|
66
|
+
export interface WaitJobResponse<R = unknown> extends Ok {
|
|
67
|
+
completed: boolean;
|
|
68
|
+
result?: R;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** GetJobCounts → counts. */
|
|
72
|
+
export interface JobCountsResponse extends Ok {
|
|
73
|
+
counts: JobCounts;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** GetProgress → progress + message. */
|
|
77
|
+
export interface ProgressResponse extends Ok {
|
|
78
|
+
progress: number;
|
|
79
|
+
message: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** IsPaused → paused. */
|
|
83
|
+
export interface PausedResponse extends Ok {
|
|
84
|
+
paused: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Count / Clean / PromoteJobs / RetryDlq → count (+ optional removed ids). */
|
|
88
|
+
export interface CountResponse extends Ok {
|
|
89
|
+
count: number;
|
|
90
|
+
ids?: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Generic data-wrapped payload (logs, workers, values, webhookId, …). */
|
|
94
|
+
export interface DataResponse<T = unknown> extends Ok {
|
|
95
|
+
data: T;
|
|
96
|
+
}
|