bunqueue-client 0.1.5 → 0.1.6

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +43 -0
  3. package/dist/ack-batcher.d.ts +28 -0
  4. package/dist/ack-batcher.js +67 -0
  5. package/dist/backpressure.d.ts +22 -0
  6. package/dist/backpressure.js +39 -0
  7. package/dist/connection-pool.d.ts +30 -0
  8. package/dist/connection-pool.js +60 -0
  9. package/dist/connection-types.d.ts +21 -1
  10. package/dist/connection.d.ts +12 -3
  11. package/dist/connection.js +39 -3
  12. package/dist/flow-types.d.ts +2 -1
  13. package/dist/flow.js +12 -3
  14. package/dist/index.d.ts +7 -2
  15. package/dist/index.js +3 -1
  16. package/dist/job.d.ts +2 -2
  17. package/dist/observability.d.ts +95 -0
  18. package/dist/observability.js +110 -0
  19. package/dist/queue-control.js +3 -4
  20. package/dist/queue-query.js +8 -18
  21. package/dist/queue.d.ts +13 -4
  22. package/dist/queue.js +14 -7
  23. package/dist/responses.d.ts +79 -0
  24. package/dist/responses.js +10 -0
  25. package/dist/worker-base.d.ts +2 -0
  26. package/dist/worker-base.js +5 -0
  27. package/dist/worker-types.d.ts +15 -1
  28. package/dist/worker.d.ts +4 -0
  29. package/dist/worker.js +43 -6
  30. package/package.json +2 -1
  31. package/src/ack-batcher.ts +76 -0
  32. package/src/backpressure.ts +40 -0
  33. package/src/connection-pool.ts +71 -0
  34. package/src/connection-types.ts +23 -1
  35. package/src/connection.ts +42 -6
  36. package/src/flow-types.ts +2 -1
  37. package/src/flow.ts +24 -16
  38. package/src/index.ts +28 -2
  39. package/src/job.ts +3 -3
  40. package/src/observability.ts +158 -0
  41. package/src/queue-control.ts +6 -6
  42. package/src/queue-query.ts +35 -27
  43. package/src/queue.ts +30 -11
  44. package/src/responses.ts +96 -0
  45. package/src/worker-base.ts +6 -0
  46. package/src/worker-types.ts +16 -1
  47. package/src/worker.ts +45 -6
package/src/job.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Mirrors the official TS client's Job surface (TCP mode).
4
4
  */
5
5
 
6
- import type { Connection, Response } from './connection.js';
6
+ import type { ConnectionLike, Response } from './connection-types.js';
7
7
 
8
8
  export type JobRaw = Record<string, unknown>;
9
9
 
