bunqueue-client 0.1.4 → 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 (48) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +44 -1
  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 +20 -3
  11. package/dist/connection.js +70 -3
  12. package/dist/flow-types.d.ts +2 -1
  13. package/dist/flow.js +33 -7
  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 +6 -5
  20. package/dist/queue-query.d.ts +9 -2
  21. package/dist/queue-query.js +44 -26
  22. package/dist/queue.d.ts +13 -4
  23. package/dist/queue.js +26 -11
  24. package/dist/responses.d.ts +79 -0
  25. package/dist/responses.js +10 -0
  26. package/dist/worker-base.d.ts +2 -0
  27. package/dist/worker-base.js +5 -0
  28. package/dist/worker-types.d.ts +15 -1
  29. package/dist/worker.d.ts +4 -0
  30. package/dist/worker.js +46 -7
  31. package/package.json +2 -1
  32. package/src/ack-batcher.ts +76 -0
  33. package/src/backpressure.ts +40 -0
  34. package/src/connection-pool.ts +71 -0
  35. package/src/connection-types.ts +23 -1
  36. package/src/connection.ts +72 -6
  37. package/src/flow-types.ts +2 -1
  38. package/src/flow.ts +46 -18
  39. package/src/index.ts +28 -2
  40. package/src/job.ts +3 -3
  41. package/src/observability.ts +158 -0
  42. package/src/queue-control.ts +9 -11
  43. package/src/queue-query.ts +73 -35
  44. package/src/queue.ts +42 -15
  45. package/src/responses.ts +96 -0
  46. package/src/worker-base.ts +6 -0
  47. package/src/worker-types.ts +16 -1
  48. package/src/worker.ts +48 -7
package/dist/queue.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * exposes them on the type.
10
10
  */
11
11
  import { Connection } from './connection.js';
12
+ import { ConnectionPool } from './connection-pool.js';
12
13
  import { Job } from './job.js';
13
14
  import { adminMethods } from './queue-admin.js';
14
15
  import { controlMethods } from './queue-control.js';
