@babelqueue/pulsar 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -20,7 +20,7 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
20
20
  * });
21
21
  * const consumer = new PulsarConsumer(sub, {
22
22
  * "urn:babel:orders:created": async (env) => { ... },
23
- * });
23
+ * }, { subscriptionType: "Shared" }); // declare it (or pass `client` together with `topic`): see PulsarConsumerOptions
24
24
  * await consumer.run();
25
25
  *
26
26
  * This implements §5 of the broker-bindings contract: the canonical envelope is the message
@@ -28,7 +28,12 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
28
28
  * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /
29
29
  * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);
30
30
  * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is
31
- * redelivered (at-least-once) and `getRedeliveryCount()` is incremented. Per §5.2 / §5.7 the
31
+ * redelivered (at-least-once) and `getRedeliveryCount()` is incremented — up to `maxAttempts`
32
+ * deliveries (default 3, ADR-0036): the message that exhausts them is written to `<queue>.dlq`
33
+ * (when a `deadLetterProducer` is given) or acknowledged and dropped. A body that does not decode
34
+ * into an envelope (including an object with no valid identity) is poison: it never reaches a
35
+ * handler, and with dead-lettering on it is recorded in `<queue>.dlq` in the ADR-0035 shape without
36
+ * retry. Per §5.2 / §5.7 the
32
37
  * consumer routes on the `bq-job` property (the body's URN is used only when the property is
33
38
  * absent or blank) and takes `attempts` from the authoritative `bq-attempts` property (the
34
39
  * body's value is used only when the property is absent or not a plain ASCII-decimal integer
@@ -37,9 +42,12 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
37
42
  *
38
43
  * **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads the
39
44
  * `bq-schema-version` property. A missing or blank value (empty or only ASCII whitespace) changes nothing; any value other than
40
- * the core's supported `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) rejects the message undecoded
41
- * through the same path as a non-conformant envelope — `onError` (with an error naming the
42
- * version, and an empty envelope since nothing was decoded) and a `negativeAcknowledge`.
45
+ * the core's supported `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) rejects the message undecoded:
46
+ * `onError` (with an error naming the version, and an empty envelope since nothing was decoded),
47
+ * then it is settled on its first delivery (ADR-0037 decision 1) — with a `deadLetterProducer` the
48
+ * body, byte for byte, goes to `<queue>.dlq` and the message is acknowledged after the write (a failed
49
+ * write is a `negativeAcknowledge`, never an acknowledgement); without one it is acknowledged and
50
+ * dropped (logged). Never retried, whatever `bq-attempts`, the redelivery count or `maxAttempts` say.
43
51
  *
44
52
  * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
45
53
  * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`
@@ -79,10 +87,27 @@ interface PulsarReceivedMessage {
79
87
  * satisfies it structurally; a fake satisfies it in tests.
80
88
  */
81
89
  interface PulsarConsumerClient {
90
+ /**
91
+ * The topic the subscription reads. pulsar-client's `Consumer` has **no** such method (only its `Producer`
92
+ * does), so with it the topic comes from the {@link PulsarConsumerOptions.topic} option; a custom
93
+ * subscription wrapper may provide this to make `topic` unnecessary.
94
+ */
95
+ getTopic?(): string;
82
96
  receive(timeoutMs?: number): Promise<PulsarReceivedMessage>;
83
97
  acknowledge(message: PulsarReceivedMessage): Promise<void> | void;
84
98
  negativeAcknowledge(message: PulsarReceivedMessage): Promise<void> | void;
85
99
  }
100
+ /**
101
+ * The subset of the Pulsar client this adapter calls to build its own retry producer. A `Client` from
102
+ * `pulsar-client` satisfies it structurally.
103
+ */
104
+ interface PulsarClientLike {
105
+ createProducer(config: {
106
+ topic: string;
107
+ }): Promise<PulsarProducer>;
108
+ }
109
+ /** The subscription type, as pulsar-client names it (`Key_Shared` is accepted as an alias of `KeyShared`). */
110
+ type PulsarSubscriptionType = "Exclusive" | "Shared" | "Failover" | "KeyShared" | "Key_Shared";
86
111
  /**
87
112
  * Project the envelope's contract fields onto native Pulsar message properties
88
113
  * (string→string): `bq-job` = URN, `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus
@@ -153,26 +178,129 @@ interface PulsarConsumerOptions {
153
178
  * rejects with) is swallowed — it never changes how the message is handled or acknowledged.
154
179
  */
155
180
  onRoutingMismatch?: (propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope) => unknown;
181
+ /**
182
+ * The total number of deliveries a message gets, the first included (default 3; `1` = no retry;
183
+ * ADR-0036 decision 1). A failing handler, an unmapped URN without {@link onUnknownUrn} and a poison
184
+ * body with dead-lettering off are retried (`negativeAcknowledge`) while `reconciled attempts + 1`
185
+ * is below it; the delivery that reaches it is dead-lettered (when {@link deadLetterProducer} is
186
+ * set) or acknowledged and dropped. Must be an integer of at least 1.
187
+ */
188
+ maxAttempts?: number;
189
+ /**
190
+ * A producer on the dead-letter topic (by convention `<queue>.dlq`). Dead-lettering is on exactly
191
+ * when it is set; the SDK does not use Pulsar's native `DeadLetterPolicy`. The original message is
192
+ * acknowledged only after the record was sent; a failed send leaves it negatively acknowledged so
193
+ * it is redelivered and the write is tried again.
194
+ */
195
+ deadLetterProducer?: PulsarProducer;
196
+ /**
197
+ * A producer on the consumer's **own** topic. A retry re-publishes the message to that topic with
198
+ * `bq-attempts` (and the body's `attempts`) raised to the counted value and then acknowledges the
199
+ * original — the Go model. The count then lives in the message itself, so the bound holds on every
200
+ * subscription type. Optional when {@link client} is given (the consumer builds the producer itself);
201
+ * a failed re-publish leaves the original negatively acknowledged (never lost).
202
+ *
203
+ * Without a producer or a client a retry is a `negativeAcknowledge`, which advances the native
204
+ * redelivery count only on `Shared` / `Key_Shared`: on `Exclusive` / `Failover` it stays 0 and the retry
205
+ * would never reach `maxAttempts`, so that combination is rejected at construction (see
206
+ * {@link subscriptionType}).
207
+ */
208
+ retryProducer?: PulsarProducer;
209
+ /**
210
+ * The Pulsar client the consumer builds its retry producer from, lazily on the first retry, on
211
+ * {@link topic}. Needs `topic` (pulsar-client's `Consumer` cannot tell its topic). Ignored when
212
+ * {@link retryProducer} is given.
213
+ */
214
+ client?: PulsarClientLike;
215
+ /**
216
+ * The topic the retry producer is created on — the topic the consumer reads. Required together with
217
+ * {@link client} (a pulsar-client `Consumer` has no `getTopic()`); a subscription wrapper that provides
218
+ * `getTopic()` makes it optional.
219
+ */
220
+ topic?: string;
221
+ /**
222
+ * The subscription type the consumer was created with. `Exclusive` and `Failover` cannot bound a
223
+ * nack-driven retry (a negative acknowledgement does not advance the redelivery count there), so with
224
+ * `maxAttempts > 1` they require a {@link retryProducer} or a {@link client}. **Not declaring it counts as
225
+ * `Exclusive`** (pulsar-client's own default, and the fail-closed choice): without a `retryProducer` or a
226
+ * `client` the constructor throws `BabelQueueError`. Declare `"Shared"` or `"Key_Shared"` to keep the
227
+ * `negativeAcknowledge` retry without a producer.
228
+ */
229
+ subscriptionType?: PulsarSubscriptionType;
230
+ /**
231
+ * The queue name written to `dead_letter.original_queue` of a poison record (a decodable envelope
232
+ * uses its own `meta.queue`). Defaults to the {@link deadLetterProducer} topic without its `.dlq`
233
+ * suffix.
234
+ */
235
+ queue?: string;
236
+ /** Receives the "message dropped" and "dead-letter write failed" warnings (default `console.warn`). */
237
+ onWarn?: (message: string) => void;
156
238
  }