@@ -12,10 +12,10 @@ export type ProgressHook = (job: Job, progress: number) => void;
12
12
  export class Job<T = unknown> {
13
13
  readonly raw: JobRaw;
14
14
  readonly token: string | undefined;
15
- private readonly conn: Connection | undefined;
15
+ private readonly conn: ConnectionLike | undefined;
16
16
  private readonly onProgress: ProgressHook | undefined;
17
17
 
18
- constructor(raw: JobRaw, connection?: Connection, token?: string, onProgress?: ProgressHook) {
18
+ constructor(raw: JobRaw, connection?: ConnectionLike, token?: string, onProgress?: ProgressHook) {
19
19
  this.raw = raw;
20
20
  this.conn = connection;
21
21
  this.token = token;
@@ -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
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { compact } from './frame.js';
7
7
  import type { Queue } from './queue.js';
8
+ import type { CountResponse, PausedResponse } from './responses.js';
8
9
  import type { JobStateName } from './types.js';
9
10
 
10
11
  type Ctx = Queue<unknown>;
@@ -19,8 +20,7 @@ export const controlMethods = {
19
20
  },
20
21
 
21
22
  async isPaused(this: Ctx): Promise<boolean> {
22
- const response = await this.call({ cmd: 'IsPaused', queue: this.name });
23
- return response.paused === true;
23
+ return (await this.call<PausedResponse>({ cmd: 'IsPaused', queue: this.name })).paused === true;
24
24
  },
25
25
 
26
26
  async drain(this: Ctx): Promise<void> {
@@ -33,10 +33,10 @@ export const controlMethods = {
33
33
 
34
34
  /** Remove old jobs; returns how many were removed. */
35
35
  async clean(this: Ctx, graceMs: number, limit?: number, state?: JobStateName): Promise<number> {
36
- const response = await this.call(
36
+ const response = await this.call<CountResponse>(
37
37
  compact({ cmd: 'Clean', queue: this.name, grace: graceMs, limit, state }) as { cmd: string }
38
38
  );
39
- return Number(response.count ?? 0);
39
+ return response.count ?? 0;
40
40
  },
41
41
 
42
42
  async remove(this: Ctx, id: string): Promise<void> {
@@ -53,10 +53,10 @@ export const controlMethods = {
53
53
 
54
54
  /** Promote delayed jobs to waiting; returns how many were promoted. */
55
55
  async promoteJobs(this: Ctx, opts: { count?: number } = {}): Promise<number> {
56
- const response = await this.call(
56
+ const response = await this.call<CountResponse>(
57
57
  compact({ cmd: 'PromoteJobs', queue: this.name, count: opts.count }) as { cmd: string }
58
58
  );
59
- return Number(response.count ?? 0);
59
+ return response.count ?? 0;
60
60
  },
61
61
 
62
62
  /** BullMQ contract: failed → waiting. */
@@ -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: Raw): Record<string, R> {
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,9 +31,8 @@ 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
- const raw = response.job as Raw | null;
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
  if (err instanceof CommandError) return null; // server: 'Job not found'
28
38
  throw err;
@@ -31,9 +41,8 @@ export const queryMethods = {
31
41
 
32
42
  async getJobByCustomId<T = unknown>(this: Ctx, customId: string): Promise<Job<T> | null> {
33
43
  try {
34
- const response = await this.call({ cmd: 'GetJobByCustomId', customId });
35
- const raw = response.job as Raw | null;
36
- return raw ? new Job<T>(raw, this.connection) : null;
44
+ const response = await this.call<JobResponse>({ cmd: 'GetJobByCustomId', customId });
45
+ return response.job ? new Job<T>(response.job, this.connection) : null;
37
46
  } catch (err) {
38
47
  if (err instanceof CommandError) return null;
39
48
  throw err;
@@ -52,7 +61,7 @@ export const queryMethods = {
52
61
  ): Promise<Job<T>[]> {
53
62
  const start = opts.start ?? 0;
54
63
  const end = opts.end !== undefined && opts.end >= 0 ? opts.end : 1000;
55
- const response = await this.call(
64
+ const response = await this.call<JobsResponse>(
56
65
  compact({
57
66
  cmd: 'GetJobs',
58
67
  queue: this.name,
@@ -61,8 +70,7 @@ export const queryMethods = {
61
70
  limit: end - start,
62
71
  }) as { cmd: string }
63
72
  );
64
- const jobs = (response.jobs ?? []) as Raw[];
65
- return jobs.map((raw) => new Job<T>(raw, this.connection));
73
+ return (response.jobs ?? []).map((raw) => new Job<T>(raw, this.connection));
66
74
  },
67
75
 
68
76
  getWaiting<T = unknown>(this: Ctx, start?: number, end?: number): Promise<Job<T>[]> {
@@ -96,25 +104,27 @@ export const queryMethods = {
96
104
  },
97
105
 
98
106
  async getJobState(this: Ctx, id: string): Promise<string> {
99
- const response = await this.call({ cmd: 'GetState', id });
100
- return String(response.state);
107
+ return (await this.call<StateResponse>({ cmd: 'GetState', id })).state;
101
108
  },
102
109
 
103
110
  async getResult<R = unknown>(this: Ctx, id: string): Promise<R> {
104
- const response = await this.call({ cmd: 'GetResult', id });
105
- return response.result as R;
111
+ return (await this.call<ResultResponse<R>>({ cmd: 'GetResult', id })).result;
106
112
  },
107
113
 
108
114
  async getChildrenValues<R = unknown>(this: Ctx, id: string): Promise<Record<string, R>> {
109
- return unwrapValues<R>(await this.call({ cmd: 'GetChildrenValues', id }));
115
+ return unwrapValues<R>(await this.call<DataResponse>({ cmd: 'GetChildrenValues', id }));
110
116
  },
111
117
 
112
118
  async getFailedChildrenValues(this: Ctx, id: string): Promise<Record<string, string>> {
113
- return unwrapValues<string>(await this.call({ cmd: 'GetFailedChildrenValues', id }));
119
+ return unwrapValues<string>(
120
+ await this.call<DataResponse>({ cmd: 'GetFailedChildrenValues', id })
121
+ );
114
122
  },
115
123
 
116
124
  async getIgnoredChildrenFailures(this: Ctx, id: string): Promise<Record<string, string>> {
117
- return unwrapValues<string>(await this.call({ cmd: 'GetIgnoredChildrenFailures', id }));
125
+ return unwrapValues<string>(
126
+ await this.call<DataResponse>({ cmd: 'GetIgnoredChildrenFailures', id })
127
+ );
118
128
  },
119
129
 
120
130
  /** Detach a child job from its parent's dependency list. */
@@ -136,7 +146,10 @@ export const queryMethods = {
136
146
  * will not complete), everything else throws CommandTimeoutError.
137
147
  */
138
148
  async waitForJob<R = unknown>(this: Ctx, id: string, ttlMs = 30_000): Promise<R> {
139
- const response = await this.call({ cmd: 'WaitJob', id, timeout: ttlMs }, ttlMs + 5000);
149
+ const response = await this.call<WaitJobResponse<R>>(
150
+ { cmd: 'WaitJob', id, timeout: ttlMs },
151
+ ttlMs + 5000
152
+ );
140
153
  if (response.completed !== true) {
141
154
  let state: string | undefined;
142
155
  try {
@@ -161,18 +174,14 @@ export const queryMethods = {
161
174
  },
162
175
 
163
176
  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
- };
177
+ const response = await this.call<ProgressResponse>({ cmd: 'GetProgress', id });
178
+ return { progress: response.progress ?? 0, message: response.message ?? null };
169
179
  },
170
180
 
171
181
  // ------------------------------------------------------------------- counts
172
182
 
173
183
  async getJobCounts(this: Ctx): Promise<JobCounts> {
174
- const response = await this.call({ cmd: 'GetJobCounts', queue: this.name });
175
- return response.counts as JobCounts;
184
+ return (await this.call<JobCountsResponse>({ cmd: 'GetJobCounts', queue: this.name })).counts;
176
185
  },
177
186
 
178
187
  async getWaitingCount(this: Ctx): Promise<number> {
@@ -206,8 +215,7 @@ export const queryMethods = {
206
215
  },
207
216
 
208
217
  async count(this: Ctx): Promise<number> {
209
- const response = await this.call({ cmd: 'Count', queue: this.name });
210
- return Number(response.count ?? 0);
218
+ return (await this.call<CountResponse>({ cmd: 'Count', queue: this.name })).count ?? 0;
211
219
  },
212
220
 
213
221
  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: 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
- new Connection({
45
- host: opts.host,
46
- port: opts.port,
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(command: Record<string, unknown> & { cmd: string }, timeoutMs?: number): Promise<Response> {
56
- return this.connection.call(command, timeoutMs);
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
@@ -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
+ }
@@ -52,6 +52,8 @@ export class WorkerBase extends EventEmitter {
52
52
  port: opts.port,
53
53
  token: opts.token,
54
54
  tls: opts.tls,
55
+ logger: opts.logger,
56
+ onTelemetry: opts.onTelemetry,
55
57
  });
56
58
  this.readyPromise = new Promise((resolve) => {
57
59
  this.readyResolve = resolve;
@@ -132,6 +134,7 @@ export class WorkerBase extends EventEmitter {
132
134
  if (this.closedFlag) return;
133
135
  this.stopped = true;
134
136
  if (this.loopPromise) await this.loopPromise;
137
+ await this.beforeClose(); // flush any batched ACKs before draining
135
138
  while (!force && this.active.size > 0) await sleep(20);
136
139
  if (this.heartbeatTimer) {
137
140
  clearInterval(this.heartbeatTimer);
@@ -155,4 +158,7 @@ export class WorkerBase extends EventEmitter {
155
158
  this.emit('error', err);
156
159
  }
157
160
  }
161
+
162
+ /** Hook run during close() before draining in-flight jobs (see Worker). */
163
+ protected async beforeClose(): Promise<void> {}
158
164
  }
@@ -2,14 +2,29 @@
2
2
 
3
3
  import type { TlsOption } from './connection.js';
4
4
  import type { Job } from './job.js';
5
+ import type { Observability } from './observability.js';
5
6
 
6
7
  export type Processor<T = unknown, R = unknown> = (job: Job<T>) => R | Promise<R>;
7
8
 
8
- export interface WorkerOptions {
9
+ export interface AckBatchOptions {
10
+ /** Batch ACKs into ACKB round-trips (default false; opt-in for throughput). */
11
+ enabled?: boolean;
12
+ /** Max ACKs per batch (default 50). */
13
+ maxSize?: number;
14
+ /** Max ms to hold a partial batch before flushing (default 5). */
15
+ maxDelayMs?: number;
16
+ }
17
+
18
+ export interface WorkerOptions extends Observability {
9
19
  host?: string;
10
20
  port?: number;
11
21
  token?: string;
12
22
  tls?: TlsOption;
23
+ /**
24
+ * Batch completed-job ACKs into ACKB commands for higher throughput under
25
+ * load. Opt-in: the default (individual ACK per job) is unchanged.
26
+ */
27
+ ackBatch?: AckBatchOptions;
13
28
  /** Max jobs processed in parallel (default 4). */
14
29
  concurrency?: number;
15
30
  /** Max jobs fetched per PULLB (default 10, capped by free slots). */