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.
Files changed (52) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +46 -3
  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/bunqueue/bunqueue.js +12 -2
  8. package/dist/connection-pool.d.ts +30 -0
  9. package/dist/connection-pool.js +60 -0
  10. package/dist/connection-types.d.ts +21 -1
  11. package/dist/connection.d.ts +12 -3
  12. package/dist/connection.js +39 -3
  13. package/dist/flow-types.d.ts +2 -1
  14. package/dist/flow.js +12 -3
  15. package/dist/index.d.ts +7 -2
  16. package/dist/index.js +3 -1
  17. package/dist/job.d.ts +2 -2
  18. package/dist/observability.d.ts +95 -0
  19. package/dist/observability.js +110 -0
  20. package/dist/queue-admin.js +16 -2
  21. package/dist/queue-control.d.ts +5 -0
  22. package/dist/queue-control.js +27 -9
  23. package/dist/queue-query.js +13 -21
  24. package/dist/queue.d.ts +13 -4
  25. package/dist/queue.js +14 -7
  26. package/dist/responses.d.ts +79 -0
  27. package/dist/responses.js +10 -0
  28. package/dist/worker-base.d.ts +15 -3
  29. package/dist/worker-base.js +15 -7
  30. package/dist/worker-types.d.ts +43 -1
  31. package/dist/worker.d.ts +5 -1
  32. package/dist/worker.js +59 -14
  33. package/package.json +3 -1
  34. package/src/ack-batcher.ts +76 -0
  35. package/src/backpressure.ts +40 -0
  36. package/src/bunqueue/bunqueue.ts +12 -1
  37. package/src/connection-pool.ts +71 -0
  38. package/src/connection-types.ts +23 -1
  39. package/src/connection.ts +42 -6
  40. package/src/flow-types.ts +2 -1
  41. package/src/flow.ts +24 -16
  42. package/src/index.ts +33 -2
  43. package/src/job.ts +3 -3
  44. package/src/observability.ts +158 -0
  45. package/src/queue-admin.ts +15 -2
  46. package/src/queue-control.ts +34 -11
  47. package/src/queue-query.ts +39 -29
  48. package/src/queue.ts +30 -11
  49. package/src/responses.ts +96 -0
  50. package/src/worker-base.ts +49 -11
  51. package/src/worker-types.ts +42 -1
  52. package/src/worker.ts +63 -16
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Batches completed-job ACKs into ACKB round-trips for higher throughput.
3
+ *
4
+ * Opt-in (see WorkerOptions.ackBatch). Items flush when the buffer reaches
5
+ * `maxSize` or after `maxDelayMs`, whichever comes first; `flush()` drains the
6
+ * rest on shutdown. Each item's `onSettled` fires after the batch is acked
7
+ * (or errored), so the worker keeps a job "active" — and its lock renewed —
8
+ * until the server has confirmed the ack.
9
+ */
10
+
11
+ import type { Connection } from './connection.js';
12
+ import { compact } from './frame.js';
13
+
14
+ export interface AckItem {
15
+ id: string;
16
+ token: string;
17
+ result: unknown;
18
+ /** Called once the batch settles: `err` set on failure, undefined on ack. */
19
+ onSettled: (err?: unknown) => void;
20
+ }
21
+
22
+ export class AckBatcher {
23
+ private buffer: AckItem[] = [];
24
+ private timer: ReturnType<typeof setTimeout> | null = null;
25
+
26
+ constructor(
27
+ private readonly connection: Connection,
28
+ private readonly maxSize: number,
29
+ private readonly maxDelayMs: number
30
+ ) {}
31
+
32
+ add(item: AckItem): void {
33
+ this.buffer.push(item);
34
+ if (this.buffer.length >= this.maxSize) {
35
+ void this.flush();
36
+ } else if (!this.timer) {
37
+ this.timer = setTimeout(() => void this.flush(), this.maxDelayMs);
38
+ this.timer.unref?.(); // don't keep the process alive for a pending flush
39
+ }
40
+ }
41
+
42
+ /** Send the buffered ACKs as one ACKB (no-op when empty). */
43
+ async flush(): Promise<void> {
44
+ if (this.timer) {
45
+ clearTimeout(this.timer);
46
+ this.timer = null;
47
+ }
48
+ if (this.buffer.length === 0) return;
49
+ const batch = this.buffer;
50
+ this.buffer = [];
51
+ // Capture the wire outcome first; settle callbacks run OUTSIDE the try so
52
+ // a throwing callback can neither be re-invoked with an error (double
53
+ // settle) nor starve the remaining items of their callback.
54
+ let error: unknown;
55
+ try {
56
+ await this.connection.call(
57
+ compact({
58
+ cmd: 'ACKB',
59
+ ids: batch.map((b) => b.id),
60
+ tokens: batch.map((b) => b.token),
61
+ results: batch.map((b) => b.result),
62
+ }) as { cmd: string }
63
+ );
64
+ } catch (err) {
65
+ error = err ?? new Error('ACKB failed');
66
+ }
67
+ for (const item of batch) {
68
+ try {
69
+ item.onSettled(error);
70
+ } catch {
71
+ // A callback error (e.g. an unhandled 'error' emit in the worker)
72
+ // must never prevent the other items from settling.
73
+ }
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Backpressure gate: bounds the number of in-flight commands on a connection.
3
+ *
4
+ * When the in-flight count reaches `max`, `acquire()` parks the caller until a
5
+ * slot frees (a pending command settles). This turns unbounded memory growth
6
+ * under a fast producer / slow server into cooperative flow control. `max <= 0`
7
+ * disables the gate (unbounded, the default).
8
+ */
9
+ export class Backpressure {
10
+ private readonly waiters: Array<() => void> = [];
11
+
12
+ constructor(
13
+ private readonly max: number,
14
+ private readonly onWait?: () => void
15
+ ) {}
16
+
17
+ /** Resolve immediately if under the limit, else park until a slot frees. */
18
+ acquire(inFlight: number): Promise<void> | void {
19
+ if (this.max <= 0 || inFlight < this.max) return;
20
+ this.onWait?.();
21
+ return new Promise<void>((resolve) => {
22
+ this.waiters.push(resolve);
23
+ });
24
+ }
25
+
26
+ /** Release one parked caller (call when a pending command settles). */
27
+ release(): void {
28
+ this.waiters.shift()?.();
29
+ }
30
+
31
+ /** Release every parked caller (call on teardown so none hang forever). */
32
+ clear(): void {
33
+ while (this.waiters.length > 0) this.waiters.shift()?.();
34
+ }
35
+
36
+ /** Number of callers currently parked. */
37
+ get waiting(): number {
38
+ return this.waiters.length;
39
+ }
40
+ }
@@ -90,7 +90,18 @@ export class Bunqueue<T = unknown, R = unknown> {
90
90
 
91
91
  // DLQ & rate limit manager
92
92
  this.dlqrl = new DlqRateLimitManager<T>(this.queue);
93
- if (opts.dlq) void this.dlqrl.setDlqConfig(opts.dlq);
93
+ // Fire-and-forget config push: without the catch, an unreachable server at
94
+ // construction time becomes an unhandled rejection that kills the process.
95
+ // Route the failure to the worker's 'error' event (the channel every other
96
+ // background command failure uses); with no listener attached, swallow it
97
+ // like pause()/resume() do — an unlistened 'error' emit would itself throw.
98
+ if (opts.dlq) {
99
+ void this.dlqrl.setDlqConfig(opts.dlq).catch((err: unknown) => {
100
+ if (this.worker.listenerCount('error') > 0) {
101
+ this.worker.emit('error', err instanceof Error ? err : new Error(String(err)));
102
+ }
103
+ });
104
+ }
94
105
 
95
106
  // Subsystems
96
107
  this.cb = opts.circuitBreaker
@@ -0,0 +1,71 @@
1
+ /**
2
+ * A round-robin pool of TCP connections for producer-side throughput.
3
+ *
4
+ * Each `call()` is dispatched to the next connection, so independent commands
5
+ * (PUSH, queries, control) fan out across N sockets instead of head-of-lining
6
+ * on one. Use it for {@link Queue} / {@link FlowProducer}; NOT for a Worker,
7
+ * whose pull→ack flow is bound to a single connection's lock ownership and
8
+ * reconnect/registration lifecycle.
9
+ *
10
+ * Satisfies {@link ConnectionLike}, so it is a drop-in for a single Connection.
11
+ * Lifecycle events from every member connection are re-emitted here.
12
+ */
13
+
14
+ import { EventEmitter } from 'node:events';
15
+ import { Connection } from './connection.js';
16
+ import type { Command, ConnectionLike, ConnectionOptions, Response } from './connection-types.js';
17
+ import { LIFECYCLE_EVENTS } from './observability.js';
18
+
19
+ export class ConnectionPool extends EventEmitter implements ConnectionLike {
20
+ private readonly connections: Connection[];
21
+ private cursor = 0;
22
+
23
+ constructor(size: number, options: ConnectionOptions = {}) {
24
+ super();
25
+ const n = Math.max(1, Math.floor(size));
26
+ this.connections = Array.from({ length: n }, () => {
27
+ const conn = new Connection(options);
28
+ for (const event of LIFECYCLE_EVENTS) {
29
+ conn.on(event, (payload) => this.emit(event, payload));
30
+ }
31
+ return conn;
32
+ });
33
+ }
34
+
35
+ /** How many connections back this pool. */
36
+ get size(): number {
37
+ return this.connections.length;
38
+ }
39
+
40
+ private next(): Connection {
41
+ const conn = this.connections[this.cursor];
42
+ this.cursor = (this.cursor + 1) % this.connections.length;
43
+ return conn;
44
+ }
45
+
46
+ call<R = Response>(command: Command, timeoutMs?: number): Promise<R> {
47
+ return this.next().call<R>(command, timeoutMs);
48
+ }
49
+
50
+ ping(): Promise<boolean> {
51
+ return this.connections[0].ping();
52
+ }
53
+
54
+ async connect(): Promise<void> {
55
+ await Promise.all(this.connections.map((conn) => conn.connect()));
56
+ }
57
+
58
+ close(): void {
59
+ for (const conn of this.connections) conn.close();
60
+ }
61
+
62
+ /** True while at least one member connection is up. */
63
+ get isConnected(): boolean {
64
+ return this.connections.some((conn) => conn.isConnected);
65
+ }
66
+
67
+ /** Representative generation (first connection); pools are producer-side. */
68
+ get generation(): number {
69
+ return this.connections[0].generation;
70
+ }
71
+ }
@@ -1,19 +1,41 @@
1
1
  /** Connection option and message types. */
2
2
 
3
+ import type { Observability } from './observability.js';
4
+
3
5
  export type TlsOption = boolean | { caFile?: string; rejectUnauthorized?: boolean } | undefined;
4
6
 
5
- export interface ConnectionOptions {
7
+ export interface ConnectionOptions extends Observability {
6
8
  host?: string;
7
9
  port?: number;
8
10
  token?: string;
9
11
  tls?: TlsOption;
10
12
  connectTimeoutMs?: number;
11
13
  commandTimeoutMs?: number;
14
+ /**
15
+ * Max in-flight commands before {@link Connection.call} applies backpressure
16
+ * (awaits a free slot). 0 / undefined = unbounded. Bounds memory under load.
17
+ */
18
+ maxInFlight?: number;
12
19
  }
13
20
 
14
21
  export type Command = Record<string, unknown> & { cmd: string };
15
22
  export type Response = Record<string, unknown> & { ok: boolean };
16
23
 
24
+ /**
25
+ * The connection surface consumers (Queue, FlowProducer, Job) depend on.
26
+ * Both {@link Connection} and a round-robin ConnectionPool satisfy it, so a
27
+ * pool can be dropped in transparently for producer-side throughput.
28
+ */
29
+ export interface ConnectionLike {
30
+ call<R = Response>(command: Command, timeoutMs?: number): Promise<R>;
31
+ ping(): Promise<boolean>;
32
+ connect(): Promise<void>;
33
+ close(): void;
34
+ readonly isConnected: boolean;
35
+ readonly generation: number;
36
+ on(event: string, listener: (...args: unknown[]) => void): this;
37
+ }
38
+
17
39
  export interface Pending {
18
40
  resolve: (response: Response) => void;
19
41
  reject: (error: Error) => void;
package/src/connection.ts CHANGED
@@ -6,8 +6,10 @@
6
6
  * builtins (net/tls), which Node, Bun and Deno all support.
7
7
  */
8
8
 
9
+ import { EventEmitter } from 'node:events';
9
10
  import type { Socket } from 'node:net';
10
11
  import { pack, unpack } from 'msgpackr';
12
+ import { Backpressure } from './backpressure.js';
11
13
  import type {
12
14
  Command,
13
15
  ConnectionOptions,
@@ -17,12 +19,19 @@ import type {
17
19
  } from './connection-types.js';
18
20
  import { AuthError, CommandError, CommandTimeoutError, ConnectionClosedError } from './errors.js';
19
21
  import { compact, FrameParser, frame, PROTOCOL_VERSION } from './frame.js';
22
+ import { nowMs, Telemetry } from './observability.js';
20
23
  import { openSocket } from './socket-factory.js';
21
24
 
22
25
  export type { Command, ConnectionOptions, Response, TlsOption } from './connection-types.js';
23
26
 
24
- /** A single pipelined TCP connection to a bunqueue server. */
25
- export class Connection {
27
+ /**
28
+ * A single pipelined TCP connection to a bunqueue server.
29
+ *
30
+ * Emits typed lifecycle events for observability: `connect`, `disconnect` and
31
+ * `reconnect_scheduled` (payloads mirror the telemetry events). Attach a
32
+ * telemetry sink via the `onTelemetry` option for per-command latency metrics.
33
+ */
34
+ export class Connection extends EventEmitter {
26
35
  readonly host: string;
27
36
  readonly port: number;
28
37
  readonly token: string | undefined;
@@ -44,14 +53,22 @@ export class Connection {
44
53
  // socket is presumed dead and torn down so the next call reconnects.
45
54
  private readonly maxCommandTimeouts = 3;
46
55
  private consecutiveTimeouts = 0;
56
+ private readonly telemetry: Telemetry;
57
+ private readonly backpressure: Backpressure;
47
58
 
48
59
  constructor(options: ConnectionOptions = {}) {
60
+ super();
49
61
  this.host = options.host ?? 'localhost';
50
62
  this.port = options.port ?? 6789;
51
63
  this.token = options.token;
52
64
  this.tls = options.tls;
53
65
  this.connectTimeoutMs = options.connectTimeoutMs ?? 5000;
54
66
  this.commandTimeoutMs = options.commandTimeoutMs ?? 10_000;
67
+ this.telemetry = new Telemetry(options, (event, payload) => this.emit(event, payload));
68
+ const maxInFlight = options.maxInFlight ?? 0;
69
+ this.backpressure = new Backpressure(maxInFlight, () =>
70
+ this.telemetry.backpressure(this.pending.size, maxInFlight)
71
+ );
55
72
  }
56
73
 
57
74
  get isConnected(): boolean {
@@ -89,6 +106,7 @@ export class Connection {
89
106
  this.failedAttempts += 1;
90
107
  const backoff = Math.min(500 * 2 ** (this.failedAttempts - 1), 5000);
91
108
  this.nextAttemptAt = Date.now() + backoff;
109
+ this.telemetry.reconnectScheduled(this.host, this.port, this.failedAttempts, backoff);
92
110
  throw err;
93
111
  })
94
112
  .finally(() => {
@@ -98,6 +116,7 @@ export class Connection {
98
116
  }
99
117
 
100
118
  private async doConnect(): Promise<void> {
119
+ const startMs = nowMs();
101
120
  const socket = await openSocket(this.host, this.port, this.tls, this.connectTimeoutMs);
102
121
  socket.setNoDelay(true);
103
122
  // TCP keepalive (~15s idle) surfaces a half-open link (cloud LB/NAT idle
@@ -113,6 +132,7 @@ export class Connection {
113
132
  this.connected = true;
114
133
  this.connectGeneration += 1;
115
134
  this.consecutiveTimeouts = 0;
135
+ this.telemetry.connected(this.host, this.port, this.connectGeneration, startMs);
116
136
 
117
137
  // INVARIANT (H3): connected is flipped true before Auth, which is safe
118
138
  // ONLY because call() writes the Auth frame synchronously — there is no
@@ -123,7 +143,9 @@ export class Connection {
123
143
  if (this.token) {
124
144
  try {
125
145
  await this.call({ cmd: 'Auth', token: this.token });
146
+ this.telemetry.auth(true);
126
147
  } catch (err) {
148
+ this.telemetry.auth(false);
127
149
  this.teardown();
128
150
  if (err instanceof CommandError) throw new AuthError(err.message);
129
151
  throw err;
@@ -135,19 +157,26 @@ export class Connection {
135
157
  * Send a command and await its response. Rejects with CommandError when
136
158
  * the server answers ok=false. Reconnects lazily if the link was lost.
137
159
  */
138
- async call(command: Command, timeoutMs?: number): Promise<Response> {
160
+ async call<R = Response>(command: Command, timeoutMs?: number): Promise<R> {
139
161
  if (!this.connected) await this.connect();
162
+ // Backpressure: park here if too many commands are already in flight. The
163
+ // socket may be torn down while parked, so re-check after the gate.
164
+ const gate = this.backpressure.acquire(this.pending.size);
165
+ if (gate) await gate;
140
166
  const socket = this.socket;
141
- if (!socket) throw new ConnectionClosedError('not connected');
167
+ if (!this.connected || !socket) throw new ConnectionClosedError('not connected');
142
168
 
143
169
  this.reqCounter = (this.reqCounter + 1) & 0x7fffffff;
144
170
  const reqId = String(this.reqCounter);
145
171
  const payload = pack({ ...compact(command), reqId });
146
172
  const gen = this.connectGeneration; // snapshot: a timeout must not tear down a newer conn
173
+ const startMs = nowMs();
147
174
 
148
- return new Promise<Response>((resolve, reject) => {
175
+ return new Promise<R>((resolve, reject) => {
149
176
  const timer = setTimeout(() => {
150
177
  this.pending.delete(reqId);
178
+ this.backpressure.release();
179
+ this.telemetry.timeout(command.cmd, reqId);
151
180
  this.noteTimeout(gen);
152
181
  reject(new CommandTimeoutError(`no response for ${command.cmd} within timeout`));
153
182
  }, timeoutMs ?? this.commandTimeoutMs);
@@ -156,10 +185,11 @@ export class Connection {
156
185
  resolve: (response) => {
157
186
  clearTimeout(timer);
158
187
  this.consecutiveTimeouts = 0; // any reply means the link is alive
188
+ this.telemetry.command(command.cmd, reqId, startMs, response.ok);
159
189
  if (!response.ok) {
160
190
  reject(new CommandError(String(response.error ?? 'unknown server error')));
161
191
  } else {
162
- resolve(response);
192
+ resolve(response as R);
163
193
  }
164
194
  },
165
195
  reject: (err) => {
@@ -173,6 +203,7 @@ export class Connection {
173
203
  if (err) {
174
204
  const entry = this.pending.get(reqId);
175
205
  this.pending.delete(reqId);
206
+ this.backpressure.release();
176
207
  entry?.reject(new ConnectionClosedError(`send failed: ${err.message}`));
177
208
  this.teardown();
178
209
  }
@@ -228,6 +259,7 @@ export class Connection {
228
259
  const entry = this.pending.get(String(reqId));
229
260
  if (entry) {
230
261
  this.pending.delete(String(reqId));
262
+ this.backpressure.release();
231
263
  entry.resolve(response);
232
264
  }
233
265
  }
@@ -247,6 +279,8 @@ export class Connection {
247
279
  }
248
280
 
249
281
  private teardown(): void {
282
+ const wasConnected = this.socket !== null;
283
+ const gen = this.connectGeneration;
250
284
  this.connected = false;
251
285
  const socket = this.socket;
252
286
  this.socket = null;
@@ -260,5 +294,7 @@ export class Connection {
260
294
  clearTimeout(entry.timer);
261
295
  entry.reject(new ConnectionClosedError('connection lost'));
262
296
  }
297
+ this.backpressure.clear(); // release parked callers; they re-check + fail/reconnect
298
+ if (wasConnected) this.telemetry.disconnected(this.host, this.port, gen);
263
299
  }
264
300
  }
package/src/flow-types.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { Connection, TlsOption } from './connection.js';
4
4
  import type { Job } from './job.js';
5
+ import type { Observability } from './observability.js';
5
6
  import type { JobOptions } from './types.js';
6
7
 
7
8
  export interface FlowJob<T = unknown> {
@@ -24,7 +25,7 @@ export interface FlowStep<T = unknown> {
24
25
  opts?: JobOptions;
25
26
  }
26
27
 
27
- export interface FlowProducerOptions {
28
+ export interface FlowProducerOptions extends Observability {
28
29
  host?: string;
29
30
  port?: number;
30
31
  token?: string;
package/src/flow.ts CHANGED
@@ -15,6 +15,7 @@ import type {
15
15
  } from './flow-types.js';
16
16
  import { compact } from './frame.js';
17
17
  import { Job } from './job.js';
18
+ import type { JobResponse, OkResponse } from './responses.js';
18
19
  import { type JobOptions, jobPayload, wireJobOptions } from './types.js';
19
20
 
20
21
  export class FlowProducer {
@@ -24,7 +25,14 @@ export class FlowProducer {
24
25
  constructor(opts: FlowProducerOptions = {}) {
25
26
  this.connection =
26
27
  opts.connection ??
27
- new Connection({ host: opts.host, port: opts.port, token: opts.token, tls: opts.tls });
28
+ new Connection({
29
+ host: opts.host,
30
+ port: opts.port,
31
+ token: opts.token,
32
+ tls: opts.tls,
33
+ logger: opts.logger,
34
+ onTelemetry: opts.onTelemetry,
35
+ });
28
36
  this.ownsConnection = opts.connection === undefined;
29
37
  }
30
38
 
@@ -70,19 +78,19 @@ export class FlowProducer {
70
78
  let prevId: string | null = null;
71
79
  try {
72
80
  for (const step of steps) {
73
- const data = compact({
81
+ const data: Record<string, unknown> = compact({
74
82
  ...jobPayload(step.name, step.data),
75
83
  __flowParentId: prevId ?? undefined,
76
84
  });
77
- const response = await this.connection.call(
78
- compact({
79
- cmd: 'PUSH',
80
- queue: step.queueName,
81
- data,
82
- ...wireJobOptions(step.opts),
83
- dependsOn: prevId ? [prevId] : undefined,
84
- }) as { cmd: string }
85
- );
85
+ // Connection.call compacts internally, so no outer compact() is needed
86
+ // (nesting it confused generic inference of `data`/`response`).
87
+ const response: OkResponse = await this.connection.call<OkResponse>({
88
+ cmd: 'PUSH',
89
+ queue: step.queueName,
90
+ data,
91
+ ...wireJobOptions(step.opts),
92
+ dependsOn: prevId ? [prevId] : undefined,
93
+ });
86
94
  const id = String(response.id);
87
95
  jobIds.push(id);
88
96
  prevId = id;
@@ -103,7 +111,7 @@ export class FlowProducer {
103
111
  try {
104
112
  const results = await Promise.allSettled(
105
113
  parallel.map(async (step) => {
106
- const response = await this.connection.call(
114
+ const response = await this.connection.call<OkResponse>(
107
115
  compact({
108
116
  cmd: 'PUSH',
109
117
  queue: step.queueName,
@@ -188,7 +196,7 @@ export class FlowProducer {
188
196
  parentRef: { id: string; queue: string } | null,
189
197
  childIds: string[]
190
198
  ): Promise<string> {
191
- const response = await this.connection.call(
199
+ const response = await this.connection.call<OkResponse>(
192
200
  compact({
193
201
  cmd: 'PUSH',
194
202
  queue,
@@ -219,16 +227,16 @@ export class FlowProducer {
219
227
  // A missing job — the root, or a child removed via removeOnComplete/cancel
220
228
  // (childrenIds is a static push-time list, never pruned) — yields null and
221
229
  // is skipped, returning the surviving partial tree instead of throwing.
222
- let response: Awaited<ReturnType<Connection['call']>>;
230
+ let response: JobResponse;
223
231
  try {
224
- response = await this.connection.call({ cmd: 'GetJob', id });
232
+ response = await this.connection.call<JobResponse>({ cmd: 'GetJob', id });
225
233
  } catch (err) {
226
234
  // Only 'Job not found' means a removed node; a real server error must not
227
235
  // masquerade as a missing child and yield a misleading partial tree.
228
236
  if (err instanceof CommandError && /not found/i.test(err.message)) return null;
229
237
  throw err;
230
238
  }
231
- const raw = response.job as Record<string, unknown> | null;
239
+ const raw = response.job;
232
240
  if (!raw) return null;
233
241
  const job = new Job<T>(raw, this.connection);
234
242
  if (depth <= 0 || job.childrenIds.length === 0) return { job };
package/src/index.ts CHANGED
@@ -26,6 +26,8 @@ export type {
26
26
  } from './bunqueue/types.js';
27
27
  export type { Command, ConnectionOptions, Response, TlsOption } from './connection.js';
28
28
  export { Connection } from './connection.js';
29
+ export { ConnectionPool } from './connection-pool.js';
30
+ export type { ConnectionLike } from './connection-types.js';
29
31
  export {
30
32
  AuthError,
31
33
  BunqueueError,
@@ -45,9 +47,33 @@ export type {
45
47
  export { MAX_FRAME_SIZE, PROTOCOL_VERSION } from './frame.js';
46
48
  export type { JobRaw } from './job.js';
47
49
  export { Job } from './job.js';
50
+ export type {
51
+ Logger,
52
+ LogLevel,
53
+ Observability,
54
+ TelemetryEvent,
55
+ TelemetryHandler,
56
+ } from './observability.js';
57
+ export { consoleLogger, noopLogger } from './observability.js';
48
58
  export type { BulkJobEntry, QueueOptions } from './queue.js';
49
59
  export { Queue } from './queue.js';
50
60
  export type { SchedulerOptions } from './queue-admin.js';
61
+ export type {
62
+ BatchResponse,
63
+ CountResponse,
64
+ DataResponse,
65
+ JobCountsResponse,
66
+ JobResponse,
67
+ JobsResponse,
68
+ OkResponse,
69
+ PausedResponse,
70
+ ProgressResponse,
71
+ PulledJobResponse,
72
+ PulledJobsResponse,
73
+ ResultResponse,
74
+ StateResponse,
75
+ WaitJobResponse,
76
+ } from './responses.js';
51
77
  export type {
52
78
  BackoffOptions,
53
79
  DeduplicationOptions,
@@ -57,6 +83,11 @@ export type {
57
83
  RepeatOptions,
58
84
  } from './types.js';
59
85
  export { Worker } from './worker.js';
60
- export type { Processor, WorkerOptions } from './worker-types.js';
86
+ export type {
87
+ AckBatchOptions,
88
+ Processor,
89
+ WorkerEventMap,
90
+ WorkerOptions,
91
+ } from './worker-types.js';
61
92
 
62
- export const __version__ = '0.1.2';
93
+ export const __version__ = '0.1.7';
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;