@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/README.md CHANGED
@@ -42,7 +42,12 @@ const consumer = new PulsarConsumer(
42
42
  console.log(env.data.order_id, env.trace_id, env.attempts);
43
43
  },
44
44
  },
45
- { onError: (err) => console.error(err) },
45
+ {
46
+ // Declare the subscription type (or pass `client` together with `topic`, the topic you subscribed to): an undeclared type counts as Exclusive and
47
+ // the constructor throws, see "Exclusive / Failover subscriptions" below.
48
+ subscriptionType: "Shared",
49
+ onError: (err) => console.error(err),
50
+ },
46
51
  );
47
52
  await consumer.run();
48
53
  ```
@@ -75,10 +80,15 @@ converge on the same number.
75
80
 
76
81
  **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads
77
82
  `bq-schema-version`. Missing or blank: nothing changes ("blank" is the same set in every SDK: the empty string or only ASCII whitespace — space, `\t`, `\n`, `\v`, `\f`, `\r`; NBSP, U+0085, U+FEFF, U+3000 and the like are not blank). Any value other than the core's supported
78
- `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) rejects the message **undecoded** through the same
79
- path as a non-conformant envelope — `onError` (an error naming the version, with an empty
80
- envelope) and a `negativeAcknowledge`; the handler, `onUnknownUrn` and `onRoutingMismatch` are
81
- not called.
83
+ `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) rejects the message **undecoded**: `onError` (an error
84
+ naming the version, with an empty envelope), then it is settled on its **first delivery**
85
+ (ADR-0037 decision 1) — with a `deadLetterProducer` the body is written to `<queue>.dlq` exactly as
86
+ received (no `dead_letter` block, no re-encoding) and acknowledged only after the write; without one
87
+ it is acknowledged and dropped (a warning goes to `onWarn`). A failed write leaves the message
88
+ negatively acknowledged (never acknowledged, never counted as a retry), so the write is tried again
89
+ on the redelivery. It is never retried, whatever `bq-attempts`, the redelivery count or `maxAttempts`
90
+ say; the handler, `onUnknownUrn` and `onRoutingMismatch` are not called. This consumer has no
91
+ unknown-URN strategy option, so it behaves as `fail`.
82
92
 
83
93
  If the `bq-job` property and the body's URN differ, routing follows the property and the
84
94
  optional `onRoutingMismatch(propertyUrn, bodyUrn, envelope)` hook is called (default: no-op).
@@ -86,9 +96,80 @@ It is separate from `onError`, its failures (including a rejected promise) are s
86
96
  it never changes the ack flow. The hook runs only for an accepted envelope — after `accepts()` and
87
97
  the `attempts` reconcile — so it sees the same envelope the handler gets. A body without a `job` is still rejected by the codec even when
88
98
  `bq-job` is set. A throwing handler `negativeAcknowledge`s, so
89
- the broker redelivers (at-least-once). The poll loop never stops on a bad message — observe
90
- via `onError` / `onUnknownUrn`. The envelope is unchanged (`schema_version` stays `1`);
91
- Pulsar is purely additive.
99
+ the broker redelivers (at-least-once) — up to `maxAttempts` deliveries (see below). The poll loop
100
+ never stops on a bad message — observe via `onError` / `onUnknownUrn`. The envelope is unchanged
101
+ (`schema_version` stays `1`); Pulsar is purely additive.
102
+
103
+ ### Max attempts, dead-letter and poison (ADR-0036)
104
+
105
+ Redelivery is bounded, so every message terminates. `maxAttempts` (default **3**, an integer of at
106
+ least 1) is the total number of deliveries a message gets, the first included; `1` means no retry.
107
+ The SDK counts `counted = reconciled attempts + 1` (the reconciled value above plus the current
108
+ delivery) and decides for a retryable outcome — a handler that throws, an unmapped URN without an
109
+ `onUnknownUrn` hook, or a poison body with dead-lettering off:
110
+
111
+ - `counted < maxAttempts` → **retry**: with a `retryProducer`, re-publish to the same topic with
112
+ `bq-attempts` (and the body's `attempts`) set to `counted`, then acknowledge (the Go model);
113
+ without a producer or client, `negativeAcknowledge` (the native redelivery count advances on
114
+ `Shared` / `Key_Shared` only; see Exclusive / Failover below).
115
+ - otherwise → **dead-letter** when a `deadLetterProducer` is given, else **drop** (acknowledge, and
116
+ a warning through `onWarn`, default `console.warn`).
117
+
118
+ ```ts
119
+ const dlq = await client.createProducer({ topic: "orders.dlq" });
120
+ const consumer = new PulsarConsumer(sub, handlers, {
121
+ maxAttempts: 3, // default
122
+ deadLetterProducer: dlq, // dead-lettering is on exactly when this is set
123
+ client, // lets the consumer build its own retry producer; see Exclusive / Failover below
124
+ topic: "orders", // ... on the topic you subscribed to (pulsar-client's Consumer has no getTopic())
125
+ queue: "orders", // dead_letter.original_queue of a poison record (default: the DLQ topic without ".dlq")
126
+ });
127
+ ```
128
+
129
+ The dead-letter record is the canonical envelope with the `dead_letter` block (`reason` `failed` or
130
+ `unknown_urn`; `dead_letter.attempts` and the record's `attempts` are the counted value, `3` with the
131
+ default). The original is acknowledged **only after** the record was sent; a failed send leaves it
132
+ negatively acknowledged, so it is redelivered and the write is tried again. The SDK does not use
133
+ Pulsar's native `DeadLetterPolicy`; adding one yourself creates a second, uncoordinated limit.
134
+
135
+ **Poison.** A body that does not decode into an envelope — not JSON, not an object, or an object with
136
+ neither a valid `job` nor a valid `urn`, or any other body the codec does not accept as a conformant envelope (for example a missing `trace_id` or a non-integer `attempts`) — never reaches a handler or `onUnknownUrn`. With
137
+ `deadLetterProducer` it is written to the dead-letter topic at once, without retry, in the ADR-0035
138
+ shape (a strict JSON object keeps every original byte and gains a `dead_letter` block with
139
+ `reason: "poison"`; anything else is wrapped as `{"raw", "raw_encoding"?, "dead_letter"}`). Without
140
+ one it takes the bounded retry above and is then dropped.
141
+
142
+ **Exclusive / Failover subscriptions.** A negative acknowledgement advances the native redelivery
143
+ count only on `Shared` / `Key_Shared`; on `Exclusive` / `Failover` (pulsar-client's default is
144
+ `Exclusive`) the count stays 0, so a nack-driven retry would never reach `maxAttempts`. The retry is
145
+ therefore a **re-publish** to the consumer's own topic with `bq-attempts` + 1 and an acknowledge, which
146
+ carries the count in the message itself (reconciled as `max(bq-attempts, redeliveryCount)`) and bounds
147
+ every subscription type. Give the consumer the means to publish:
148
+
149
+ ```ts
150
+ const consumer = new PulsarConsumer(sub, handlers, {
151
+ client, // the pulsar-client Client: the consumer builds the retry producer itself,
152
+ // lazily on the first retry
153
+ topic: "orders", // the topic you subscribed to (pulsar-client's Consumer has no getTopic())
154
+ subscriptionType: "Exclusive", // or "Shared" / "Key_Shared" (not declared = Exclusive, see below)
155
+ });
156
+ // or pass your own producer on the topic you read: { retryProducer: await client.createProducer({ topic }) }
157
+ ```
158
+
159
+ A given `retryProducer` wins over `client`. The constructor **fails closed**: with `maxAttempts > 1`
160
+ and neither a `retryProducer` nor a `client` (with its `topic`), `subscriptionType` `Exclusive`,
161
+ `Failover` **or not declared** (it then counts as `Exclusive`, pulsar-client's default) throws a
162
+ `BabelQueueError`. Declare `subscriptionType: "Shared"` or `"Key_Shared"` to keep the
163
+ `negativeAcknowledge` retry without a producer. A poison body is re-published byte for byte, only
164
+ `bq-attempts` moves; out-of-band headers (e.g. `traceparent`) ride along, and the incoming `bq-job`
165
+ route is kept on the retry and on the DLQ record so every delivery reaches the same handler. A failed
166
+ re-publish or producer creation leaves the original negatively acknowledged and is reported through
167
+ `onWarn`.
168
+
169
+ **Behaviour change (MINOR).** Redelivery used to be unbounded; with the defaults a message that keeps
170
+ failing is now dropped (or dead-lettered) at its third delivery. Set `maxAttempts` higher to keep
171
+ more retries. A message with an unsupported `bq-schema-version` takes the version gate above, not this
172
+ retry path.
92
173
 
93
174
  The producer/consumer are injected, so the unit tests use fakes — no Pulsar, no broker. Dual
94
175
  ESM + CJS.
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ DEFAULT_MAX_ATTEMPTS: () => DEFAULT_MAX_ATTEMPTS,
23
24
  PulsarConsumer: () => PulsarConsumer,
24
25
  PulsarPublisher: () => PulsarPublisher,
25
26
  headersOf: () => headersOf,
@@ -105,12 +106,19 @@ var PulsarPublisher = class {
105
106
  return envelope.meta.id;
106
107
  }
107
108
  };
109
+ var DEFAULT_MAX_ATTEMPTS = 3;
108
110
  var MAX_ATTEMPTS = 2 ** 31 - 1;
109
111
  function parseAttempts(raw) {
110
112
  if (typeof raw !== "string" || !/^[0-9]+$/.test(raw)) return void 0;
111
113
  const value = Number(raw);
112
114
  return value <= MAX_ATTEMPTS ? value : void 0;
113
115
  }
116
+ function reconcileAttempts(properties, bodyAttempts, redeliveryCount) {
117
+ let attempts = bodyAttempts;
118
+ const fromProperty = parseAttempts(properties["bq-attempts"]);
119
+ if (fromProperty !== void 0) attempts = fromProperty;
120
+ return redeliveryCount > attempts ? redeliveryCount : attempts;
121
+ }
114
122
  var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
115
123
  function unsupportedSchemaVersion(properties) {
116
124
  const raw = properties["bq-schema-version"];
@@ -129,10 +137,62 @@ var PulsarConsumer = class {
129
137
  this.consumer = consumer;
130
138
  this.handlers = handlers;
131
139
  this.options = options;
140
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
141
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
142
+ throw new import_core.BabelQueueError(`maxAttempts must be an integer of at least 1, got ${String(maxAttempts)}.`);
143
+ }
144
+ this.maxAttempts = maxAttempts;
145
+ this.checkRetryBound(maxAttempts);
146
+ this.warn = options.onWarn ?? ((message) => console.warn(message));
132
147
  }
133
148
  consumer;
134
149
  handlers;
135
150
  options;
151
+ maxAttempts;
152
+ builtRetryProducer;
153
+ warn;
154
+ /**
155
+ * Setup check: on `Exclusive` / `Failover` (and an undeclared type) a negative acknowledgement does not advance the native
156
+ * redelivery count, so a retry needs a re-publish. Fail loudly here instead of looping at run time.
157
+ */
158
+ checkRetryBound(maxAttempts) {
159
+ const declared = this.options.subscriptionType;
160
+ if (declared !== void 0 && !["Exclusive", "Shared", "Failover", "KeyShared"].includes(declared.replace("_", ""))) {
161
+ throw new import_core.BabelQueueError(`Unknown subscriptionType "${String(declared)}".`);
162
+ }
163
+ const type = declared === void 0 ? "Exclusive" : declared.replace("_", "");
164
+ if (type !== "Exclusive" && type !== "Failover") return;
165
+ if (maxAttempts === 1 || this.options.retryProducer !== void 0) return;
166
+ if (this.options.client !== void 0 && this.retryTopic() !== void 0) return;
167
+ const shown = declared === void 0 ? "An undeclared subscriptionType (treated as Exclusive)" : `A ${type} subscription`;
168
+ if (this.options.client !== void 0) {
169
+ throw new import_core.BabelQueueError(
170
+ `${shown} with a client needs the \`topic\` option (the topic this consumer reads) to build the retry producer: pulsar-client's Consumer cannot report its own topic.`
171
+ );
172
+ }
173
+ throw new import_core.BabelQueueError(
174
+ `${shown} needs a retryProducer (or a client plus a topic) to bound retries: a negative acknowledgement does not advance the redelivery count there, so a retry would never reach maxAttempts. Pass a client plus topic (the topic you subscribed to), or a retryProducer, or declare subscriptionType: "Shared" / "Key_Shared".`
175
+ );
176
+ }
177
+ retryTopic() {
178
+ const configured = this.options.topic;
179
+ if (configured !== void 0 && configured !== "") return configured;
180
+ const own = typeof this.consumer.getTopic === "function" ? this.consumer.getTopic() : void 0;
181
+ return own !== void 0 && own !== "" ? own : void 0;
182
+ }
183
+ /** The retry producer: the one given, else one built (once) from the client; undefined when neither exists. */
184
+ async resolveRetryProducer() {
185
+ if (this.options.retryProducer !== void 0) return this.options.retryProducer;
186
+ const client = this.options.client;
187
+ const topic = this.retryTopic();
188
+ if (client === void 0 || topic === void 0) return void 0;
189
+ this.builtRetryProducer ??= client.createProducer({ topic }).catch((error) => {
190
+ this.builtRetryProducer = void 0;
191
+ this.warn(`babelqueue: creating the retry producer on ${topic} failed (${errorText(error)}).`);
192
+ return void 0;
193
+ });
194
+ return this.builtRetryProducer;
195
+ }
136
196
  /** Receive one message (up to the receive timeout), route + settle it. Returns 1, or 0 on timeout. */