239
+ /** Default total number of deliveries (ADR-0036 decision 1). */
240
+ declare const DEFAULT_MAX_ATTEMPTS = 3;
157
241
  /**
158
242
  * Receives from a Pulsar subscription, decodes + validates each message, routes it to the
159
243
  * handler for its URN, and acknowledges it on success. A throwing handler
160
244
  * `negativeAcknowledge`s the message — the broker redelivers it and increments
161
- * `getRedeliveryCount()` (at-least-once). Routing uses the `bq-job` property (body URN as the
162
- * fallback); `attempts` is the `bq-attempts` property (body value as the fallback) reconciled to
163
- * `max(.., getRedeliveryCount())` for the handler.
245
+ * `getRedeliveryCount()` (at-least-once; with a `retryProducer` the message is instead re-published with
246
+ * `bq-attempts` + 1, which also bounds `Exclusive` / `Failover` subscriptions) — until `maxAttempts` deliveries are used up; the last one is
247
+ * dead-lettered to `<queue>.dlq` or acknowledged and dropped (ADR-0036). Routing uses the `bq-job`
248
+ * property (body URN as the fallback); `attempts` is the `bq-attempts` property (body value as the
249
+ * fallback) reconciled to `max(.., getRedeliveryCount())` for the handler.
164
250
  */