@@ -21,15 +22,21 @@ export class Queue {
21
22
  ownsConnection;
22
23
  constructor(name, opts = {}) {
23
24
  this.name = name;
25
+ const connOptions = {
26
+ host: opts.host,
27
+ port: opts.port,
28
+ token: opts.token,
29
+ tls: opts.tls,
30
+ commandTimeoutMs: opts.commandTimeoutMs,
31
+ maxInFlight: opts.maxInFlight,
32
+ logger: opts.logger,
33
+ onTelemetry: opts.onTelemetry,
34
+ };
24
35
  this.connection =
25
36
  opts.connection ??
26
- new Connection({
27
- host: opts.host,
28
- port: opts.port,
29
- token: opts.token,
30
- tls: opts.tls,
31
- commandTimeoutMs: opts.commandTimeoutMs,
32
- });
37
+ (opts.poolSize && opts.poolSize > 1
38
+ ? new ConnectionPool(opts.poolSize, connOptions)
39
+ : new Connection(connOptions));
33
40
  this.ownsConnection = opts.connection === undefined;
34
41
  }
35
42
  /** Send a raw command on this queue's connection (used by area modules). */
@@ -50,10 +57,18 @@ export class Queue {
50
57
  }
51
58
  /** Add many jobs in one round-trip; returns Job stubs. */
52
59
  async addBulk(jobs) {
53
- const inputs = jobs.map((entry) => ({
54
- data: jobPayload(entry.name, entry.data),
55
- ...wireJobOptions(entry.opts),
56
- }));
60
+ const inputs = jobs.map((entry) => {
61
+ const opts = wireJobOptions(entry.opts);
62
+ // PUSHB entries are JobInput, whose custom-id field is `customId` —
63
+ // unlike single PUSH which renames `jobId`->`customId` server-side.
64
+ // Without this the batch custom id is silently dropped (idempotency /
65
+ // getJobByCustomId broken).
66
+ if (opts.jobId !== undefined) {
67
+ opts.customId = opts.jobId;
68
+ delete opts.jobId;
69
+ }
70
+ return { data: jobPayload(entry.name, entry.data), ...opts };
71
+ });
57
72
  const response = await this.call({ cmd: 'PUSHB', queue: this.name, jobs: inputs });
58
73
  const ids = (response.ids ?? []);
59
74
  return ids.map((id, i) => new Job({ id, queue: this.name, data: inputs[i].data }, this.connection));
@@ -0,0 +1,79 @@
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
+ import type { JobRaw } from './job.js';
11
+ import type { JobCounts, JobStateName } from './types.js';
12
+ interface Ok {
13
+ ok: true;
14
+ reqId?: string;
15
+ [key: string]: unknown;
16
+ }
17
+ /** PUSH → id, or a bare ok ack. */
18
+ export interface OkResponse extends Ok {
19
+ id?: string;
20
+ }
21
+ /** PUSHB → ids. */
22
+ export interface BatchResponse extends Ok {
23
+ ids: string[];
24
+ }
25
+ /** GetJob → job (null when not found is surfaced as a CommandError instead). */
26
+ export interface JobResponse extends Ok {
27
+ job: JobRaw | null;
28
+ }
29
+ /** PULL → job + lock token. */
30
+ export interface PulledJobResponse extends Ok {
31
+ job: JobRaw | null;
32
+ token: string | null;
33
+ }
34
+ /** PULLB → jobs + lock tokens (same order). */
35
+ export interface PulledJobsResponse extends Ok {
36
+ jobs: JobRaw[];
37
+ tokens: string[];
38
+ }
39
+ /** GetJobs → jobs. */
40
+ export interface JobsResponse extends Ok {
41
+ jobs: JobRaw[];
42
+ }
43
+ /** GetState → state. */
44
+ export interface StateResponse extends Ok {
45
+ id: string;
46
+ state: JobStateName;
47
+ }
48
+ /** GetResult → result. */
49
+ export interface ResultResponse<R = unknown> extends Ok {
50
+ result: R;
51
+ }
52
+ /** WaitJob → completed flag + (on completion) result. */
53
+ export interface WaitJobResponse<R = unknown> extends Ok {
54
+ completed: boolean;
55
+ result?: R;
56
+ }
57
+ /** GetJobCounts → counts. */
58
+ export interface JobCountsResponse extends Ok {
59
+ counts: JobCounts;
60
+ }
61
+ /** GetProgress → progress + message. */
62
+ export interface ProgressResponse extends Ok {
63
+ progress: number;
64
+ message: string | null;
65
+ }
66
+ /** IsPaused → paused. */
67
+ export interface PausedResponse extends Ok {
68
+ paused: boolean;
69
+ }
70
+ /** Count / Clean / PromoteJobs / RetryDlq → count (+ optional removed ids). */
71
+ export interface CountResponse extends Ok {
72
+ count: number;
73
+ ids?: string[];
74
+ }
75
+ /** Generic data-wrapped payload (logs, workers, values, webhookId, …). */
76
+ export interface DataResponse<T = unknown> extends Ok {
77
+ data: T;
78
+ }
79
+ export {};
@@ -0,0 +1,10 @@
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
+ export {};
@@ -59,4 +59,6 @@ export declare class WorkerBase extends EventEmitter {
59
59
  protected safeCall(command: Record<string, unknown> & {
60
60
  cmd: string;
61
61
  }): Promise<void>;
62
+ /** Hook run during close() before draining in-flight jobs (see Worker). */
63
+ protected beforeClose(): Promise<void>;
62
64
  }
@@ -49,6 +49,8 @@ export class WorkerBase extends EventEmitter {
49
49
  port: opts.port,
50
50
  token: opts.token,
51
51
  tls: opts.tls,
52
+ logger: opts.logger,
53
+ onTelemetry: opts.onTelemetry,
52
54
  });
53
55
  this.readyPromise = new Promise((resolve) => {
54
56
  this.readyResolve = resolve;
@@ -120,6 +122,7 @@ export class WorkerBase extends EventEmitter {
120
122
  this.stopped = true;
121
123
  if (this.loopPromise)
122
124
  await this.loopPromise;
125
+ await this.beforeClose(); // flush any batched ACKs before draining
123
126
  while (!force && this.active.size > 0)
124
127
  await sleep(20);
125
128
  if (this.heartbeatTimer) {
@@ -144,4 +147,6 @@ export class WorkerBase extends EventEmitter {
144
147
  this.emit('error', err);
145
148
  }
146
149
  }
150
+ /** Hook run during close() before draining in-flight jobs (see Worker). */
151
+ async beforeClose() { }
147
152
  }
@@ -1,12 +1,26 @@
1
1
  /** Worker option types and shared constants. */
2
2
  import type { TlsOption } from './connection.js';
3
3
  import type { Job } from './job.js';
4
+ import type { Observability } from './observability.js';
4
5
  export type Processor<T = unknown, R = unknown> = (job: Job<T>) => R | Promise<R>;
5
- export interface WorkerOptions {
6
+ export interface AckBatchOptions {
7
+ /** Batch ACKs into ACKB round-trips (default false; opt-in for throughput). */
8
+ enabled?: boolean;
9
+ /** Max ACKs per batch (default 50). */
10
+ maxSize?: number;
11
+ /** Max ms to hold a partial batch before flushing (default 5). */
12
+ maxDelayMs?: number;
13
+ }
14
+ export interface WorkerOptions extends Observability {
6
15
  host?: string;
7
16
  port?: number;
8
17
  token?: string;
9
18
  tls?: TlsOption;
19
+ /**
20
+ * Batch completed-job ACKs into ACKB commands for higher throughput under
21
+ * load. Opt-in: the default (individual ACK per job) is unchanged.
22
+ */
23
+ ackBatch?: AckBatchOptions;
10
24
  /** Max jobs processed in parallel (default 4). */
11
25
  concurrency?: number;
12
26
  /** Max jobs fetched per PULLB (default 10, capped by free slots). */
package/dist/worker.d.ts CHANGED
@@ -9,12 +9,16 @@ import { WorkerBase } from './worker-base.js';
9
9
  import { type Processor, type WorkerOptions } from './worker-types.js';
10
10
  export declare class Worker<T = unknown, R = unknown> extends WorkerBase {
11
11
  private readonly processor;
12
+ private readonly ackBatcher;
12
13
  constructor(queue: string, processor: Processor<T, R>, opts?: WorkerOptions);
14
+ /** Flush batched ACKs before the base class drains in-flight jobs. */
15
+ protected beforeClose(): Promise<void>;
13
16
  /** Start the pull loop (no-op if already running). */
14
17
  run(): void;
15
18
  private loop;
16
19
  private pollOnce;
17
20
  private runJob;
21
+ private finishJob;
18
22
  private startHeartbeat;
19
23
  private register;
20
24
  }
package/dist/worker.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * Lifecycle state and cooperative cancel live in WorkerBase.
7
7
  */
8
8
  import { hostname } from 'node:os';
9
+ import { AckBatcher } from './ack-batcher.js';
9
10
  import { CommandTimeoutError, ConnectionClosedError, UnrecoverableError } from './errors.js';
10
11
  import { compact } from './frame.js';
11
12
  import { Job } from './job.js';
@@ -13,12 +14,22 @@ import { WorkerBase } from './worker-base.js';
13
14
  import { MAX_STACK_LINES, RECONNECT_BACKOFF_MS, sleep, } from './worker-types.js';
14
15
  export class Worker extends WorkerBase {
15
16
  processor;
17
+ ackBatcher;
16
18
  constructor(queue, processor, opts = {}) {
17
19
  super(queue, opts);
18
20
  this.processor = processor;
21
+ const ab = opts.ackBatch;
22
+ this.ackBatcher = ab?.enabled
23
+ ? new AckBatcher(this.connection, ab.maxSize ?? 50, ab.maxDelayMs ?? 5)
24
+ : null;
19
25
  if (opts.autorun !== false)
20
26
  this.run();
21
27
  }
28
+ /** Flush batched ACKs before the base class drains in-flight jobs. */
29
+ async beforeClose() {
30
+ if (this.ackBatcher)
31
+ await this.ackBatcher.flush();
32
+ }
22
33
  /** Start the pull loop (no-op if already running). */
23
34
  run() {
24
35
  if (this.running || this.closedFlag)
@@ -78,8 +89,8 @@ export class Worker extends WorkerBase {
78
89
  owner: this.workerId,
79
90
  lockTtl: this.lockTtlMs,
80
91
  }, this.pollTimeoutMs + 10_000);
81
- const jobs = (response.jobs ?? []);
82
- const tokens = (response.tokens ?? []);
92
+ const jobs = response.jobs ?? [];
93
+ const tokens = response.tokens ?? [];
83
94
  if (jobs.length === 0) {
84
95
  if (this.wasBusy && this.active.size === 0) {
85
96
  this.wasBusy = false;
@@ -100,14 +111,41 @@ export class Worker extends WorkerBase {
100
111
  this.emit('active', job);
101
112
  try {
102
113
  const result = await this.processor(job);
114
+ if (this.ackBatcher) {
115
+ // Defer the ACK into a batch; the job stays active (lock renewed) until
116
+ // the ACKB settles. onSettled frees the slot FIRST — a throwing
117
+ // listener (e.g. an unhandled 'error' emit) must never leak the slot
118
+ // and permanently shrink the worker's effective concurrency.
119
+ this.ackBatcher.add({
120
+ id: job.id,
121
+ token,
122
+ result: result ?? undefined,
123
+ onSettled: (err) => {
124
+ this.finishJob(job.id);
125
+ if (err) {
126
+ this.emit('error', err);
127
+ }
128
+ else {
129
+ this.processed += 1;
130
+ this.emit('completed', job, result);
131
+ }
132
+ },
133
+ });
134
+ return;
135
+ }
103
136
  this.processed += 1;
104
137
  await this.safeCall(compact({ cmd: 'ACK', id: job.id, token, result: result ?? undefined }));
138
+ // Free the slot BEFORE emitting: a throwing 'completed' listener must
139
+ // not leak the active slot (same rationale as the batched path).
140
+ this.finishJob(job.id);
105
141
  this.emit('completed', job, result);
106
142
  }
107
143
  catch (err) {
108
144
  this.failedCount += 1;
109
145
  const error = err instanceof Error ? err : new Error(String(err));
110
- const stack = (error.stack ?? error.message).split('\n').slice(-MAX_STACK_LINES);
146
+ // Keep the FIRST lines: in a JS stack the message + throw site lead, so
147
+ // slice(0,N) preserves them (slice(-N) would drop them on long stacks).
148
+ const stack = (error.stack ?? error.message).split('\n').slice(0, MAX_STACK_LINES);
111
149
  await this.safeCall(compact({
112
150
  cmd: 'FAIL',
113
151
  id: job.id,
@@ -116,12 +154,13 @@ export class Worker extends WorkerBase {
116
154
  stack,
117
155
  unrecoverable: err instanceof UnrecoverableError ? true : undefined,
118
156
  }));
157
+ this.finishJob(job.id);
119
158
  this.emit('failed', job, error);
120
159
  }
121
- finally {
122
- this.active.delete(job.id);
123
- this.cancelledJobs.delete(job.id);
124
- }
160
+ }
161
+ finishJob(id) {
162
+ this.active.delete(id);
163
+ this.cancelledJobs.delete(id);
125
164
  }
126
165
  // --------------------------------------------------------------- heartbeat
127
166
  startHeartbeat() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunqueue-client",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Cross-runtime TypeScript client for the bunqueue job queue server — Node.js, Bun, Deno and Cloudflare Workers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,6 +17,7 @@
17
17
  "dist",
18
18
  "src",
19
19
  "README.md",
20
+ "CHANGELOG.md",
20
21
  "LICENSE"
21
22
  ],
22
23
  "engines": {
@@ -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
+ }
@@ -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;