137
197
  async poll() {
138
198
  let message;
@@ -162,32 +222,26 @@ var PulsarConsumer = class {
162
222
  {},
163
223
  message
164
224
  );
165
- await this.consumer.negativeAcknowledge(message);
225
+ await this.settleVersionGated(message);
166
226
  return;
167
227
  }
168
- const envelope = import_core.EnvelopeCodec.decode(decodePayload(message.getData()));
169
228
  const redeliveryCount = typeof message.getRedeliveryCount === "function" ? message.getRedeliveryCount() : 0;
170
- const bodyAttempts = envelope.attempts;
171
- let attempts = typeof bodyAttempts === "number" && Number.isInteger(bodyAttempts) ? bodyAttempts : 0;
172
- const fromProperty = parseAttempts(properties["bq-attempts"]);
173
- if (fromProperty !== void 0) attempts = fromProperty;
174
- if (redeliveryCount > attempts) attempts = redeliveryCount;
175
- if (typeof bodyAttempts === "number" && Number.isInteger(bodyAttempts) && bodyAttempts !== attempts) {
176
- envelope.attempts = attempts;
177
- }
229
+ const envelope = import_core.EnvelopeCodec.decode(decodePayload(message.getData()));
178
230
  const bodyUrn = import_core.EnvelopeCodec.urn(envelope);
179
231
  const rawJob = properties["bq-job"];
180
232
  const propertyUrn = typeof rawJob === "string" ? rawJob.trim() : "";
181
233
  const urn = propertyUrn !== "" ? propertyUrn : bodyUrn;
182
234
  if (!import_core.EnvelopeCodec.accepts(envelope)) {
183
- this.options.onError?.(
184
- new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Apache Pulsar."),
185
- envelope,
186
- message
187
- );
188
- await this.consumer.negativeAcknowledge(message);
235
+ const error = new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Apache Pulsar.");
236
+ this.options.onError?.(error, envelope, message);
237
+ await this.settle("poison", reconcileAttempts(properties, 0, redeliveryCount) + 1, null, message, error);
189
238
  return;
190
239
  }
240
+ const bodyAttempts = envelope.attempts;
241
+ const attempts = reconcileAttempts(properties, bodyAttempts, redeliveryCount);
242
+ if (bodyAttempts !== attempts) {
243
+ envelope.attempts = attempts;
244
+ }
191
245
  if (propertyUrn !== "" && bodyUrn !== "" && propertyUrn !== bodyUrn) {
192
246
  this.notifyRoutingMismatch(propertyUrn, bodyUrn, envelope);
193
247
  }
@@ -197,8 +251,9 @@ var PulsarConsumer = class {
197
251
  await this.options.onUnknownUrn(envelope, message);
198
252
  await this.consumer.acknowledge(message);
199
253
  } else {
200
- this.options.onError?.(new import_core.UnknownUrnError(urn), envelope, message);
201
- await this.consumer.negativeAcknowledge(message);
254
+ const error = new import_core.UnknownUrnError(urn);
255
+ this.options.onError?.(error, envelope, message);
256
+ await this.settle("unknown_urn", attempts + 1, envelope, message, error);
202
257
  }
203
258
  return;
204
259
  }