165
251
  declare class PulsarConsumer {
166
252
  private readonly consumer;
167
253
  private readonly handlers;
168
254
  private readonly options;
255
+ private readonly maxAttempts;
256
+ private builtRetryProducer;
257
+ private readonly warn;
169
258
  constructor(consumer: PulsarConsumerClient, handlers: BabelHandlers, options?: PulsarConsumerOptions);
259
+ /**
260
+ * Setup check: on `Exclusive` / `Failover` (and an undeclared type) a negative acknowledgement does not advance the native
261
+ * redelivery count, so a retry needs a re-publish. Fail loudly here instead of looping at run time.
262
+ */
263
+ private checkRetryBound;
264
+ private retryTopic;
265
+ /** The retry producer: the one given, else one built (once) from the client; undefined when neither exists. */
266
+ private resolveRetryProducer;
170
267
  /** Receive one message (up to the receive timeout), route + settle it. Returns 1, or 0 on timeout. */
171
268
  poll(): Promise<number>;
172
269
  /** Poll until `signal` aborts. */
173
270
  run(signal?: AbortSignal): Promise<void>;
174
271
  private handle;
272
+ /**
273
+ * Settle a version-gated delivery on its first delivery (ADR-0037 decision 1, broker-bindings §5.7): it is
274
+ * never decoded, never reaches a handler or {@link PulsarConsumerOptions.onUnknownUrn}, and is never retried,
275
+ * whatever `bq-attempts`, the redelivery count or `maxAttempts` say. This consumer has no unknown-URN
276
+ * strategy option, so it behaves as `fail`: with a {@link PulsarConsumerOptions.deadLetterProducer} the body is
277
+ * written to `<queue>.dlq` exactly as received (no `dead_letter` block, no re-encoding) and acknowledged only
278
+ * after the write; without one it is acknowledged and dropped (logged). A failed write leaves the delivery
279
+ * negatively acknowledged (never acknowledged, never counted as a retry): the gate rejects the redelivery
280
+ * again, so the write is tried again.
281
+ */
282
+ private settleVersionGated;
283
+ /**
284
+ * The settle decision of a retryable outcome (ADR-0036 decisions 1 and 2). `counted` is the reconciled
285
+ * attempts plus one for this delivery. A poison body with dead-lettering on is recorded without retry;
286
+ * every other outcome retries (`negativeAcknowledge`: the native redelivery count advances, so the
287
+ * next delivery counts one more) while `counted < maxAttempts`, then dead-letters or drops.
288
+ */
289
+ private settle;
290
+ /**
291
+ * Retry one delivery. With a {@link PulsarConsumerOptions.retryProducer}: re-publish to the consumer's
292
+ * own topic carrying `bq-attempts = counted` (reconcile = max(bq-attempts, RedeliveryCount) on the next
293
+ * delivery), then acknowledge the original — only after the send succeeded. Otherwise `negativeAcknowledge`.
294
+ */
295
+ private retry;
296
+ private retryMessage;
297
+ /**
298
+ * Write the DLQ record, then acknowledge the original — only after the send succeeded. A failed send
299
+ * leaves the original negatively acknowledged, so it is redelivered and the write is tried again.
300
+ */
301
+ private deadLetter;
302
+ private deadLetterMessage;
175
303
  private notifyRoutingMismatch;
176
304
  }
177
305
 
178
- export { type BabelHandler, type BabelHandlers, type PublishOptions, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, headersOf, pulsarProperties, toPulsarMessage };
306
+ export { type BabelHandler, type BabelHandlers, DEFAULT_MAX_ATTEMPTS, type PublishOptions, type PulsarClientLike, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, type PulsarSubscriptionType, headersOf, pulsarProperties, toPulsarMessage };
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
20
20
  * });