@@ -207,8 +262,140 @@ var PulsarConsumer = class {
207
262
  await this.consumer.acknowledge(message);
208
263
  } catch (error) {
209
264
  this.options.onError?.(error, envelope, message);
265
+ await this.settle("failed", attempts + 1, envelope, message, error);
266
+ }
267
+ }
268
+ /**
269
+ * Settle a version-gated delivery on its first delivery (ADR-0037 decision 1, broker-bindings §5.7): it is
270
+ * never decoded, never reaches a handler or {@link PulsarConsumerOptions.onUnknownUrn}, and is never retried,
271
+ * whatever `bq-attempts`, the redelivery count or `maxAttempts` say. This consumer has no unknown-URN
272
+ * strategy option, so it behaves as `fail`: with a {@link PulsarConsumerOptions.deadLetterProducer} the body is
273
+ * written to `<queue>.dlq` exactly as received (no `dead_letter` block, no re-encoding) and acknowledged only
274
+ * after the write; without one it is acknowledged and dropped (logged). A failed write leaves the delivery
275
+ * negatively acknowledged (never acknowledged, never counted as a retry): the gate rejects the redelivery
276
+ * again, so the write is tried again.
277
+ */
278
+ async settleVersionGated(message) {
279
+ const producer = this.options.deadLetterProducer;
280
+ if (producer === void 0) {
281
+ this.warn("babelqueue: dropped a version-gated Apache Pulsar message (dead-lettering is off).");
282
+ await this.consumer.acknowledge(message);
283
+ return;
284
+ }
285
+ try {
286
+ const data = message.getData();
287
+ await producer.send({
288
+ data: typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data),
289
+ properties: { ...safeProperties(message) }
290
+ });
291
+ } catch (writeError) {
292
+ this.warn(
293
+ `babelqueue: dead-letter write of a version-gated message to ${producer.getTopic()} failed (${errorText(writeError)}); the message is left for redelivery.`
294
+ );
210
295
  await this.consumer.negativeAcknowledge(message);
296
+ return;
211
297
  }
298
+ await this.consumer.acknowledge(message);
299
+ }
300
+ /**
301
+ * The settle decision of a retryable outcome (ADR-0036 decisions 1 and 2). `counted` is the reconciled
302
+ * attempts plus one for this delivery. A poison body with dead-lettering on is recorded without retry;
303
+ * every other outcome retries (`negativeAcknowledge`: the native redelivery count advances, so the
304
+ * next delivery counts one more) while `counted < maxAttempts`, then dead-letters or drops.
305
+ */
306
+ async settle(kind, counted, envelope, message, error) {
307
+ const producer = this.options.deadLetterProducer;
308
+ if (kind === "poison" && producer !== void 0) {
309
+ await this.deadLetter(producer, kind, counted, null, message, error);
310
+ return;
311
+ }
312
+ if (counted < this.maxAttempts) {
313
+ await this.retry(counted, envelope, message);
314
+ return;
315
+ }
316
+ if (producer !== void 0) {
317
+ await this.deadLetter(producer, kind, counted, envelope, message, error);
318
+ return;
319
+ }
320
+ this.warn(
321
+ `babelqueue: dropped a ${kind} Apache Pulsar message after ${counted} of ${this.maxAttempts} deliveries (dead-lettering is off).`
322
+ );
323
+ await this.consumer.acknowledge(message);
324
+ }
325
+ /**
326
+ * Retry one delivery. With a {@link PulsarConsumerOptions.retryProducer}: re-publish to the consumer's
327
+ * own topic carrying `bq-attempts = counted` (reconcile = max(bq-attempts, RedeliveryCount) on the next
328
+ * delivery), then acknowledge the original — only after the send succeeded. Otherwise `negativeAcknowledge`.
329
+ */
330
+ async retry(counted, envelope, message) {
331
+ const producer = await this.resolveRetryProducer();
332
+ if (producer === void 0) {
333
+ await this.consumer.negativeAcknowledge(message);
334
+ return;
335
+ }
336
+ try {
337
+ await producer.send(this.retryMessage(counted, envelope, message));
338
+ } catch (writeError) {
339
+ this.warn(
340
+ `babelqueue: retry re-publish to ${producer.getTopic()} failed (${errorText(writeError)}); the message is left for redelivery.`
341
+ );
342
+ await this.consumer.negativeAcknowledge(message);
343
+ return;
344
+ }
345
+ await this.consumer.acknowledge(message);
346
+ }
347
+ retryMessage(counted, envelope, message) {
348
+ const carried = headersOf(message);
349
+ delete carried["bq-delay"];
350
+ if (envelope !== null) {
351
+ return keepRoute(toPulsarMessage({ ...envelope, attempts: counted }, carried), message);
352
+ }
353
+ const data = message.getData();
354
+ return keepRoute(
355
+ {
356
+ data: typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data),
357
+ properties: { ...carried, "bq-attempts": String(counted) }
358
+ },
359
+ message
360
+ );
361
+ }
362
+ /**
363
+ * Write the DLQ record, then acknowledge the original — only after the send succeeded. A failed send
364
+ * leaves the original negatively acknowledged, so it is redelivered and the write is tried again.
365
+ */
366
+ async deadLetter(producer, kind, counted, envelope, message, error) {
367
+ try {
368
+ await producer.send(this.deadLetterMessage(producer, kind, counted, envelope, message, error));
369
+ } catch (writeError) {
370
+ this.warn(
371
+ `babelqueue: dead-letter write to ${producer.getTopic()} failed (${errorText(writeError)}); the message is left for redelivery.`
372
+ );
373
+ await this.consumer.negativeAcknowledge(message);
374
+ return;
375
+ }
376
+ await this.consumer.acknowledge(message);
377
+ }
378
+ deadLetterMessage(producer, kind, counted, envelope, message, error) {
379
+ const details = {
380
+ attempts: counted,
381
+ error: error instanceof Error ? error.message : null,
382
+ exception: error instanceof Error ? error.name : null
383
+ };
384
+ if (envelope === null) {
385
+ const original2 = this.options.queue ?? stripDlqSuffix(topicName(producer.getTopic()));
386
+ const delivered = counted - 1;
387
+ const deadLetter = (0, import_core.annotate)({ attempts: delivered }, kind, original2, {
388
+ ...details,
389
+ attempts: delivered
390
+ }).dead_letter;
391
+ const data = message.getData();
392
+ const body = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
393
+ const record = (0, import_core.buildPoisonRecord)(body, JSON.stringify(deadLetter));
394
+ return keepRoute({ data: Buffer.from(record, "utf8"), properties: poisonProperties(record) }, message);
395
+ }
396
+ const original = envelope.meta?.queue || this.options.queue || stripDlqSuffix(topicName(producer.getTopic()));
397
+ const annotated = (0, import_core.annotate)({ ...envelope, attempts: counted }, kind, original, details);
398
+ return keepRoute(toPulsarMessage(annotated), message);
212
399
  }
213
400
  notifyRoutingMismatch(propertyUrn, bodyUrn, envelope) {
214
401
  const hook = this.options.onRoutingMismatch;
@@ -228,8 +415,42 @@ function safeProperties(message) {
228
415
  return {};
229
416
  }
230
417
  }
418
+ function keepRoute(outgoing, incoming) {
419
+ const route = safeProperties(incoming)["bq-job"];
420
+ if (typeof route === "string" && route.trim() !== "") {
421
+ outgoing.properties = { ...outgoing.properties, "bq-job": route };
422
+ }
423
+ return outgoing;
424
+ }
425
+ function errorText(error) {
426
+ return error instanceof Error ? error.message : String(error);
427
+ }
428
+ function stripDlqSuffix(topic) {
429
+ return topic.endsWith(".dlq") && topic.length > 4 ? topic.slice(0, -4) : topic;
430
+ }
431
+ function poisonProperties(recordJson) {
432
+ const properties = {};
433
+ let record;
434
+ try {
435
+ record = JSON.parse(recordJson);
436
+ } catch {
437
+ return properties;
438
+ }
439
+ if (record === null || typeof record !== "object") return properties;
440
+ const top = record;
441
+ const str = (value) => typeof value === "string" && value !== "" ? value : void 0;
442
+ const job = str(top["job"]) ?? str(top["urn"]);
443
+ if (job) properties["bq-job"] = job;
444
+ const trace = str(top["trace_id"]);
445
+ if (trace) properties["bq-trace-id"] = trace;
446
+ const meta = top["meta"] !== null && typeof top["meta"] === "object" ? top["meta"] : {};
447
+ const id = str(meta["id"]);
448
+ if (id) properties["bq-message-id"] = id;
449
+ return properties;
450
+ }
231
451
  // Annotate the CommonJS export names for ESM import in node:
232
452
  0 && (module.exports = {
453
+ DEFAULT_MAX_ATTEMPTS,
233
454
  PulsarConsumer,
234
455
  PulsarPublisher,
235
456
  headersOf,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache Pulsar adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Apache Pulsar, so a\n * Pulsar-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import Pulsar from \"pulsar-client\";\n * import { PulsarPublisher, PulsarConsumer } from \"@babelqueue/pulsar\";\n *\n * const client = new Pulsar.Client({ serviceUrl: \"pulsar://localhost:6650\" });\n *\n * const producer = await client.createProducer({ topic: \"orders\" });\n * await new PulsarPublisher(producer).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const sub = await client.subscribe({\n * topic: \"orders\", subscription: \"babelqueue\", subscriptionType: \"Shared\",\n * });\n * const consumer = new PulsarConsumer(sub, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §5 of the broker-bindings contract: the canonical envelope is the message\n * payload, projected onto native Pulsar message properties (string→string) — `bq-job` = URN,\n * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /\n * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);\n * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is\n * redelivered (at-least-once) and `getRedeliveryCount()` is incremented. Per §5.2 / §5.7 the\n * consumer routes on the `bq-job` property (the body's URN is used only when the property is\n * absent or blank) and takes `attempts` from the authoritative `bq-attempts` property (the\n * body's value is used only when the property is absent or not a plain ASCII-decimal integer\n * that fits a 32-bit int), reconciled to `max(bq-attempts, getRedeliveryCount())` — no −1,\n * because Pulsar's redelivery count is 0-based. The handler still receives the body's envelope.\n *\n * **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads the\n * `bq-schema-version` property. A missing or blank value (empty or only ASCII whitespace) changes nothing; any value other than\n * the core's supported `SCHEMA_VERSION` (`\"2\"`, `\"x\"`, `\"01\"`, ...) rejects the message undecoded\n * through the same path as a non-conformant envelope — `onError` (with an error naming the\n * version, and an empty envelope since nothing was decoded) and a `negativeAcknowledge`.\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`\n * properties — the contract properties win a key collision (merge-not-clobber), and blanks are\n * dropped. On consume the inbound `getProperties()` are surfaced as a `Record<string,string>` so\n * the core's `otel` extract sees the `traceparent` and links the consumer span as a child. A\n * header-less publish is byte-identical. GR-1: the wire envelope (the payload) is never touched.\n */\n\nimport { BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Pulsar shapes (a structural subset of pulsar-client) --------------\n\n/** A message to send (structural subset of pulsar-client `ProducerMessage`). */\nexport interface PulsarProducerMessage {\n data: Buffer;\n properties?: { [key: string]: string };\n /** Relative delay in milliseconds before the message becomes available (native `deliverAfter`). */\n deliverAfter?: number;\n}\n\n/**\n * The subset of the Pulsar producer this adapter calls. A `Producer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarProducer {\n getTopic(): string;\n send(message: PulsarProducerMessage): Promise<unknown>;\n}\n\n/** A received message (structural subset of pulsar-client `Message`). */\nexport interface PulsarReceivedMessage {\n getData(): Buffer | string;\n getProperties(): { [key: string]: string };\n getRedeliveryCount(): number;\n}\n\n/**\n * The subset of the Pulsar consumer this adapter calls. A `Consumer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarConsumerClient {\n receive(timeoutMs?: number): Promise<PulsarReceivedMessage>;\n acknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n negativeAcknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n}\n\n// --- Property projection (contract §5.2) ---------------------------------------\n\n/** Bare topic name (the part after the last `/`), used as `meta.queue`. */\nfunction topicName(topic: string): string {\n const slash = topic.lastIndexOf(\"/\");\n return slash >= 0 ? topic.slice(slash + 1) : topic;\n}\n\n/**\n * Project the envelope's contract fields onto native Pulsar message properties\n * (string→string): `bq-job` = URN, `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus\n * `bq-schema-version` / `bq-source-lang` / `bq-attempts`. The body stays authoritative.\n */\nexport function pulsarProperties(envelope: Envelope): { [key: string]: string } {\n const properties: { [key: string]: string } = {};\n if (envelope.job) properties[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) properties[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) properties[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) {\n properties[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) properties[\"bq-source-lang\"] = envelope.meta.lang;\n properties[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return properties;\n}\n\n/**\n * Project the envelope onto a native Pulsar producer message (payload + properties). When `extra`\n * is given, those out-of-band headers (e.g. a `traceparent`, ADR-0028) are merged beside the\n * contract `bq-*` properties without clobbering them (the contract wins a key collision).\n */\nexport function toPulsarMessage(envelope: Envelope, extra?: HeaderCarrier | null): PulsarProducerMessage {\n return {\n data: Buffer.from(EnvelopeCodec.encode(envelope), \"utf8\"),\n properties: mergeInto(pulsarProperties(envelope), extra),\n };\n}\n\n/**\n * Read a received message's native properties back into a flat {@link HeaderCarrier} via\n * `getProperties()`. Returns an empty object when there are none. Both the contract `bq-*`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: PulsarReceivedMessage): HeaderCarrier {\n const props = typeof message.getProperties === \"function\" ? message.getProperties() : undefined;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value != null && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link PulsarPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as native Pulsar message properties beside the contract\n * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s\n * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the\n * message unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */\nexport class PulsarPublisher {\n constructor(private readonly producer: PulsarProducer) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the §5 property projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery via `deliverAfter` and mirrors `bq-delay`.\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: topicName(this.producer.getTopic()),\n traceId: options.traceId,\n });\n const message = toPulsarMessage(envelope, options.headers);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.properties = { ...message.properties, \"bq-delay\": String(options.delayMs) };\n message.deliverAfter = options.delayMs;\n }\n\n await this.producer.send(message);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Pulsar message, and the out-of-band\n * {@link HeaderCarrier} read from the message's properties (empty when there are none). Pass\n * `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the\n * producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link PulsarConsumer}. */\nexport interface PulsarConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is acknowledged). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;\n /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;\n /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */\n receiveTimeoutMs?: number;\n /**\n * Optional diagnostic hook, called when the `bq-job` property and the body's URN differ (the\n * property wins and the message is routed on it). `envelope` is the body's envelope. Separate\n * from {@link PulsarConsumerOptions.onError}: it is not an error, and anything it throws (or\n * rejects with) is swallowed — it never changes how the message is handled or acknowledged.\n */\n onRoutingMismatch?: (propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope) => unknown;\n}\n\n/** `bq-attempts` is valid only as ASCII digits that fit a signed 32-bit int (matches Java/.NET, GR-5). */\nconst MAX_ATTEMPTS = 2 ** 31 - 1;\n\nfunction parseAttempts(raw: string | undefined): number | undefined {\n if (typeof raw !== \"string\" || !/^[0-9]+$/.test(raw)) return undefined;\n const value = Number(raw);\n return value <= MAX_ATTEMPTS ? value : undefined;\n}\n\n/** Blank = empty or only ASCII whitespace (space, \\t, \\n, \\v, \\f, \\r) — not `String.prototype.trim()`'s wider set. */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The `bq-schema-version` value when it is present, non-blank and not the supported version\n * (compared against the core's `SCHEMA_VERSION`, exact string match: `\"01\"`, `\" 1\"`, `\"1.0\"` are\n * rejected). Blank means the empty string or only ASCII whitespace (the same set in every SDK, GR-5);\n * NBSP, U+0085, U+FEFF, U+3000 and the like are not blank. `undefined` when the message may be decoded.\n */\nfunction unsupportedSchemaVersion(properties: { [key: string]: string }): string | undefined {\n const raw: unknown = properties[\"bq-schema-version\"];\n if (typeof raw !== \"string\" || BLANK_ASCII.test(raw)) return undefined;\n return raw === String(SCHEMA_VERSION) ? undefined : raw;\n}\n\nfunction isReceiveTimeout(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return /timeout/i.test(message);\n}\n\nfunction decodePayload(data: Buffer | string): string {\n return typeof data === \"string\" ? data : data.toString(\"utf8\");\n}\n\n/**\n * Receives from a Pulsar subscription, decodes + validates each message, routes it to the\n * handler for its URN, and acknowledges it on success. A throwing handler\n * `negativeAcknowledge`s the message — the broker redelivers it and increments\n * `getRedeliveryCount()` (at-least-once). Routing uses the `bq-job` property (body URN as the\n * fallback); `attempts` is the `bq-attempts` property (body value as the fallback) reconciled to\n * `max(.., getRedeliveryCount())` for the handler.\n */\nexport class PulsarConsumer {\n constructor(\n private readonly consumer: PulsarConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: PulsarConsumerOptions = {},\n ) {}\n\n /** Receive one message (up to the receive timeout), route + settle it. Returns 1, or 0 on timeout. */\n async poll(): Promise<number> {\n let message: PulsarReceivedMessage;\n try {\n message = await this.consumer.receive(this.options.receiveTimeoutMs ?? 1000);\n } catch (error) {\n if (isReceiveTimeout(error)) return 0;\n throw error;\n }\n await this.handle(message);\n return 1;\n }\n\n /** Poll until `signal` aborts. */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: PulsarReceivedMessage): Promise<void> {\n const properties = safeProperties(message);\n\n // §5.7 step 3: gate on bq-schema-version before the body is decoded.\n const rejectedVersion = unsupportedSchemaVersion(properties);\n if (rejectedVersion !== undefined) {\n this.options.onError?.(\n new BabelQueueError(\n `Rejected a message from Apache Pulsar with unsupported bq-schema-version \"${rejectedVersion}\" (supported: ${SCHEMA_VERSION}).`,\n ),\n {},\n message,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n\n const envelope = EnvelopeCodec.decode(decodePayload(message.getData()));\n\n // attempts: the authoritative bq-attempts property (the body's value when it is absent or\n // invalid), then max(.., getRedeliveryCount()): the redelivery count is 0-based, so it maps\n // directly with no −1; the max never lowers a higher count republished from another SDK\n // (the Go/Python transports retry by re-sending with attempts+1).\n const redeliveryCount = typeof message.getRedeliveryCount === \"function\" ? message.getRedeliveryCount() : 0;\n // A body without an integer attempts stays as sent, so accepts() below still rejects it\n // (the property can't repair a non-conformant body); the value is written only on a change.\n const bodyAttempts = envelope.attempts;\n let attempts = typeof bodyAttempts === \"number\" && Number.isInteger(bodyAttempts) ? bodyAttempts : 0;\n const fromProperty = parseAttempts(properties[\"bq-attempts\"]);\n if (fromProperty !== undefined) attempts = fromProperty;\n if (redeliveryCount > attempts) attempts = redeliveryCount;\n if (typeof bodyAttempts === \"number\" && Number.isInteger(bodyAttempts) && bodyAttempts !== attempts) {\n envelope.attempts = attempts;\n }\n\n // routing: the bq-job property wins; the body's URN is the fallback. A body with no URN is\n // still rejected below, whatever bq-job says.\n const bodyUrn = EnvelopeCodec.urn(envelope);\n const rawJob: unknown = properties[\"bq-job\"];\n const propertyUrn = typeof rawJob === \"string\" ? rawJob.trim() : \"\";\n const urn = propertyUrn !== \"\" ? propertyUrn : bodyUrn;\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Apache Pulsar.\"),\n envelope,\n message,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n\n // Diagnostics only, and only for an accepted envelope (07a Java/.NET order): after accepts()\n // and the attempts reconcile, so the hook sees the same envelope the handler gets.\n if (propertyUrn !== \"\" && bodyUrn !== \"\" && propertyUrn !== bodyUrn) {\n this.notifyRoutingMismatch(propertyUrn, bodyUrn, envelope);\n }\n\n const handler = this.handlers[urn];\n if (!handler) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.consumer.acknowledge(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n return;\n }\n\n try {\n await handler(envelope as Envelope, message, headersOf(message));\n await this.consumer.acknowledge(message);\n } catch (error) {\n // Negative-ack releases the message — the broker redelivers and increments the count.\n this.options.onError?.(error, envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n }\n\n private notifyRoutingMismatch(propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope): void {\n const hook = this.options.onRoutingMismatch;\n if (hook === undefined) return;\n try {\n const result = hook(propertyUrn, bodyUrn, envelope);\n Promise.resolve(result).catch(() => undefined);\n } catch {\n // A diagnostic hook must never disturb consumption.\n }\n }\n}\n\nfunction safeProperties(message: PulsarReceivedMessage): { [key: string]: string } {\n try {\n const properties = typeof message.getProperties === \"function\" ? message.getProperties() : undefined;\n return properties ?? {};\n } catch {\n return {};\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Merge out-of-band `extra` headers onto an already-built `base` map **without clobbering** the\n * contract headers in `base` (they win a key collision) and dropping blanks. Returns `base`,\n * mutated in place.\n */\nexport function mergeInto(base: { [key: string]: string }, extra: HeaderCarrier | null | undefined): { [key: string]: string } {\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {\n if (key in base) continue; // the contract header already there wins\n base[key] = value;\n }\n return base;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache Pulsar adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Apache Pulsar, so a\n * Pulsar-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import Pulsar from \"pulsar-client\";\n * import { PulsarPublisher, PulsarConsumer } from \"@babelqueue/pulsar\";\n *\n * const client = new Pulsar.Client({ serviceUrl: \"pulsar://localhost:6650\" });\n *\n * const producer = await client.createProducer({ topic: \"orders\" });\n * await new PulsarPublisher(producer).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const sub = await client.subscribe({\n * topic: \"orders\", subscription: \"babelqueue\", subscriptionType: \"Shared\",\n * });\n * const consumer = new PulsarConsumer(sub, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * }, { subscriptionType: \"Shared\" }); // declare it (or pass `client` together with `topic`): see PulsarConsumerOptions\n * await consumer.run();\n *\n * This implements §5 of the broker-bindings contract: the canonical envelope is the message\n * payload, projected onto native Pulsar message properties (string→string) — `bq-job` = URN,\n * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /\n * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);\n * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is\n * redelivered (at-least-once) and `getRedeliveryCount()` is incremented — up to `maxAttempts`\n * deliveries (default 3, ADR-0036): the message that exhausts them is written to `<queue>.dlq`\n * (when a `deadLetterProducer` is given) or acknowledged and dropped. A body that does not decode\n * into an envelope (including an object with no valid identity) is poison: it never reaches a\n * handler, and with dead-lettering on it is recorded in `<queue>.dlq` in the ADR-0035 shape without\n * retry. Per §5.2 / §5.7 the\n * consumer routes on the `bq-job` property (the body's URN is used only when the property is\n * absent or blank) and takes `attempts` from the authoritative `bq-attempts` property (the\n * body's value is used only when the property is absent or not a plain ASCII-decimal integer\n * that fits a 32-bit int), reconciled to `max(bq-attempts, getRedeliveryCount())` — no −1,\n * because Pulsar's redelivery count is 0-based. The handler still receives the body's envelope.\n *\n * **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads the\n * `bq-schema-version` property. A missing or blank value (empty or only ASCII whitespace) changes nothing; any value other than\n * the core's supported `SCHEMA_VERSION` (`\"2\"`, `\"x\"`, `\"01\"`, ...) rejects the message undecoded:\n * `onError` (with an error naming the version, and an empty envelope since nothing was decoded),\n * then it is settled on its first delivery (ADR-0037 decision 1) — with a `deadLetterProducer` the\n * body, byte for byte, goes to `<queue>.dlq` and the message is acknowledged after the write (a failed\n * write is a `negativeAcknowledge`, never an acknowledgement); without one it is acknowledged and\n * dropped (logged). Never retried, whatever `bq-attempts`, the redelivery count or `maxAttempts` say.\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`\n * properties — the contract properties win a key collision (merge-not-clobber), and blanks are\n * dropped. On consume the inbound `getProperties()` are surfaced as a `Record<string,string>` so\n * the core's `otel` extract sees the `traceparent` and links the consumer span as a child. A\n * header-less publish is byte-identical. GR-1: the wire envelope (the payload) is never touched.\n */\n\nimport { BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, annotate, buildPoisonRecord } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Pulsar shapes (a structural subset of pulsar-client) --------------\n\n/** A message to send (structural subset of pulsar-client `ProducerMessage`). */\nexport interface PulsarProducerMessage {\n data: Buffer;\n properties?: { [key: string]: string };\n /** Relative delay in milliseconds before the message becomes available (native `deliverAfter`). */\n deliverAfter?: number;\n}\n\n/**\n * The subset of the Pulsar producer this adapter calls. A `Producer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarProducer {\n getTopic(): string;\n send(message: PulsarProducerMessage): Promise<unknown>;\n}\n\n/** A received message (structural subset of pulsar-client `Message`). */\nexport interface PulsarReceivedMessage {\n getData(): Buffer | string;\n getProperties(): { [key: string]: string };\n getRedeliveryCount(): number;\n}\n\n/**\n * The subset of the Pulsar consumer this adapter calls. A `Consumer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarConsumerClient {\n /**\n * The topic the subscription reads. pulsar-client's `Consumer` has **no** such method (only its `Producer`\n * does), so with it the topic comes from the {@link PulsarConsumerOptions.topic} option; a custom\n * subscription wrapper may provide this to make `topic` unnecessary.\n */\n getTopic?(): string;\n receive(timeoutMs?: number): Promise<PulsarReceivedMessage>;\n acknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n negativeAcknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n}\n\n/**\n * The subset of the Pulsar client this adapter calls to build its own retry producer. A `Client` from\n * `pulsar-client` satisfies it structurally.\n */\nexport interface PulsarClientLike {\n createProducer(config: { topic: string }): Promise<PulsarProducer>;\n}\n\n/** The subscription type, as pulsar-client names it (`Key_Shared` is accepted as an alias of `KeyShared`). */\nexport type PulsarSubscriptionType = \"Exclusive\" | \"Shared\" | \"Failover\" | \"KeyShared\" | \"Key_Shared\";\n\n// --- Property projection (contract §5.2) ---------------------------------------\n\n/** Bare topic name (the part after the last `/`), used as `meta.queue`. */\nfunction topicName(topic: string): string {\n const slash = topic.lastIndexOf(\"/\");\n return slash >= 0 ? topic.slice(slash + 1) : topic;\n}\n\n/**\n * Project the envelope's contract fields onto native Pulsar message properties\n * (string→string): `bq-job` = URN, `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus\n * `bq-schema-version` / `bq-source-lang` / `bq-attempts`. The body stays authoritative.\n */\nexport function pulsarProperties(envelope: Envelope): { [key: string]: string } {\n const properties: { [key: string]: string } = {};\n if (envelope.job) properties[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) properties[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) properties[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) {\n properties[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) properties[\"bq-source-lang\"] = envelope.meta.lang;\n properties[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return properties;\n}\n\n/**\n * Project the envelope onto a native Pulsar producer message (payload + properties). When `extra`\n * is given, those out-of-band headers (e.g. a `traceparent`, ADR-0028) are merged beside the\n * contract `bq-*` properties without clobbering them (the contract wins a key collision).\n */\nexport function toPulsarMessage(envelope: Envelope, extra?: HeaderCarrier | null): PulsarProducerMessage {\n return {\n data: Buffer.from(EnvelopeCodec.encode(envelope), \"utf8\"),\n properties: mergeInto(pulsarProperties(envelope), extra),\n };\n}\n\n/**\n * Read a received message's native properties back into a flat {@link HeaderCarrier} via\n * `getProperties()`. Returns an empty object when there are none. Both the contract `bq-*`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: PulsarReceivedMessage): HeaderCarrier {\n const props = typeof message.getProperties === \"function\" ? message.getProperties() : undefined;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value != null && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link PulsarPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as native Pulsar message properties beside the contract\n * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s\n * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the\n * message unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */\nexport class PulsarPublisher {\n constructor(private readonly producer: PulsarProducer) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the §5 property projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery via `deliverAfter` and mirrors `bq-delay`.\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: topicName(this.producer.getTopic()),\n traceId: options.traceId,\n });\n const message = toPulsarMessage(envelope, options.headers);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.properties = { ...message.properties, \"bq-delay\": String(options.delayMs) };\n message.deliverAfter = options.delayMs;\n }\n\n await this.producer.send(message);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Pulsar message, and the out-of-band\n * {@link HeaderCarrier} read from the message's properties (empty when there are none). Pass\n * `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the\n * producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link PulsarConsumer}. */\nexport interface PulsarConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is acknowledged). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;\n /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;\n /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */\n receiveTimeoutMs?: number;\n /**\n * Optional diagnostic hook, called when the `bq-job` property and the body's URN differ (the\n * property wins and the message is routed on it). `envelope` is the body's envelope. Separate\n * from {@link PulsarConsumerOptions.onError}: it is not an error, and anything it throws (or\n * rejects with) is swallowed — it never changes how the message is handled or acknowledged.\n */\n onRoutingMismatch?: (propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope) => unknown;\n /**\n * The total number of deliveries a message gets, the first included (default 3; `1` = no retry;\n * ADR-0036 decision 1). A failing handler, an unmapped URN without {@link onUnknownUrn} and a poison\n * body with dead-lettering off are retried (`negativeAcknowledge`) while `reconciled attempts + 1`\n * is below it; the delivery that reaches it is dead-lettered (when {@link deadLetterProducer} is\n * set) or acknowledged and dropped. Must be an integer of at least 1.\n */\n maxAttempts?: number;\n /**\n * A producer on the dead-letter topic (by convention `<queue>.dlq`). Dead-lettering is on exactly\n * when it is set; the SDK does not use Pulsar's native `DeadLetterPolicy`. The original message is\n * acknowledged only after the record was sent; a failed send leaves it negatively acknowledged so\n * it is redelivered and the write is tried again.\n */\n deadLetterProducer?: PulsarProducer;\n /**\n * A producer on the consumer's **own** topic. A retry re-publishes the message to that topic with\n * `bq-attempts` (and the body's `attempts`) raised to the counted value and then acknowledges the\n * original — the Go model. The count then lives in the message itself, so the bound holds on every\n * subscription type. Optional when {@link client} is given (the consumer builds the producer itself);\n * a failed re-publish leaves the original negatively acknowledged (never lost).\n *\n * Without a producer or a client a retry is a `negativeAcknowledge`, which advances the native\n * redelivery count only on `Shared` / `Key_Shared`: on `Exclusive` / `Failover` it stays 0 and the retry\n * would never reach `maxAttempts`, so that combination is rejected at construction (see\n * {@link subscriptionType}).\n */\n retryProducer?: PulsarProducer;\n /**\n * The Pulsar client the consumer builds its retry producer from, lazily on the first retry, on\n * {@link topic}. Needs `topic` (pulsar-client's `Consumer` cannot tell its topic). Ignored when\n * {@link retryProducer} is given.\n */\n client?: PulsarClientLike;\n /**\n * The topic the retry producer is created on — the topic the consumer reads. Required together with\n * {@link client} (a pulsar-client `Consumer` has no `getTopic()`); a subscription wrapper that provides\n * `getTopic()` makes it optional.\n */\n topic?: string;\n /**\n * The subscription type the consumer was created with. `Exclusive` and `Failover` cannot bound a\n * nack-driven retry (a negative acknowledgement does not advance the redelivery count there), so with\n * `maxAttempts > 1` they require a {@link retryProducer} or a {@link client}. **Not declaring it counts as\n * `Exclusive`** (pulsar-client's own default, and the fail-closed choice): without a `retryProducer` or a\n * `client` the constructor throws `BabelQueueError`. Declare `\"Shared\"` or `\"Key_Shared\"` to keep the\n * `negativeAcknowledge` retry without a producer.\n */\n subscriptionType?: PulsarSubscriptionType;\n /**\n * The queue name written to `dead_letter.original_queue` of a poison record (a decodable envelope\n * uses its own `meta.queue`). Defaults to the {@link deadLetterProducer} topic without its `.dlq`\n * suffix.\n */\n queue?: string;\n /** Receives the \"message dropped\" and \"dead-letter write failed\" warnings (default `console.warn`). */\n onWarn?: (message: string) => void;\n}\n\n/** What a retryable outcome is about: the reason written to `dead_letter.reason`. */\ntype SettleKind = \"failed\" | \"unknown_urn\" | \"poison\";\n\n/** Default total number of deliveries (ADR-0036 decision 1). */\nexport const DEFAULT_MAX_ATTEMPTS = 3;\n\n/** `bq-attempts` is valid only as ASCII digits that fit a signed 32-bit int (matches Java/.NET, GR-5). */\nconst MAX_ATTEMPTS = 2 ** 31 - 1;\n\nfunction parseAttempts(raw: string | undefined): number | undefined {\n if (typeof raw !== \"string\" || !/^[0-9]+$/.test(raw)) return undefined;\n const value = Number(raw);\n return value <= MAX_ATTEMPTS ? value : undefined;\n}\n\n/**\n * The reconciled attempt count of a delivery (broker-bindings §5.5): the `bq-attempts` property when\n * valid, else `bodyAttempts` (0 when the body has none), floored by the 0-based native redelivery count.\n */\nfunction reconcileAttempts(\n properties: { [key: string]: string },\n bodyAttempts: number,\n redeliveryCount: number,\n): number {\n let attempts = bodyAttempts;\n const fromProperty = parseAttempts(properties[\"bq-attempts\"]);\n if (fromProperty !== undefined) attempts = fromProperty;\n return redeliveryCount > attempts ? redeliveryCount : attempts;\n}\n\n/** Blank = empty or only ASCII whitespace (space, \\t, \\n, \\v, \\f, \\r) — not `String.prototype.trim()`'s wider set. */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The `bq-schema-version` value when it is present, non-blank and not the supported version\n * (compared against the core's `SCHEMA_VERSION`, exact string match: `\"01\"`, `\" 1\"`, `\"1.0\"` are\n * rejected). Blank means the empty string or only ASCII whitespace (the same set in every SDK, GR-5);\n * NBSP, U+0085, U+FEFF, U+3000 and the like are not blank. `undefined` when the message may be decoded.\n */\nfunction unsupportedSchemaVersion(properties: { [key: string]: string }): string | undefined {\n const raw: unknown = properties[\"bq-schema-version\"];\n if (typeof raw !== \"string\" || BLANK_ASCII.test(raw)) return undefined;\n return raw === String(SCHEMA_VERSION) ? undefined : raw;\n}\n\nfunction isReceiveTimeout(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return /timeout/i.test(message);\n}\n\nfunction decodePayload(data: Buffer | string): string {\n return typeof data === \"string\" ? data : data.toString(\"utf8\");\n}\n\n/**\n * Receives from a Pulsar subscription, decodes + validates each message, routes it to the\n * handler for its URN, and acknowledges it on success. A throwing handler\n * `negativeAcknowledge`s the message — the broker redelivers it and increments\n * `getRedeliveryCount()` (at-least-once; with a `retryProducer` the message is instead re-published with\n * `bq-attempts` + 1, which also bounds `Exclusive` / `Failover` subscriptions) — until `maxAttempts` deliveries are used up; the last one is\n * dead-lettered to `<queue>.dlq` or acknowledged and dropped (ADR-0036). Routing uses the `bq-job`\n * property (body URN as the fallback); `attempts` is the `bq-attempts` property (body value as the\n * fallback) reconciled to `max(.., getRedeliveryCount())` for the handler.\n */\nexport class PulsarConsumer {\n private readonly maxAttempts: number;\n private builtRetryProducer: Promise<PulsarProducer | undefined> | undefined;\n private readonly warn: (message: string) => void;\n\n constructor(\n private readonly consumer: PulsarConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: PulsarConsumerOptions = {},\n ) {\n const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n throw new BabelQueueError(`maxAttempts must be an integer of at least 1, got ${String(maxAttempts)}.`);\n }\n this.maxAttempts = maxAttempts;\n this.checkRetryBound(maxAttempts);\n this.warn = options.onWarn ?? ((message) => console.warn(message));\n }\n\n /**\n * Setup check: on `Exclusive` / `Failover` (and an undeclared type) a negative acknowledgement does not advance the native\n * redelivery count, so a retry needs a re-publish. Fail loudly here instead of looping at run time.\n */\n private checkRetryBound(maxAttempts: number): void {\n const declared = this.options.subscriptionType;\n if (declared !== undefined && ![\"Exclusive\", \"Shared\", \"Failover\", \"KeyShared\"].includes(declared.replace(\"_\", \"\"))) {\n throw new BabelQueueError(`Unknown subscriptionType \"${String(declared)}\".`);\n }\n // An undeclared type is treated as Exclusive (fail closed).\n const type = declared === undefined ? \"Exclusive\" : declared.replace(\"_\", \"\");\n if (type !== \"Exclusive\" && type !== \"Failover\") return;\n if (maxAttempts === 1 || this.options.retryProducer !== undefined) return;\n if (this.options.client !== undefined && this.retryTopic() !== undefined) return;\n const shown = declared === undefined ? \"An undeclared subscriptionType (treated as Exclusive)\" : `A ${type} subscription`;\n if (this.options.client !== undefined) {\n // A client alone cannot name the topic: pulsar-client's Consumer has no getTopic().\n throw new BabelQueueError(\n `${shown} with a client needs the \\`topic\\` option (the topic this consumer reads) to build the retry producer: ` +\n \"pulsar-client's Consumer cannot report its own topic.\",\n );\n }\n throw new BabelQueueError(\n `${shown} needs a retryProducer (or a client plus a topic) to bound retries: ` +\n \"a negative acknowledgement does not advance the redelivery count there, so a retry would never reach maxAttempts. \" +\n 'Pass a client plus topic (the topic you subscribed to), or a retryProducer, or declare subscriptionType: \"Shared\" / \"Key_Shared\".',\n );\n }\n\n private retryTopic(): string | undefined {\n const configured = this.options.topic;\n if (configured !== undefined && configured !== \"\") return configured;\n const own = typeof this.consumer.getTopic === \"function\" ? this.consumer.getTopic() : undefined;\n return own !== undefined && own !== \"\" ? own : undefined;\n }\n\n /** The retry producer: the one given, else one built (once) from the client; undefined when neither exists. */\n private async resolveRetryProducer(): Promise<PulsarProducer | undefined> {\n if (this.options.retryProducer !== undefined) return this.options.retryProducer;\n const client = this.options.client;\n const topic = this.retryTopic();\n if (client === undefined || topic === undefined) return undefined;\n this.builtRetryProducer ??= client.createProducer({ topic }).catch((error: unknown) => {\n this.builtRetryProducer = undefined; // try again on the next retry\n this.warn(`babelqueue: creating the retry producer on ${topic} failed (${errorText(error)}).`);\n return undefined;\n });\n return this.builtRetryProducer;\n }\n\n /** Receive one message (up to the receive timeout), route + settle it. Returns 1, or 0 on timeout. */\n async poll(): Promise<number> {\n let message: PulsarReceivedMessage;\n try {\n message = await this.consumer.receive(this.options.receiveTimeoutMs ?? 1000);\n } catch (error) {\n if (isReceiveTimeout(error)) return 0;\n throw error;\n }\n await this.handle(message);\n return 1;\n }\n\n /** Poll until `signal` aborts. */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: PulsarReceivedMessage): Promise<void> {\n const properties = safeProperties(message);\n\n // §5.7 step 3: gate on bq-schema-version before the body is decoded.\n const rejectedVersion = unsupportedSchemaVersion(properties);\n if (rejectedVersion !== undefined) {\n this.options.onError?.(\n new BabelQueueError(\n `Rejected a message from Apache Pulsar with unsupported bq-schema-version \"${rejectedVersion}\" (supported: ${SCHEMA_VERSION}).`,\n ),\n {},\n message,\n );\n await this.settleVersionGated(message);\n return;\n }\n\n const redeliveryCount = typeof message.getRedeliveryCount === \"function\" ? message.getRedeliveryCount() : 0;\n const envelope = EnvelopeCodec.decode(decodePayload(message.getData()));\n\n // routing: the bq-job property wins; the body's URN is the fallback. A body with no URN is\n // still rejected below, whatever bq-job says.\n const bodyUrn = EnvelopeCodec.urn(envelope);\n const rawJob: unknown = properties[\"bq-job\"];\n const propertyUrn = typeof rawJob === \"string\" ? rawJob.trim() : \"\";\n const urn = propertyUrn !== \"\" ? propertyUrn : bodyUrn;\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // Poison (ADR-0036 decisions 2 and 4): a body that does not decode into an envelope, including an\n // object with no valid identity, never reaches a handler. Its attempts come from the transport\n // alone (bq-attempts, the redelivery count): there is no decodable body to read them from.\n const error = new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Apache Pulsar.\");\n this.options.onError?.(error, envelope, message);\n await this.settle(\"poison\", reconcileAttempts(properties, 0, redeliveryCount) + 1, null, message, error);\n return;\n }\n\n // attempts: the authoritative bq-attempts property (the body's value when it is absent or\n // invalid), then max(.., getRedeliveryCount()): the redelivery count is 0-based, so it maps\n // directly with no −1; the max never lowers a higher count republished from another SDK\n // (the Go/Python transports retry by re-sending with attempts+1).\n // accepts() above guarantees an integer body attempts; the value is written only on a change.\n const bodyAttempts = envelope.attempts as number;\n const attempts = reconcileAttempts(properties, bodyAttempts, redeliveryCount);\n if (bodyAttempts !== attempts) {\n envelope.attempts = attempts;\n }\n\n // Diagnostics only, and only for an accepted envelope (07a Java/.NET order): after accepts()\n // and the attempts reconcile, so the hook sees the same envelope the handler gets.\n if (propertyUrn !== \"\" && bodyUrn !== \"\" && propertyUrn !== bodyUrn) {\n this.notifyRoutingMismatch(propertyUrn, bodyUrn, envelope);\n }\n\n const handler = this.handlers[urn];\n if (!handler) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.consumer.acknowledge(message);\n } else {\n const error = new UnknownUrnError(urn);\n this.options.onError?.(error, envelope, message);\n await this.settle(\"unknown_urn\", attempts + 1, envelope as Envelope, message, error);\n }\n return;\n }\n\n try {\n await handler(envelope as Envelope, message, headersOf(message));\n await this.consumer.acknowledge(message);\n } catch (error) {\n this.options.onError?.(error, envelope, message);\n await this.settle(\"failed\", attempts + 1, envelope as Envelope, message, error);\n }\n }\n\n /**\n * Settle a version-gated delivery on its first delivery (ADR-0037 decision 1, broker-bindings §5.7): it is\n * never decoded, never reaches a handler or {@link PulsarConsumerOptions.onUnknownUrn}, and is never retried,\n * whatever `bq-attempts`, the redelivery count or `maxAttempts` say. This consumer has no unknown-URN\n * strategy option, so it behaves as `fail`: with a {@link PulsarConsumerOptions.deadLetterProducer} the body is\n * written to `<queue>.dlq` exactly as received (no `dead_letter` block, no re-encoding) and acknowledged only\n * after the write; without one it is acknowledged and dropped (logged). A failed write leaves the delivery\n * negatively acknowledged (never acknowledged, never counted as a retry): the gate rejects the redelivery\n * again, so the write is tried again.\n */\n private async settleVersionGated(message: PulsarReceivedMessage): Promise<void> {\n const producer = this.options.deadLetterProducer;\n if (producer === undefined) {\n this.warn(\"babelqueue: dropped a version-gated Apache Pulsar message (dead-lettering is off).\");\n await this.consumer.acknowledge(message);\n return;\n }\n try {\n const data = message.getData();\n await producer.send({\n data: typeof data === \"string\" ? Buffer.from(data, \"utf8\") : Buffer.from(data),\n properties: { ...safeProperties(message) },\n });\n } catch (writeError) {\n this.warn(\n `babelqueue: dead-letter write of a version-gated message to ${producer.getTopic()} failed (${errorText(writeError)}); the message is left for redelivery.`,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n await this.consumer.acknowledge(message);\n }\n\n /**\n * The settle decision of a retryable outcome (ADR-0036 decisions 1 and 2). `counted` is the reconciled\n * attempts plus one for this delivery. A poison body with dead-lettering on is recorded without retry;\n * every other outcome retries (`negativeAcknowledge`: the native redelivery count advances, so the\n * next delivery counts one more) while `counted < maxAttempts`, then dead-letters or drops.\n */\n private async settle(\n kind: SettleKind,\n counted: number,\n envelope: Envelope | null,\n message: PulsarReceivedMessage,\n error: unknown,\n ): Promise<void> {\n const producer = this.options.deadLetterProducer;\n if (kind === \"poison\" && producer !== undefined) {\n await this.deadLetter(producer, kind, counted, null, message, error);\n return;\n }\n if (counted < this.maxAttempts) {\n await this.retry(counted, envelope, message);\n return;\n }\n if (producer !== undefined) {\n await this.deadLetter(producer, kind, counted, envelope, message, error);\n return;\n }\n this.warn(\n `babelqueue: dropped a ${kind} Apache Pulsar message after ${counted} of ${this.maxAttempts} deliveries (dead-lettering is off).`,\n );\n await this.consumer.acknowledge(message);\n }\n\n /**\n * Retry one delivery. With a {@link PulsarConsumerOptions.retryProducer}: re-publish to the consumer's\n * own topic carrying `bq-attempts = counted` (reconcile = max(bq-attempts, RedeliveryCount) on the next\n * delivery), then acknowledge the original — only after the send succeeded. Otherwise `negativeAcknowledge`.\n */\n private async retry(counted: number, envelope: Envelope | null, message: PulsarReceivedMessage): Promise<void> {\n const producer = await this.resolveRetryProducer();\n if (producer === undefined) {\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n try {\n await producer.send(this.retryMessage(counted, envelope, message));\n } catch (writeError) {\n this.warn(\n `babelqueue: retry re-publish to ${producer.getTopic()} failed (${errorText(writeError)}); the message is left for redelivery.`,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n await this.consumer.acknowledge(message);\n }\n\n private retryMessage(counted: number, envelope: Envelope | null, message: PulsarReceivedMessage): PulsarProducerMessage {\n // Out-of-band headers (e.g. a traceparent) ride along; the contract bq-* properties are rebuilt and win,\n // and the one-shot bq-delay of the first publish is not repeated.\n const carried = headersOf(message);\n delete carried[\"bq-delay\"];\n if (envelope !== null) {\n return keepRoute(toPulsarMessage({ ...envelope, attempts: counted }, carried), message);\n }\n // A poison body is retried byte for byte (there is no envelope to re-encode); only the counter moves.\n const data = message.getData();\n return keepRoute(\n {\n data: typeof data === \"string\" ? Buffer.from(data, \"utf8\") : Buffer.from(data),\n properties: { ...carried, \"bq-attempts\": String(counted) },\n },\n message,\n );\n }\n\n /**\n * Write the DLQ record, then acknowledge the original — only after the send succeeded. A failed send\n * leaves the original negatively acknowledged, so it is redelivered and the write is tried again.\n */\n private async deadLetter(\n producer: PulsarProducer,\n kind: SettleKind,\n counted: number,\n envelope: Envelope | null,\n message: PulsarReceivedMessage,\n error: unknown,\n ): Promise<void> {\n try {\n await producer.send(this.deadLetterMessage(producer, kind, counted, envelope, message, error));\n } catch (writeError) {\n this.warn(\n `babelqueue: dead-letter write to ${producer.getTopic()} failed (${errorText(writeError)}); the message is left for redelivery.`,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n await this.consumer.acknowledge(message);\n }\n\n private deadLetterMessage(\n producer: PulsarProducer,\n kind: SettleKind,\n counted: number,\n envelope: Envelope | null,\n message: PulsarReceivedMessage,\n error: unknown,\n ): PulsarProducerMessage {\n const details = {\n attempts: counted,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n };\n if (envelope === null) {\n // ADR-0035: the original bytes are kept; only the dead_letter block is added.\n const original = this.options.queue ?? stripDlqSuffix(topicName(producer.getTopic()));\n // No retry decision was taken, so the value is informational: the delivered count.\n const delivered = counted - 1;\n const deadLetter = annotate({ attempts: delivered } as unknown as Envelope, kind, original, {\n ...details,\n attempts: delivered,\n }).dead_letter;\n const data = message.getData();\n const body = typeof data === \"string\" ? Buffer.from(data, \"utf8\") : Buffer.from(data);\n const record = buildPoisonRecord(body, JSON.stringify(deadLetter));\n return keepRoute({ data: Buffer.from(record, \"utf8\"), properties: poisonProperties(record) }, message);\n }\n // The envelope as the next hop will see it: attempts is the counted value, the same one that was\n // compared against maxAttempts and that dead_letter.attempts records.\n const original = envelope.meta?.queue || this.options.queue || stripDlqSuffix(topicName(producer.getTopic()));\n const annotated = annotate({ ...envelope, attempts: counted }, kind, original, details);\n return keepRoute(toPulsarMessage(annotated), message);\n }\n\n private notifyRoutingMismatch(propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope): void {\n const hook = this.options.onRoutingMismatch;\n if (hook === undefined) return;\n try {\n const result = hook(propertyUrn, bodyUrn, envelope);\n Promise.resolve(result).catch(() => undefined);\n } catch {\n // A diagnostic hook must never disturb consumption.\n }\n }\n}\n\nfunction safeProperties(message: PulsarReceivedMessage): { [key: string]: string } {\n try {\n const properties = typeof message.getProperties === \"function\" ? message.getProperties() : undefined;\n return properties ?? {};\n } catch {\n return {};\n }\n}\n\n/**\n * Keep the route a delivery arrived on: when the incoming message carries a non-blank `bq-job` property\n * (the consumer routes on it, §5.2/§5.7) the re-published retry or DLQ message carries the same value, even\n * if the body names another URN. Otherwise the second delivery could reach a different handler (Go's\n * `publishKeepingRoute`).\n */\nfunction keepRoute(outgoing: PulsarProducerMessage, incoming: PulsarReceivedMessage): PulsarProducerMessage {\n const route: unknown = safeProperties(incoming)[\"bq-job\"];\n if (typeof route === \"string\" && route.trim() !== \"\") {\n outgoing.properties = { ...outgoing.properties, \"bq-job\": route };\n }\n return outgoing;\n}\n\nfunction errorText(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** `orders.dlq` -> `orders`: the queue a DLQ topic belongs to (a name without the suffix is kept). */\nfunction stripDlqSuffix(topic: string): string {\n return topic.endsWith(\".dlq\") && topic.length > 4 ? topic.slice(0, -4) : topic;\n}\n\n/**\n * Contract `bq-*` properties for a poison DLQ record, projected from whatever the body carries (a body\n * that is not an object, or carries none of them, yields none).\n */\nfunction poisonProperties(recordJson: string): { [key: string]: string } {\n const properties: { [key: string]: string } = {};\n let record: unknown;\n try {\n record = JSON.parse(recordJson);\n } catch {\n return properties; // nested deeper than JSON.parse can recurse\n }\n if (record === null || typeof record !== \"object\") return properties;\n const top = record as Record<string, unknown>;\n const str = (value: unknown): string | undefined => (typeof value === \"string\" && value !== \"\" ? value : undefined);\n const job = str(top[\"job\"]) ?? str(top[\"urn\"]);\n if (job) properties[\"bq-job\"] = job;\n const trace = str(top[\"trace_id\"]);\n if (trace) properties[\"bq-trace-id\"] = trace;\n const meta = top[\"meta\"] !== null && typeof top[\"meta\"] === \"object\" ? (top[\"meta\"] as Record<string, unknown>) : {};\n const id = str(meta[\"id\"]);\n if (id) properties[\"bq-message-id\"] = id;\n return properties;\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Merge out-of-band `extra` headers onto an already-built `base` map **without clobbering** the\n * contract headers in `base` (they win a key collision) and dropping blanks. Returns `base`,\n * mutated in place.\n */\nexport function mergeInto(base: { [key: string]: string }, extra: HeaderCarrier | null | undefined): { [key: string]: string } {\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {\n if (key in base) continue; // the contract header already there wins\n base[key] = value;\n }\n return base;\n}\n"],"mappings":"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","names":["original"]}