21
21
  * const consumer = new PulsarConsumer(sub, {
22
22
  * "urn:babel:orders:created": async (env) => { ... },
23
- * });
23
+ * }, { subscriptionType: "Shared" }); // declare it (or pass `client` together with `topic`): see PulsarConsumerOptions
24
24
  * await consumer.run();
25
25
  *
26
26
  * This implements §5 of the broker-bindings contract: the canonical envelope is the message
@@ -28,7 +28,12 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
28
28
  * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /
29
29
  * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);
30
30
  * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is
31
- * redelivered (at-least-once) and `getRedeliveryCount()` is incremented. Per §5.2 / §5.7 the
31
+ * redelivered (at-least-once) and `getRedeliveryCount()` is incremented — up to `maxAttempts`
32
+ * deliveries (default 3, ADR-0036): the message that exhausts them is written to `<queue>.dlq`
33
+ * (when a `deadLetterProducer` is given) or acknowledged and dropped. A body that does not decode
34
+ * into an envelope (including an object with no valid identity) is poison: it never reaches a
35
+ * handler, and with dead-lettering on it is recorded in `<queue>.dlq` in the ADR-0035 shape without
36
+ * retry. Per §5.2 / §5.7 the
32
37
  * consumer routes on the `bq-job` property (the body's URN is used only when the property is
33
38
  * absent or blank) and takes `attempts` from the authoritative `bq-attempts` property (the
34
39
  * body's value is used only when the property is absent or not a plain ASCII-decimal integer
@@ -37,9 +42,12 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
37
42
  *
38
43
  * **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads the
39
44
  * `bq-schema-version` property. A missing or blank value (empty or only ASCII whitespace) changes nothing; any value other than
40
- * the core's supported `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) rejects the message undecoded
41
- * through the same path as a non-conformant envelope — `onError` (with an error naming the
42
- * version, and an empty envelope since nothing was decoded) and a `negativeAcknowledge`.
45
+ * the core's supported `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) rejects the message undecoded:
46
+ * `onError` (with an error naming the version, and an empty envelope since nothing was decoded),
47
+ * then it is settled on its first delivery (ADR-0037 decision 1) — with a `deadLetterProducer` the
48
+ * body, byte for byte, goes to `<queue>.dlq` and the message is acknowledged after the write (a failed
49
+ * write is a `negativeAcknowledge`, never an acknowledgement); without one it is acknowledged and
50
+ * dropped (logged). Never retried, whatever `bq-attempts`, the redelivery count or `maxAttempts` say.
43
51
  *
44
52
  * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
45
53
  * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`
@@ -79,10 +87,27 @@ interface PulsarReceivedMessage {
79
87
  * satisfies it structurally; a fake satisfies it in tests.
80
88
  */
81
89
  interface PulsarConsumerClient {
90
+ /**
91
+ * The topic the subscription reads. pulsar-client's `Consumer` has **no** such method (only its `Producer`
92
+ * does), so with it the topic comes from the {@link PulsarConsumerOptions.topic} option; a custom
93
+ * subscription wrapper may provide this to make `topic` unnecessary.
94
+ */
95
+ getTopic?(): string;
82
96
  receive(timeoutMs?: number): Promise<PulsarReceivedMessage>;
83
97
  acknowledge(message: PulsarReceivedMessage): Promise<void> | void;
84
98
  negativeAcknowledge(message: PulsarReceivedMessage): Promise<void> | void;
85
99
  }
100
+ /**
101
+ * The subset of the Pulsar client this adapter calls to build its own retry producer. A `Client` from
102
+ * `pulsar-client` satisfies it structurally.
103
+ */
104
+ interface PulsarClientLike {
105
+ createProducer(config: {
106
+ topic: string;
107
+ }): Promise<PulsarProducer>;
108
+ }
109
+ /** The subscription type, as pulsar-client names it (`Key_Shared` is accepted as an alias of `KeyShared`). */
110
+ type PulsarSubscriptionType = "Exclusive" | "Shared" | "Failover" | "KeyShared" | "Key_Shared";
86
111
  /**
87
112
  * Project the envelope's contract fields onto native Pulsar message properties
88
113
  * (string→string): `bq-job` = URN, `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus
@@ -153,26 +178,129 @@ interface PulsarConsumerOptions {
153
178
  * rejects with) is swallowed — it never changes how the message is handled or acknowledged.
154
179
  */
155
180
  onRoutingMismatch?: (propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope) => unknown;
181
+ /**
182
+ * The total number of deliveries a message gets, the first included (default 3; `1` = no retry;
183
+ * ADR-0036 decision 1). A failing handler, an unmapped URN without {@link onUnknownUrn} and a poison
184
+ * body with dead-lettering off are retried (`negativeAcknowledge`) while `reconciled attempts + 1`
185
+ * is below it; the delivery that reaches it is dead-lettered (when {@link deadLetterProducer} is
186
+ * set) or acknowledged and dropped. Must be an integer of at least 1.
187
+ */
188
+ maxAttempts?: number;
189
+ /**
190
+ * A producer on the dead-letter topic (by convention `<queue>.dlq`). Dead-lettering is on exactly
191
+ * when it is set; the SDK does not use Pulsar's native `DeadLetterPolicy`. The original message is
192
+ * acknowledged only after the record was sent; a failed send leaves it negatively acknowledged so
193
+ * it is redelivered and the write is tried again.
194
+ */
195
+ deadLetterProducer?: PulsarProducer;
196
+ /**
197
+ * A producer on the consumer's **own** topic. A retry re-publishes the message to that topic with
198
+ * `bq-attempts` (and the body's `attempts`) raised to the counted value and then acknowledges the
199
+ * original — the Go model. The count then lives in the message itself, so the bound holds on every
200
+ * subscription type. Optional when {@link client} is given (the consumer builds the producer itself);
201
+ * a failed re-publish leaves the original negatively acknowledged (never lost).
202
+ *
203
+ * Without a producer or a client a retry is a `negativeAcknowledge`, which advances the native
204
+ * redelivery count only on `Shared` / `Key_Shared`: on `Exclusive` / `Failover` it stays 0 and the retry
205
+ * would never reach `maxAttempts`, so that combination is rejected at construction (see
206
+ * {@link subscriptionType}).
207
+ */
208
+ retryProducer?: PulsarProducer;
209
+ /**
210
+ * The Pulsar client the consumer builds its retry producer from, lazily on the first retry, on
211
+ * {@link topic}. Needs `topic` (pulsar-client's `Consumer` cannot tell its topic). Ignored when
212
+ * {@link retryProducer} is given.
213
+ */
214
+ client?: PulsarClientLike;
215
+ /**
216
+ * The topic the retry producer is created on — the topic the consumer reads. Required together with
217
+ * {@link client} (a pulsar-client `Consumer` has no `getTopic()`); a subscription wrapper that provides
218
+ * `getTopic()` makes it optional.
219
+ */
220
+ topic?: string;
221
+ /**
222
+ * The subscription type the consumer was created with. `Exclusive` and `Failover` cannot bound a
223
+ * nack-driven retry (a negative acknowledgement does not advance the redelivery count there), so with
224
+ * `maxAttempts > 1` they require a {@link retryProducer} or a {@link client}. **Not declaring it counts as
225
+ * `Exclusive`** (pulsar-client's own default, and the fail-closed choice): without a `retryProducer` or a
226
+ * `client` the constructor throws `BabelQueueError`. Declare `"Shared"` or `"Key_Shared"` to keep the
227
+ * `negativeAcknowledge` retry without a producer.
228
+ */
229
+ subscriptionType?: PulsarSubscriptionType;
230
+ /**
231
+ * The queue name written to `dead_letter.original_queue` of a poison record (a decodable envelope
232
+ * uses its own `meta.queue`). Defaults to the {@link deadLetterProducer} topic without its `.dlq`
233
+ * suffix.
234
+ */
235
+ queue?: string;
236
+ /** Receives the "message dropped" and "dead-letter write failed" warnings (default `console.warn`). */
237
+ onWarn?: (message: string) => void;
156
238
  }
239
+ /** Default total number of deliveries (ADR-0036 decision 1). */
240
+ declare const DEFAULT_MAX_ATTEMPTS = 3;
157
241
  /**
158
242
  * Receives from a Pulsar subscription, decodes + validates each message, routes it to the
159
243
  * handler for its URN, and acknowledges it on success. A throwing handler
160
244
  * `negativeAcknowledge`s the message — the broker redelivers it and increments
161
- * `getRedeliveryCount()` (at-least-once). Routing uses the `bq-job` property (body URN as the
162
- * fallback); `attempts` is the `bq-attempts` property (body value as the fallback) reconciled to
163
- * `max(.., getRedeliveryCount())` for the handler.
245
+ * `getRedeliveryCount()` (at-least-once; with a `retryProducer` the message is instead re-published with
246
+ * `bq-attempts` + 1, which also bounds `Exclusive` / `Failover` subscriptions) — until `maxAttempts` deliveries are used up; the last one is
247
+ * dead-lettered to `<queue>.dlq` or acknowledged and dropped (ADR-0036). Routing uses the `bq-job`
248
+ * property (body URN as the fallback); `attempts` is the `bq-attempts` property (body value as the
249
+ * fallback) reconciled to `max(.., getRedeliveryCount())` for the handler.
164
250
  */
165
251
  declare class PulsarConsumer {
166
252
  private readonly consumer;
167
253
  private readonly handlers;
168
254
  private readonly options;
255
+ private readonly maxAttempts;
256
+ private builtRetryProducer;
257
+ private readonly warn;
169
258
  constructor(consumer: PulsarConsumerClient, handlers: BabelHandlers, options?: PulsarConsumerOptions);
259
+ /**
260
+ * Setup check: on `Exclusive` / `Failover` (and an undeclared type) a negative acknowledgement does not advance the native
261
+ * redelivery count, so a retry needs a re-publish. Fail loudly here instead of looping at run time.
262
+ */
263
+ private checkRetryBound;
264
+ private retryTopic;
265
+ /** The retry producer: the one given, else one built (once) from the client; undefined when neither exists. */
266
+ private resolveRetryProducer;
170
267
  /** Receive one message (up to the receive timeout), route + settle it. Returns 1, or 0 on timeout. */
171
268
  poll(): Promise<number>;
172
269
  /** Poll until `signal` aborts. */
173
270
  run(signal?: AbortSignal): Promise<void>;
174
271
  private handle;
272
+ /**
273
+ * Settle a version-gated delivery on its first delivery (ADR-0037 decision 1, broker-bindings §5.7): it is
274
+ * never decoded, never reaches a handler or {@link PulsarConsumerOptions.onUnknownUrn}, and is never retried,
275
+ * whatever `bq-attempts`, the redelivery count or `maxAttempts` say. This consumer has no unknown-URN
276
+ * strategy option, so it behaves as `fail`: with a {@link PulsarConsumerOptions.deadLetterProducer} the body is
277
+ * written to `<queue>.dlq` exactly as received (no `dead_letter` block, no re-encoding) and acknowledged only
278
+ * after the write; without one it is acknowledged and dropped (logged). A failed write leaves the delivery
279
+ * negatively acknowledged (never acknowledged, never counted as a retry): the gate rejects the redelivery
280
+ * again, so the write is tried again.
281
+ */
282
+ private settleVersionGated;
283
+ /**
284
+ * The settle decision of a retryable outcome (ADR-0036 decisions 1 and 2). `counted` is the reconciled
285
+ * attempts plus one for this delivery. A poison body with dead-lettering on is recorded without retry;
286
+ * every other outcome retries (`negativeAcknowledge`: the native redelivery count advances, so the
287
+ * next delivery counts one more) while `counted < maxAttempts`, then dead-letters or drops.
288
+ */
289
+ private settle;
290
+ /**
291
+ * Retry one delivery. With a {@link PulsarConsumerOptions.retryProducer}: re-publish to the consumer's
292
+ * own topic carrying `bq-attempts = counted` (reconcile = max(bq-attempts, RedeliveryCount) on the next
293
+ * delivery), then acknowledge the original — only after the send succeeded. Otherwise `negativeAcknowledge`.
294
+ */
295
+ private retry;
296
+ private retryMessage;
297
+ /**
298
+ * Write the DLQ record, then acknowledge the original — only after the send succeeded. A failed send
299
+ * leaves the original negatively acknowledged, so it is redelivered and the write is tried again.
300
+ */
301
+ private deadLetter;
302
+ private deadLetterMessage;
175
303
  private notifyRoutingMismatch;
176
304
  }
177
305
 
178
- export { type BabelHandler, type BabelHandlers, type PublishOptions, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, headersOf, pulsarProperties, toPulsarMessage };
306
+ export { type BabelHandler, type BabelHandlers, DEFAULT_MAX_ATTEMPTS, type PublishOptions, type PulsarClientLike, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, type PulsarSubscriptionType, headersOf, pulsarProperties, toPulsarMessage };