@babelqueue/kafka 1.1.2 → 1.2.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.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  KafkaConsumer: () => KafkaConsumer,
24
24
  KafkaPublisher: () => KafkaPublisher,
25
+ KafkaRedriveIO: () => KafkaRedriveIO,
25
26
  RetryTopics: () => RetryTopics,
26
27
  headersOf: () => headersOf,
27
28
  kafkaHeaders: () => kafkaHeaders
@@ -86,6 +87,9 @@ function headerInt(headers, key, fallback) {
86
87
  const n = Number.parseInt(s.trim(), 10);
87
88
  return Number.isNaN(n) ? fallback : n;
88
89
  }
90
+ function isThenable(value) {
91
+ return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
92
+ }
89
93
  var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
90
94
  function unsupportedSchemaVersion(message) {
91
95
  const value = headerString(message.headers, "bq-schema-version");
@@ -176,6 +180,33 @@ var KafkaPublisher = class _KafkaPublisher {
176
180
  });
177
181
  }
178
182
  };
183
+ var KafkaRedriveIO = class {
184
+ constructor(options) {
185
+ this.options = options;
186
+ }
187
+ options;
188
+ pop(queue) {
189
+ return this.options.pop(queue);
190
+ }
191
+ /** Append an already-encoded body to `queue` as a keyless record. */
192
+ async publish(queue, body) {
193
+ await this.send(queue, body, null);
194
+ }
195
+ /** Append `body` to `queue`, carrying the key of the reserved record `from` byte-for-byte. */
196
+ async publishFrom(queue, body, from) {
197
+ await this.send(queue, body, from.key ?? null);
198
+ }
199
+ async send(queue, body, key) {
200
+ const record = { value: body };
201
+ const decoded = import_core.EnvelopeCodec.decode(body);
202
+ if (import_core.EnvelopeCodec.accepts(decoded)) {
203
+ record.headers = kafkaHeaders(decoded);
204
+ record.timestamp = String(decoded.meta.created_at);
205
+ }
206
+ if (key !== null) record.key = Buffer.from(key);
207
+ await this.options.producer.send({ topic: queue, messages: [record] });
208
+ }
209
+ };
179
210
  var KafkaConsumer = class {
180
211
  constructor(consumer, handlers, options = {}) {
181
212
  this.consumer = consumer;
@@ -198,13 +229,15 @@ var KafkaConsumer = class {
198
229
  const { message } = payload;
199
230
  const unsupported = unsupportedSchemaVersion(message);
200
231
  if (unsupported !== void 0) {
201
- this.report(
202
- new import_core.BabelQueueError(`Rejected a record from Kafka with unsupported bq-schema-version "${unsupported}" (supported: ${import_core.SCHEMA_VERSION}).`),
203
- null,
204
- payload
232
+ const rejected = new import_core.BabelQueueError(
233
+ `Rejected a record from Kafka with unsupported bq-schema-version "${unsupported}" (supported: ${import_core.SCHEMA_VERSION}).`
205
234
  );
206
- await this.deadLetterRaw(payload);
207
- await this.commit(payload);
235
+ try {
236
+ await this.deadLetterRaw(payload);
237
+ await this.commit(payload);
238
+ } finally {
239
+ this.report(rejected, null, payload);
240
+ }
208
241
  return;
209
242
  }
210
243
  const raw = message.value ? message.value.toString("utf8") : "";
@@ -212,15 +245,22 @@ var KafkaConsumer = class {
212
245
  try {
213
246
  envelope = this.reconcile(import_core.EnvelopeCodec.decode(raw), message);
214
247
  } catch (decodeError) {
215
- this.report(decodeError, null, payload);
216
- await this.deadLetterRaw(payload);
217
- await this.commit(payload);
248
+ try {
249
+ await this.deadLetterRaw(payload);
250
+ await this.commit(payload);
251
+ } finally {
252
+ this.report(decodeError, null, payload);
253
+ }
218
254
  return;
219
255
  }
220
256
  if (!import_core.EnvelopeCodec.accepts(envelope)) {
221
- this.report(new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Kafka."), envelope, payload);
222
- await this.deadLetterRaw(payload);
223
- await this.commit(payload);
257
+ const rejected = new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Kafka.");
258
+ try {
259
+ await this.deadLetterRaw(payload);
260
+ await this.commit(payload);
261
+ } finally {
262
+ this.report(rejected, envelope, payload);
263
+ }
224
264
  return;
225
265
  }
226
266
  const urn = headerString(message.headers, "bq-job") ?? import_core.EnvelopeCodec.urn(envelope);
@@ -233,9 +273,12 @@ var KafkaConsumer = class {
233
273
  await handler(envelope, message, headersOf(message));
234
274
  await this.commit(payload);
235
275
  } catch (error) {
236
- this.report(error, envelope, payload);
237
- await this.retryOrDeadLetter(payload, envelope, error);
238
- await this.commit(payload);
276
+ try {
277
+ await this.retryOrDeadLetter(payload, envelope, error);
278
+ await this.commit(payload);
279
+ } finally {
280
+ this.report(error, envelope, payload);
281
+ }
239
282
  }
240
283
  }
241
284
  reconcile(envelope, message) {
@@ -285,7 +328,7 @@ var KafkaConsumer = class {
285
328
  const headers = kafkaHeaders(bumped);
286
329
  headers["bq-delay"] = String(tier.delayMs);
287
330
  headers["bq-original-topic"] = this.originalTopic(payload);
288
- await this.produce(tier.topic, bumped, headers);
331
+ await this.produce(tier.topic, bumped, headers, payload.message.key);
289
332
  }
290
333
  async deadLetter(envelope, payload, reason, error) {
291
334
  const dlq = this.options.retryTopics?.dlqTopic ?? null;
@@ -298,7 +341,7 @@ var KafkaConsumer = class {
298
341
  });
299
342
  const headers = kafkaHeaders(annotated);
300
343
  headers["bq-original-topic"] = original;
301
- await this.produce(dlq, annotated, headers);
344
+ await this.produce(dlq, annotated, headers, payload.message.key);
302
345
  }
303
346
  async deadLetterRaw(payload) {
304
347
  const dlq = this.options.retryTopics?.dlqTopic ?? null;
@@ -314,12 +357,16 @@ var KafkaConsumer = class {
314
357
  if (message.key !== void 0 && message.key !== null) forwarded.key = Buffer.from(message.key);
315
358
  await this.options.producer.send({ topic: dlq, messages: [forwarded] });
316
359
  }
317
- async produce(topic, envelope, headers) {
360
+ /**
361
+ * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
362
+ * empty); a null/absent key is left off the record, never synthesised. Partition equality is not
363
+ * guaranteed.
364
+ */
365
+ async produce(topic, envelope, headers, key) {
318
366
  this.requireProducer();
319
- await this.options.producer.send({
320
- topic,
321
- messages: [{ value: import_core.EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }]
322
- });
367
+ const record = { value: import_core.EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) };
368
+ if (key !== void 0 && key !== null) record.key = Buffer.from(key);
369
+ await this.options.producer.send({ topic, messages: [record] });
323
370
  }
324
371
  async commit(payload) {
325
372
  await this.consumer.commitOffsets([
@@ -334,14 +381,27 @@ var KafkaConsumer = class {
334
381
  throw new import_core.BabelQueueError("This Kafka consumer needs a producer to republish (retry/DLQ).");
335
382
  }
336
383
  }
384
+ /**
385
+ * Hand a failure to the user's `onError` callback. Callers invoke this only AFTER the record has been
386
+ * settled (dead-lettered / retried and committed). A callback that throws synchronously, or returns a
387
+ * promise that later rejects (an `async` function is assignable to the `void` callback type), is
388
+ * swallowed here so user code can never bypass settlement or stop the consumer.
389
+ */
337
390
  report(error, envelope, payload) {
338
- this.options.onError?.(error, envelope, payload);
391
+ try {
392
+ const returned = this.options.onError?.(error, envelope, payload);
393
+ if (isThenable(returned)) {
394
+ returned.then(void 0, () => void 0);
395
+ }
396
+ } catch {
397
+ }
339
398
  }
340
399
  };
341
400
  // Annotate the CommonJS export names for ESM import in node:
342
401
  0 && (module.exports = {
343
402
  KafkaConsumer,
344
403
  KafkaPublisher,
404
+ KafkaRedriveIO,
345
405
  RetryTopics,
346
406
  headersOf,
347
407
  kafkaHeaders
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache Kafka adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed, **process-then-commit** consumer over\n * Apache Kafka (KafkaJS), so a Kafka-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * Kafka has **no native** per-message ack, delayed delivery, dead-letter queue, or delivery\n * counter — this adapter absorbs all four in the binding layer (the envelope stays\n * `schema_version: 1`): the envelope JSON is the record **value**; the contract fields are\n * mirrored onto `bq-` headers (route on `bq-job` without decoding the body) and the record\n * timestamp mirrors `meta.created_at`; **`bq-attempts` is the authoritative retry counter**;\n * consume is process-then-commit (manual commit, at-least-once); retry/delay use SDK-owned\n * tiered retry topics `<topic>.retry.<n>`; terminal failures go to an opt-in `<topic>.dlq`.\n *\n * This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you\n * provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter\n * structurally).\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 Kafka **record headers** beside the contract `bq-*` headers —\n * the contract headers win a key collision (merge-not-clobber), and blanks are dropped. On consume\n * the inbound record headers are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope (the record value) is never touched.\n *\n * **Version gate (§6.7).** Before decoding the record value the consumer reads the `bq-schema-version`\n * header. A missing or blank value (empty or only ASCII whitespace) follows the normal decode path;\n * any other value is compared, untrimmed, to the supported `\"1\"`, and a record that does not match is\n * rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and\n * headers) forwarded to the DLQ (when configured) and the offset committed.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Kafka shapes (a structural subset of kafkajs) ---------------------\n\n/** Incoming record headers (KafkaJS decodes header values to Buffers). */\nexport type IncomingHeaders = { [key: string]: Buffer | string | (Buffer | string)[] | null | undefined };\n\n/** A record to produce (structural subset of a KafkaJS message). */\nexport interface KafkaProducerMessage {\n key?: Buffer | string | null;\n value: Buffer | string;\n /**\n * Header values are strings for records the adapter builds. Raw DLQ forwarding (gate rejection,\n * decode error, non-conformant envelope) instead passes the inbound values through as\n * `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.\n */\n headers?: { [key: string]: string };\n timestamp?: string;\n}\n\n/** The subset of the KafkaJS producer this adapter calls. */\nexport interface KafkaProducerClient {\n send(record: { topic: string; messages: KafkaProducerMessage[] }): Promise<unknown>;\n}\n\n/** A received record (structural subset of a KafkaJS message). */\nexport interface KafkaIncomingMessage {\n value: Buffer | null;\n headers?: IncomingHeaders;\n offset: string;\n timestamp?: string;\n key?: Buffer | null;\n}\n\n/** The payload KafkaJS passes to `eachMessage`. */\nexport interface EachMessagePayload {\n topic: string;\n partition: number;\n message: KafkaIncomingMessage;\n}\n\n/** The subset of the KafkaJS consumer this adapter calls (manual commit — `autoCommit: false`). */\nexport interface KafkaConsumerClient {\n run(config: { autoCommit?: boolean; eachMessage: (payload: EachMessagePayload) => Promise<void> }): Promise<void>;\n commitOffsets(offsets: { topic: string; partition: number; offset: string }[]): Promise<void>;\n}\n\n// --- Header projection (contract §6.3) -----------------------------------------\n\n/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */\nexport function kafkaHeaders(envelope: Envelope): { [key: string]: string } {\n const headers: { [key: string]: string } = {};\n if (envelope.job) headers[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) headers[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) headers[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) headers[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n if (envelope.meta.lang) headers[\"bq-source-lang\"] = envelope.meta.lang;\n headers[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return headers;\n}\n\n/**\n * Copy inbound record headers for a raw DLQ forward: every value becomes a `Buffer` copy (a string is\n * UTF-8 encoded), array-valued headers keep all their entries, and `undefined` / `null` values are dropped.\n */\nfunction copyHeaders(headers: IncomingHeaders | undefined): { [key: string]: Buffer | Buffer[] } {\n const out: { [key: string]: Buffer | Buffer[] } = {};\n if (!headers) return out;\n const copy = (v: Buffer | string): Buffer => (Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from(v, \"utf8\"));\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n out[name] = value.filter((v) => v !== undefined && v !== null).map(copy);\n } else {\n out[name] = copy(value);\n }\n }\n return out;\n}\n\nfunction headerString(headers: IncomingHeaders | undefined, key: string): string | undefined {\n const value = headers?.[key];\n // A null header value is \"no header\" (broker-bindings.md §6.7): never stringified to \"null\".\n if (value === undefined || value === null) return undefined;\n const v = Array.isArray(value) ? value[0] : value;\n if (v === undefined || v === null) return undefined;\n return Buffer.isBuffer(v) ? v.toString(\"utf8\") : String(v);\n}\n\nfunction headerInt(headers: IncomingHeaders | undefined, key: string, fallback: number): number {\n const s = headerString(headers, key);\n if (s === undefined) return fallback;\n const n = Number.parseInt(s.trim(), 10);\n return Number.isNaN(n) ? fallback : n;\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq-schema-version` header when it is present, non-blank and not exactly the supported\n * version (compared untrimmed), else `undefined` (the record may be decoded).\n */\nfunction unsupportedSchemaVersion(message: KafkaIncomingMessage): string | undefined {\n const value = headerString(message.headers, \"bq-schema-version\");\n if (value === undefined || BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\n/**\n * Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer\n * values. Returns an empty object when there are none. Both the contract `bq-*` headers and any\n * out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it\n * knows.\n */\nexport function headersOf(message: KafkaIncomingMessage): HeaderCarrier {\n const headers = message.headers;\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headerString(headers, key);\n if (value !== undefined && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Retry / delay topology (contract §6.4–§6.5) -------------------------------\n\n/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */\nexport interface Tier {\n topic: string;\n delayMs: number;\n}\n\n/**\n * The SDK-owned retry/delay topology for one work topic: tiered delay topics\n * `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no\n * tiers configured raises rather than silently dropping.\n */\nexport class RetryTopics {\n readonly tiers: Tier[];\n readonly dlqTopic: string | null;\n\n constructor(\n readonly workTopic: string,\n delaysMs: number[] = [],\n dlqTopic: string | null | undefined = undefined,\n ) {\n this.tiers = [...delaysMs]\n .sort((a, b) => a - b)\n .map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));\n this.dlqTopic = dlqTopic === undefined ? `${workTopic}.dlq` : dlqTopic;\n }\n\n hasTiers(): boolean {\n return this.tiers.length > 0;\n }\n\n /** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */\n tierForDelay(delayMs: number): Tier {\n this.requireTiers();\n for (const tier of this.tiers) {\n if (tier.delayMs >= delayMs) return tier;\n }\n throw new BabelQueueError(\n `Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1]!.delayMs}ms).`,\n );\n }\n\n /** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */\n tierForAttempt(attempt: number): Tier {\n this.requireTiers();\n return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)]!;\n }\n\n private requireTiers(): void {\n if (!this.tiers.length) {\n throw new BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);\n }\n }\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link KafkaPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`\n * headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */\nexport class KafkaPublisher {\n private constructor(\n private readonly producer: KafkaProducerClient,\n private readonly workTopic: string,\n private readonly retryTopics?: RetryTopics,\n ) {}\n\n /** A publisher onto `topic` with no retry topics (a delay raises). */\n static create(producer: KafkaProducerClient, topic: string): KafkaPublisher {\n return new KafkaPublisher(producer, topic);\n }\n\n /** A publisher onto the topology's work topic, with delay routed via its retry tiers. */\n static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher {\n return new KafkaPublisher(producer, retryTopics.workTopic, retryTopics);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });\n if (options.delayMs != null && options.delayMs > 0) {\n if (!this.retryTopics) {\n throw new BabelQueueError(\"Kafka has no native delayed delivery; a delay requires retry topics (none configured).\");\n }\n const tier = this.retryTopics.tierForDelay(options.delayMs);\n await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope, options.headers);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(\n topic: string,\n envelope: Envelope,\n extra?: HeaderCarrier | null,\n delayMs?: number,\n originalTopic?: string,\n ): Promise<void> {\n // The contract bq-* headers are seeded first, then the out-of-band riders are merged beside\n // them without clobbering (the contract wins a collision).\n const headers = mergeInto(kafkaHeaders(envelope), extra);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band\n * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to\n * `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span\n * (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link KafkaConsumer}. */\nexport interface KafkaConsumerOptions {\n /** The producer used to republish retry/DLQ records (required for retry/DLQ). */\n producer?: KafkaProducerClient;\n /** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */\n retryTopics?: RetryTopics;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a poison record, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;\n}\n\n/**\n * Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,\n * routed to the handler for its URN (read from `bq-job`), and its offset committed only after\n * the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with\n * `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`\n * block. The `bq-attempts` header is the authoritative counter (the body is the fallback).\n */\nexport class KafkaConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly consumer: KafkaConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: KafkaConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */\n async run(): Promise<void> {\n await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });\n }\n\n /** Route + settle one record (process-then-commit). Exposed for testing. */\n async handle(payload: EachMessagePayload): Promise<void> {\n const { message } = payload;\n\n // Version gate (§6.7): decided on the header alone, before the value is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n this.report(\n new BabelQueueError(`Rejected a record from Kafka with unsupported bq-schema-version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`),\n null,\n payload,\n );\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n this.report(decodeError, null, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // A non-conformant / poison envelope may be missing the fields needed to annotate, so\n // forward the raw record to the DLQ rather than building a dead_letter block.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Kafka.\"), envelope, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const urn = headerString(message.headers, \"bq-job\") ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(payload, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n await this.commit(payload);\n } catch (error) {\n this.report(error, envelope, payload);\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError before this; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers);\n }\n\n private async deadLetterRaw(payload: EachMessagePayload): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return;\n this.requireProducer();\n const { message } = payload;\n // Forward the original record verbatim (value, key, headers) so the DLQ copy keeps its provenance\n // (Java reference; the .NET consumer forwards value + headers). Only the timestamp is re-stamped.\n const forwarded: KafkaProducerMessage = {\n value: message.value ?? Buffer.alloc(0),\n // KafkaJS accepts Buffer / array header values; the published structural type stays string-only.\n headers: copyHeaders(message.headers) as unknown as KafkaProducerMessage[\"headers\"],\n timestamp: String(Date.now()),\n };\n if (message.key !== undefined && message.key !== null) forwarded.key = Buffer.from(message.key);\n await this.options.producer!.send({ topic: dlq, messages: [forwarded] });\n }\n\n private async produce(topic: string, envelope: Envelope, headers: { [key: string]: string }): Promise<void> {\n this.requireProducer();\n await this.options.producer!.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }],\n });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n this.options.onError?.(error, envelope, payload);\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` header map **without clobbering**\n * the contract headers in `base` (they win a key collision) and dropping blanks. The result is\n * `base`, 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 Kafka adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed, **process-then-commit** consumer over\n * Apache Kafka (KafkaJS), so a Kafka-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * Kafka has **no native** per-message ack, delayed delivery, dead-letter queue, or delivery\n * counter — this adapter absorbs all four in the binding layer (the envelope stays\n * `schema_version: 1`): the envelope JSON is the record **value**; the contract fields are\n * mirrored onto `bq-` headers (route on `bq-job` without decoding the body) and the record\n * timestamp mirrors `meta.created_at`; **`bq-attempts` is the authoritative retry counter**;\n * consume is process-then-commit (manual commit, at-least-once); retry/delay use SDK-owned\n * tiered retry topics `<topic>.retry.<n>`; terminal failures go to an opt-in `<topic>.dlq`.\n *\n * This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you\n * provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter\n * structurally).\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 Kafka **record headers** beside the contract `bq-*` headers —\n * the contract headers win a key collision (merge-not-clobber), and blanks are dropped. On consume\n * the inbound record headers are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope (the record value) is never touched.\n *\n * **Version gate (§6.7).** Before decoding the record value the consumer reads the `bq-schema-version`\n * header. A missing or blank value (empty or only ASCII whitespace) follows the normal decode path;\n * any other value is compared, untrimmed, to the supported `\"1\"`, and a record that does not match is\n * rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and\n * headers) forwarded to the DLQ (when configured) and the offset committed.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Kafka shapes (a structural subset of kafkajs) ---------------------\n\n/** Incoming record headers (KafkaJS decodes header values to Buffers). */\nexport type IncomingHeaders = { [key: string]: Buffer | string | (Buffer | string)[] | null | undefined };\n\n/** A record to produce (structural subset of a KafkaJS message). */\nexport interface KafkaProducerMessage {\n key?: Buffer | string | null;\n value: Buffer | string;\n /**\n * Header values are strings for records the adapter builds. Raw DLQ forwarding (gate rejection,\n * decode error, non-conformant envelope) instead passes the inbound values through as\n * `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.\n */\n headers?: { [key: string]: string };\n timestamp?: string;\n}\n\n/** The subset of the KafkaJS producer this adapter calls. */\nexport interface KafkaProducerClient {\n send(record: { topic: string; messages: KafkaProducerMessage[] }): Promise<unknown>;\n}\n\n/** A received record (structural subset of a KafkaJS message). */\nexport interface KafkaIncomingMessage {\n value: Buffer | null;\n headers?: IncomingHeaders;\n offset: string;\n timestamp?: string;\n key?: Buffer | null;\n}\n\n/** The payload KafkaJS passes to `eachMessage`. */\nexport interface EachMessagePayload {\n topic: string;\n partition: number;\n message: KafkaIncomingMessage;\n}\n\n/** The subset of the KafkaJS consumer this adapter calls (manual commit — `autoCommit: false`). */\nexport interface KafkaConsumerClient {\n run(config: { autoCommit?: boolean; eachMessage: (payload: EachMessagePayload) => Promise<void> }): Promise<void>;\n commitOffsets(offsets: { topic: string; partition: number; offset: string }[]): Promise<void>;\n}\n\n// --- Header projection (contract §6.3) -----------------------------------------\n\n/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */\nexport function kafkaHeaders(envelope: Envelope): { [key: string]: string } {\n const headers: { [key: string]: string } = {};\n if (envelope.job) headers[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) headers[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) headers[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) headers[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n if (envelope.meta.lang) headers[\"bq-source-lang\"] = envelope.meta.lang;\n headers[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return headers;\n}\n\n/**\n * Copy inbound record headers for a raw DLQ forward: every value becomes a `Buffer` copy (a string is\n * UTF-8 encoded), array-valued headers keep all their entries, and `undefined` / `null` values are dropped.\n */\nfunction copyHeaders(headers: IncomingHeaders | undefined): { [key: string]: Buffer | Buffer[] } {\n const out: { [key: string]: Buffer | Buffer[] } = {};\n if (!headers) return out;\n const copy = (v: Buffer | string): Buffer => (Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from(v, \"utf8\"));\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n out[name] = value.filter((v) => v !== undefined && v !== null).map(copy);\n } else {\n out[name] = copy(value);\n }\n }\n return out;\n}\n\nfunction headerString(headers: IncomingHeaders | undefined, key: string): string | undefined {\n const value = headers?.[key];\n // A null header value is \"no header\" (broker-bindings.md §6.7): never stringified to \"null\".\n if (value === undefined || value === null) return undefined;\n const v = Array.isArray(value) ? value[0] : value;\n if (v === undefined || v === null) return undefined;\n return Buffer.isBuffer(v) ? v.toString(\"utf8\") : String(v);\n}\n\nfunction headerInt(headers: IncomingHeaders | undefined, key: string, fallback: number): number {\n const s = headerString(headers, key);\n if (s === undefined) return fallback;\n const n = Number.parseInt(s.trim(), 10);\n return Number.isNaN(n) ? fallback : n;\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq-schema-version` header when it is present, non-blank and not exactly the supported\n * version (compared untrimmed), else `undefined` (the record may be decoded).\n */\nfunction unsupportedSchemaVersion(message: KafkaIncomingMessage): string | undefined {\n const value = headerString(message.headers, \"bq-schema-version\");\n if (value === undefined || BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\n/**\n * Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer\n * values. Returns an empty object when there are none. Both the contract `bq-*` headers and any\n * out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it\n * knows.\n */\nexport function headersOf(message: KafkaIncomingMessage): HeaderCarrier {\n const headers = message.headers;\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headerString(headers, key);\n if (value !== undefined && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Retry / delay topology (contract §6.4–§6.5) -------------------------------\n\n/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */\nexport interface Tier {\n topic: string;\n delayMs: number;\n}\n\n/**\n * The SDK-owned retry/delay topology for one work topic: tiered delay topics\n * `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no\n * tiers configured raises rather than silently dropping.\n */\nexport class RetryTopics {\n readonly tiers: Tier[];\n readonly dlqTopic: string | null;\n\n constructor(\n readonly workTopic: string,\n delaysMs: number[] = [],\n dlqTopic: string | null | undefined = undefined,\n ) {\n this.tiers = [...delaysMs]\n .sort((a, b) => a - b)\n .map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));\n this.dlqTopic = dlqTopic === undefined ? `${workTopic}.dlq` : dlqTopic;\n }\n\n hasTiers(): boolean {\n return this.tiers.length > 0;\n }\n\n /** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */\n tierForDelay(delayMs: number): Tier {\n this.requireTiers();\n for (const tier of this.tiers) {\n if (tier.delayMs >= delayMs) return tier;\n }\n throw new BabelQueueError(\n `Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1]!.delayMs}ms).`,\n );\n }\n\n /** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */\n tierForAttempt(attempt: number): Tier {\n this.requireTiers();\n return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)]!;\n }\n\n private requireTiers(): void {\n if (!this.tiers.length) {\n throw new BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);\n }\n }\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link KafkaPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`\n * headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */\nexport class KafkaPublisher {\n private constructor(\n private readonly producer: KafkaProducerClient,\n private readonly workTopic: string,\n private readonly retryTopics?: RetryTopics,\n ) {}\n\n /** A publisher onto `topic` with no retry topics (a delay raises). */\n static create(producer: KafkaProducerClient, topic: string): KafkaPublisher {\n return new KafkaPublisher(producer, topic);\n }\n\n /** A publisher onto the topology's work topic, with delay routed via its retry tiers. */\n static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher {\n return new KafkaPublisher(producer, retryTopics.workTopic, retryTopics);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });\n if (options.delayMs != null && options.delayMs > 0) {\n if (!this.retryTopics) {\n throw new BabelQueueError(\"Kafka has no native delayed delivery; a delay requires retry topics (none configured).\");\n }\n const tier = this.retryTopics.tierForDelay(options.delayMs);\n await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope, options.headers);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(\n topic: string,\n envelope: Envelope,\n extra?: HeaderCarrier | null,\n delayMs?: number,\n originalTopic?: string,\n ): Promise<void> {\n // The contract bq-* headers are seeded first, then the out-of-band riders are merged beside\n // them without clobbering (the contract wins a collision).\n const headers = mergeInto(kafkaHeaders(envelope), extra);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- DLQ redrive IO (ADR-0026, §6.5 \"Record key on republish\") -------------------\n\n/**\n * A DLQ record reserved for {@link KafkaRedriveIO}: the structural shape `@babelqueue/core`'s\n * `RedriveMessage` expects (`body` + `ack`), plus the consumed record's `key` — `null`/absent for a\n * keyless record, a (possibly empty) `Buffer` otherwise.\n */\nexport interface KafkaRedriveMessage {\n body: string;\n key?: Buffer | null;\n ack(): Promise<void>;\n}\n\n/** Options for {@link KafkaRedriveIO}. */\nexport interface KafkaRedriveIOOptions {\n /** The producer used for every write the redrive makes (redrive write + restore to the DLQ). */\n producer: KafkaProducerClient;\n /**\n * Reserve the next record from `queue` (the DLQ topic), or `null` when it is empty. Kafka consumption\n * is push-based in KafkaJS, so the caller supplies the pull (a bounded fetch with manual commit) and\n * its `ack` commits the offset. Return the record's `key` so the redrive can carry it.\n */\n pop(queue: string): Promise<KafkaRedriveMessage | null>;\n}\n\n/**\n * A `@babelqueue/core` `RedriveIO` over Kafka that carries the DLQ record key byte-for-byte through the\n * redrive (contract §6.5): the redrive write back to the work topic and the restore write back to the\n * DLQ both go through {@link KafkaRedriveIO.publishFrom}, which copies the reserved record's key — a null\n * key stays null (no key on the record), an empty key stays empty — and never synthesises one. Partition\n * equality is not guaranteed (client partitioners differ). The §6.3 header projection is rebuilt from\n * the body being written (`bq-attempts` therefore reads 0 for a reset envelope); a body that is not a\n * canonical envelope (an undecodable record being restored) is written verbatim with no `bq-*` headers.\n *\n * Plain {@link KafkaRedriveIO.publish} writes a keyless record. `publishWithHeaders` is not offered, so\n * `bypass` is a no-op on this transport (as in the Go reference's keyed path).\n */\nexport class KafkaRedriveIO {\n constructor(private readonly options: KafkaRedriveIOOptions) {}\n\n pop(queue: string): Promise<KafkaRedriveMessage | null> {\n return this.options.pop(queue);\n }\n\n /** Append an already-encoded body to `queue` as a keyless record. */\n async publish(queue: string, body: string): Promise<void> {\n await this.send(queue, body, null);\n }\n\n /** Append `body` to `queue`, carrying the key of the reserved record `from` byte-for-byte. */\n async publishFrom(queue: string, body: string, from: KafkaRedriveMessage): Promise<void> {\n await this.send(queue, body, from.key ?? null);\n }\n\n private async send(queue: string, body: string, key: Buffer | null): Promise<void> {\n const record: KafkaProducerMessage = { value: body };\n const decoded = EnvelopeCodec.decode(body);\n if (EnvelopeCodec.accepts(decoded)) {\n record.headers = kafkaHeaders(decoded);\n record.timestamp = String(decoded.meta.created_at);\n }\n if (key !== null) record.key = Buffer.from(key);\n await this.options.producer.send({ topic: queue, messages: [record] });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band\n * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to\n * `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span\n * (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link KafkaConsumer}. */\nexport interface KafkaConsumerOptions {\n /** The producer used to republish retry/DLQ records (required for retry/DLQ). */\n producer?: KafkaProducerClient;\n /** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */\n retryTopics?: RetryTopics;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a poison record, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;\n}\n\n/**\n * Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,\n * routed to the handler for its URN (read from `bq-job`), and its offset committed only after\n * the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with\n * `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`\n * block. The `bq-attempts` header is the authoritative counter (the body is the fallback).\n */\nexport class KafkaConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly consumer: KafkaConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: KafkaConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */\n async run(): Promise<void> {\n await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });\n }\n\n /** Route + settle one record (process-then-commit). Exposed for testing. */\n async handle(payload: EachMessagePayload): Promise<void> {\n const { message } = payload;\n\n // Version gate (§6.7): decided on the header alone, before the value is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const rejected = new BabelQueueError(\n `Rejected a record from Kafka with unsupported bq-schema-version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(rejected, null, payload);\n }\n return;\n }\n\n const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(decodeError, null, payload);\n }\n return;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // A non-conformant / poison envelope may be missing the fields needed to annotate, so\n // forward the raw record to the DLQ rather than building a dead_letter block.\n const rejected = new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Kafka.\");\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(rejected, envelope, payload);\n }\n return;\n }\n\n const urn = headerString(message.headers, \"bq-job\") ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(payload, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n await this.commit(payload);\n } catch (error) {\n try {\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n } finally {\n this.report(error, envelope, payload);\n }\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError afterwards; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers, payload.message.key);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers, payload.message.key);\n }\n\n private async deadLetterRaw(payload: EachMessagePayload): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return;\n this.requireProducer();\n const { message } = payload;\n // Forward the original record verbatim (value, key, headers) so the DLQ copy keeps its provenance\n // (Java reference). Only the timestamp is re-stamped.\n const forwarded: KafkaProducerMessage = {\n value: message.value ?? Buffer.alloc(0),\n // KafkaJS accepts Buffer / array header values; the published structural type stays string-only.\n headers: copyHeaders(message.headers) as unknown as KafkaProducerMessage[\"headers\"],\n timestamp: String(Date.now()),\n };\n if (message.key !== undefined && message.key !== null) forwarded.key = Buffer.from(message.key);\n await this.options.producer!.send({ topic: dlq, messages: [forwarded] });\n }\n\n /**\n * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays\n * empty); a null/absent key is left off the record, never synthesised. Partition equality is not\n * guaranteed.\n */\n private async produce(\n topic: string,\n envelope: Envelope,\n headers: { [key: string]: string },\n key?: Buffer | null,\n ): Promise<void> {\n this.requireProducer();\n const record: KafkaProducerMessage = { value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) };\n if (key !== undefined && key !== null) record.key = Buffer.from(key);\n await this.options.producer!.send({ topic, messages: [record] });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n /**\n * Hand a failure to the user's `onError` callback. Callers invoke this only AFTER the record has been\n * settled (dead-lettered / retried and committed). A callback that throws synchronously, or returns a\n * promise that later rejects (an `async` function is assignable to the `void` callback type), is\n * swallowed here so user code can never bypass settlement or stop the consumer.\n */\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n try {\n const returned: unknown = this.options.onError?.(error, envelope, payload);\n if (isThenable(returned)) {\n // An async callback's rejection would otherwise surface as an unhandled rejection.\n returned.then(undefined, () => undefined);\n }\n } catch {\n // user callback failure must not escape the consume loop\n }\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` header map **without clobbering**\n * the contract headers in `base` (they win a key collision) and dropping blanks. The result is\n * `base`, 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":[]}
package/dist/index.d.cts CHANGED
@@ -143,6 +143,49 @@ declare class KafkaPublisher {
143
143
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
144
144
  private sendRecord;
145
145
  }
146
+ /**
147
+ * A DLQ record reserved for {@link KafkaRedriveIO}: the structural shape `@babelqueue/core`'s
148
+ * `RedriveMessage` expects (`body` + `ack`), plus the consumed record's `key` — `null`/absent for a
149
+ * keyless record, a (possibly empty) `Buffer` otherwise.
150
+ */
151
+ interface KafkaRedriveMessage {
152
+ body: string;
153
+ key?: Buffer | null;
154
+ ack(): Promise<void>;
155
+ }
156
+ /** Options for {@link KafkaRedriveIO}. */
157
+ interface KafkaRedriveIOOptions {
158
+ /** The producer used for every write the redrive makes (redrive write + restore to the DLQ). */
159
+ producer: KafkaProducerClient;
160
+ /**
161
+ * Reserve the next record from `queue` (the DLQ topic), or `null` when it is empty. Kafka consumption
162
+ * is push-based in KafkaJS, so the caller supplies the pull (a bounded fetch with manual commit) and
163
+ * its `ack` commits the offset. Return the record's `key` so the redrive can carry it.
164
+ */
165
+ pop(queue: string): Promise<KafkaRedriveMessage | null>;
166
+ }
167
+ /**
168
+ * A `@babelqueue/core` `RedriveIO` over Kafka that carries the DLQ record key byte-for-byte through the
169
+ * redrive (contract §6.5): the redrive write back to the work topic and the restore write back to the
170
+ * DLQ both go through {@link KafkaRedriveIO.publishFrom}, which copies the reserved record's key — a null
171
+ * key stays null (no key on the record), an empty key stays empty — and never synthesises one. Partition
172
+ * equality is not guaranteed (client partitioners differ). The §6.3 header projection is rebuilt from
173
+ * the body being written (`bq-attempts` therefore reads 0 for a reset envelope); a body that is not a
174
+ * canonical envelope (an undecodable record being restored) is written verbatim with no `bq-*` headers.
175
+ *
176
+ * Plain {@link KafkaRedriveIO.publish} writes a keyless record. `publishWithHeaders` is not offered, so
177
+ * `bypass` is a no-op on this transport (as in the Go reference's keyed path).
178
+ */
179
+ declare class KafkaRedriveIO {
180
+ private readonly options;
181
+ constructor(options: KafkaRedriveIOOptions);
182
+ pop(queue: string): Promise<KafkaRedriveMessage | null>;
183
+ /** Append an already-encoded body to `queue` as a keyless record. */
184
+ publish(queue: string, body: string): Promise<void>;
185
+ /** Append `body` to `queue`, carrying the key of the reserved record `from` byte-for-byte. */
186
+ publishFrom(queue: string, body: string, from: KafkaRedriveMessage): Promise<void>;
187
+ private send;
188
+ }
146
189
  /**
147
190
  * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band
148
191
  * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to
@@ -189,11 +232,22 @@ declare class KafkaConsumer {
189
232
  private republishRetry;
190
233
  private deadLetter;
191
234
  private deadLetterRaw;
235
+ /**
236
+ * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
237
+ * empty); a null/absent key is left off the record, never synthesised. Partition equality is not
238
+ * guaranteed.
239
+ */
192
240
  private produce;
193
241
  private commit;
194
242
  private originalTopic;
195
243
  private requireProducer;
244
+ /**
245
+ * Hand a failure to the user's `onError` callback. Callers invoke this only AFTER the record has been
246
+ * settled (dead-lettered / retried and committed). A callback that throws synchronously, or returns a
247
+ * promise that later rejects (an `async` function is assignable to the `void` callback type), is
248
+ * swallowed here so user code can never bypass settlement or stop the consumer.
249
+ */
196
250
  private report;
197
251
  }
198
252
 
199
- export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
253
+ export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, KafkaRedriveIO, type KafkaRedriveIOOptions, type KafkaRedriveMessage, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
package/dist/index.d.ts CHANGED
@@ -143,6 +143,49 @@ declare class KafkaPublisher {
143
143
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
144
144
  private sendRecord;
145
145
  }
146
+ /**
147
+ * A DLQ record reserved for {@link KafkaRedriveIO}: the structural shape `@babelqueue/core`'s
148
+ * `RedriveMessage` expects (`body` + `ack`), plus the consumed record's `key` — `null`/absent for a
149
+ * keyless record, a (possibly empty) `Buffer` otherwise.
150
+ */
151
+ interface KafkaRedriveMessage {
152
+ body: string;
153
+ key?: Buffer | null;
154
+ ack(): Promise<void>;
155
+ }
156
+ /** Options for {@link KafkaRedriveIO}. */
157
+ interface KafkaRedriveIOOptions {
158
+ /** The producer used for every write the redrive makes (redrive write + restore to the DLQ). */
159
+ producer: KafkaProducerClient;
160
+ /**
161
+ * Reserve the next record from `queue` (the DLQ topic), or `null` when it is empty. Kafka consumption
162
+ * is push-based in KafkaJS, so the caller supplies the pull (a bounded fetch with manual commit) and
163
+ * its `ack` commits the offset. Return the record's `key` so the redrive can carry it.
164
+ */
165
+ pop(queue: string): Promise<KafkaRedriveMessage | null>;
166
+ }
167
+ /**
168
+ * A `@babelqueue/core` `RedriveIO` over Kafka that carries the DLQ record key byte-for-byte through the
169
+ * redrive (contract §6.5): the redrive write back to the work topic and the restore write back to the
170
+ * DLQ both go through {@link KafkaRedriveIO.publishFrom}, which copies the reserved record's key — a null
171
+ * key stays null (no key on the record), an empty key stays empty — and never synthesises one. Partition
172
+ * equality is not guaranteed (client partitioners differ). The §6.3 header projection is rebuilt from
173
+ * the body being written (`bq-attempts` therefore reads 0 for a reset envelope); a body that is not a
174
+ * canonical envelope (an undecodable record being restored) is written verbatim with no `bq-*` headers.
175
+ *
176
+ * Plain {@link KafkaRedriveIO.publish} writes a keyless record. `publishWithHeaders` is not offered, so
177
+ * `bypass` is a no-op on this transport (as in the Go reference's keyed path).
178
+ */
179
+ declare class KafkaRedriveIO {
180
+ private readonly options;
181
+ constructor(options: KafkaRedriveIOOptions);
182
+ pop(queue: string): Promise<KafkaRedriveMessage | null>;
183
+ /** Append an already-encoded body to `queue` as a keyless record. */
184
+ publish(queue: string, body: string): Promise<void>;
185
+ /** Append `body` to `queue`, carrying the key of the reserved record `from` byte-for-byte. */
186
+ publishFrom(queue: string, body: string, from: KafkaRedriveMessage): Promise<void>;
187
+ private send;
188
+ }
146
189
  /**
147
190
  * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band
148
191
  * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to
@@ -189,11 +232,22 @@ declare class KafkaConsumer {
189
232
  private republishRetry;
190
233
  private deadLetter;
191
234
  private deadLetterRaw;
235
+ /**
236
+ * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
237
+ * empty); a null/absent key is left off the record, never synthesised. Partition equality is not
238
+ * guaranteed.
239
+ */
192
240
  private produce;
193
241
  private commit;
194
242
  private originalTopic;
195
243
  private requireProducer;
244
+ /**
245
+ * Hand a failure to the user's `onError` callback. Callers invoke this only AFTER the record has been
246
+ * settled (dead-lettered / retried and committed). A callback that throws synchronously, or returns a
247
+ * promise that later rejects (an `async` function is assignable to the `void` callback type), is
248
+ * swallowed here so user code can never bypass settlement or stop the consumer.
249
+ */
196
250
  private report;
197
251
  }
198
252
 
199
- export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
253
+ export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, KafkaRedriveIO, type KafkaRedriveIOOptions, type KafkaRedriveMessage, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
package/dist/index.js CHANGED
@@ -58,6 +58,9 @@ function headerInt(headers, key, fallback) {
58
58
  const n = Number.parseInt(s.trim(), 10);
59
59
  return Number.isNaN(n) ? fallback : n;
60
60
  }
61
+ function isThenable(value) {
62
+ return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
63
+ }
61
64
  var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
62
65
  function unsupportedSchemaVersion(message) {
63
66
  const value = headerString(message.headers, "bq-schema-version");
@@ -148,6 +151,33 @@ var KafkaPublisher = class _KafkaPublisher {
148
151
  });
149
152
  }
150
153
  };
154
+ var KafkaRedriveIO = class {
155
+ constructor(options) {
156
+ this.options = options;
157
+ }
158
+ options;
159
+ pop(queue) {
160
+ return this.options.pop(queue);
161
+ }
162
+ /** Append an already-encoded body to `queue` as a keyless record. */
163
+ async publish(queue, body) {
164
+ await this.send(queue, body, null);
165
+ }
166
+ /** Append `body` to `queue`, carrying the key of the reserved record `from` byte-for-byte. */
167
+ async publishFrom(queue, body, from) {
168
+ await this.send(queue, body, from.key ?? null);
169
+ }
170
+ async send(queue, body, key) {
171
+ const record = { value: body };
172
+ const decoded = EnvelopeCodec.decode(body);
173
+ if (EnvelopeCodec.accepts(decoded)) {
174
+ record.headers = kafkaHeaders(decoded);
175
+ record.timestamp = String(decoded.meta.created_at);
176
+ }
177
+ if (key !== null) record.key = Buffer.from(key);
178
+ await this.options.producer.send({ topic: queue, messages: [record] });
179
+ }
180
+ };
151
181
  var KafkaConsumer = class {
152
182
  constructor(consumer, handlers, options = {}) {
153
183
  this.consumer = consumer;
@@ -170,13 +200,15 @@ var KafkaConsumer = class {
170
200
  const { message } = payload;
171
201
  const unsupported = unsupportedSchemaVersion(message);
172
202
  if (unsupported !== void 0) {
173
- this.report(
174
- new BabelQueueError(`Rejected a record from Kafka with unsupported bq-schema-version "${unsupported}" (supported: ${SCHEMA_VERSION}).`),
175
- null,
176
- payload
203
+ const rejected = new BabelQueueError(
204
+ `Rejected a record from Kafka with unsupported bq-schema-version "${unsupported}" (supported: ${SCHEMA_VERSION}).`
177
205
  );
178
- await this.deadLetterRaw(payload);
179
- await this.commit(payload);
206
+ try {
207
+ await this.deadLetterRaw(payload);
208
+ await this.commit(payload);
209
+ } finally {
210
+ this.report(rejected, null, payload);
211
+ }
180
212
  return;
181
213
  }
182
214
  const raw = message.value ? message.value.toString("utf8") : "";
@@ -184,15 +216,22 @@ var KafkaConsumer = class {
184
216
  try {
185
217
  envelope = this.reconcile(EnvelopeCodec.decode(raw), message);
186
218
  } catch (decodeError) {
187
- this.report(decodeError, null, payload);
188
- await this.deadLetterRaw(payload);
189
- await this.commit(payload);
219
+ try {
220
+ await this.deadLetterRaw(payload);
221
+ await this.commit(payload);
222
+ } finally {
223
+ this.report(decodeError, null, payload);
224
+ }
190
225
  return;
191
226
  }
192
227
  if (!EnvelopeCodec.accepts(envelope)) {
193
- this.report(new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Kafka."), envelope, payload);
194
- await this.deadLetterRaw(payload);
195
- await this.commit(payload);
228
+ const rejected = new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Kafka.");
229
+ try {
230
+ await this.deadLetterRaw(payload);
231
+ await this.commit(payload);
232
+ } finally {
233
+ this.report(rejected, envelope, payload);
234
+ }
196
235
  return;
197
236
  }
198
237
  const urn = headerString(message.headers, "bq-job") ?? EnvelopeCodec.urn(envelope);
@@ -205,9 +244,12 @@ var KafkaConsumer = class {
205
244
  await handler(envelope, message, headersOf(message));
206
245
  await this.commit(payload);
207
246
  } catch (error) {
208
- this.report(error, envelope, payload);
209
- await this.retryOrDeadLetter(payload, envelope, error);
210
- await this.commit(payload);
247
+ try {
248
+ await this.retryOrDeadLetter(payload, envelope, error);
249
+ await this.commit(payload);
250
+ } finally {
251
+ this.report(error, envelope, payload);
252
+ }
211
253
  }
212
254
  }
213
255
  reconcile(envelope, message) {
@@ -257,7 +299,7 @@ var KafkaConsumer = class {
257
299
  const headers = kafkaHeaders(bumped);
258
300
  headers["bq-delay"] = String(tier.delayMs);
259
301
  headers["bq-original-topic"] = this.originalTopic(payload);
260
- await this.produce(tier.topic, bumped, headers);
302
+ await this.produce(tier.topic, bumped, headers, payload.message.key);
261
303
  }
262
304
  async deadLetter(envelope, payload, reason, error) {
263
305
  const dlq = this.options.retryTopics?.dlqTopic ?? null;
@@ -270,7 +312,7 @@ var KafkaConsumer = class {
270
312
  });
271
313
  const headers = kafkaHeaders(annotated);
272
314
  headers["bq-original-topic"] = original;
273
- await this.produce(dlq, annotated, headers);
315
+ await this.produce(dlq, annotated, headers, payload.message.key);
274
316
  }
275
317
  async deadLetterRaw(payload) {
276
318
  const dlq = this.options.retryTopics?.dlqTopic ?? null;
@@ -286,12 +328,16 @@ var KafkaConsumer = class {
286
328
  if (message.key !== void 0 && message.key !== null) forwarded.key = Buffer.from(message.key);
287
329
  await this.options.producer.send({ topic: dlq, messages: [forwarded] });
288
330
  }
289
- async produce(topic, envelope, headers) {
331
+ /**
332
+ * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
333
+ * empty); a null/absent key is left off the record, never synthesised. Partition equality is not
334
+ * guaranteed.
335
+ */
336
+ async produce(topic, envelope, headers, key) {
290
337
  this.requireProducer();
291
- await this.options.producer.send({
292
- topic,
293
- messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }]
294
- });
338
+ const record = { value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) };
339
+ if (key !== void 0 && key !== null) record.key = Buffer.from(key);
340
+ await this.options.producer.send({ topic, messages: [record] });
295
341
  }
296
342
  async commit(payload) {
297
343
  await this.consumer.commitOffsets([
@@ -306,13 +352,26 @@ var KafkaConsumer = class {
306
352
  throw new BabelQueueError("This Kafka consumer needs a producer to republish (retry/DLQ).");
307
353
  }
308
354
  }
355
+ /**
356
+ * Hand a failure to the user's `onError` callback. Callers invoke this only AFTER the record has been
357
+ * settled (dead-lettered / retried and committed). A callback that throws synchronously, or returns a
358
+ * promise that later rejects (an `async` function is assignable to the `void` callback type), is
359
+ * swallowed here so user code can never bypass settlement or stop the consumer.
360
+ */
309
361
  report(error, envelope, payload) {
310
- this.options.onError?.(error, envelope, payload);
362
+ try {
363
+ const returned = this.options.onError?.(error, envelope, payload);
364
+ if (isThenable(returned)) {
365
+ returned.then(void 0, () => void 0);
366
+ }
367
+ } catch {
368
+ }
311
369
  }
312
370
  };
313
371
  export {
314
372
  KafkaConsumer,
315
373
  KafkaPublisher,
374
+ KafkaRedriveIO,
316
375
  RetryTopics,
317
376
  headersOf,
318
377
  kafkaHeaders
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache Kafka adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed, **process-then-commit** consumer over\n * Apache Kafka (KafkaJS), so a Kafka-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * Kafka has **no native** per-message ack, delayed delivery, dead-letter queue, or delivery\n * counter — this adapter absorbs all four in the binding layer (the envelope stays\n * `schema_version: 1`): the envelope JSON is the record **value**; the contract fields are\n * mirrored onto `bq-` headers (route on `bq-job` without decoding the body) and the record\n * timestamp mirrors `meta.created_at`; **`bq-attempts` is the authoritative retry counter**;\n * consume is process-then-commit (manual commit, at-least-once); retry/delay use SDK-owned\n * tiered retry topics `<topic>.retry.<n>`; terminal failures go to an opt-in `<topic>.dlq`.\n *\n * This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you\n * provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter\n * structurally).\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 Kafka **record headers** beside the contract `bq-*` headers —\n * the contract headers win a key collision (merge-not-clobber), and blanks are dropped. On consume\n * the inbound record headers are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope (the record value) is never touched.\n *\n * **Version gate (§6.7).** Before decoding the record value the consumer reads the `bq-schema-version`\n * header. A missing or blank value (empty or only ASCII whitespace) follows the normal decode path;\n * any other value is compared, untrimmed, to the supported `\"1\"`, and a record that does not match is\n * rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and\n * headers) forwarded to the DLQ (when configured) and the offset committed.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Kafka shapes (a structural subset of kafkajs) ---------------------\n\n/** Incoming record headers (KafkaJS decodes header values to Buffers). */\nexport type IncomingHeaders = { [key: string]: Buffer | string | (Buffer | string)[] | null | undefined };\n\n/** A record to produce (structural subset of a KafkaJS message). */\nexport interface KafkaProducerMessage {\n key?: Buffer | string | null;\n value: Buffer | string;\n /**\n * Header values are strings for records the adapter builds. Raw DLQ forwarding (gate rejection,\n * decode error, non-conformant envelope) instead passes the inbound values through as\n * `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.\n */\n headers?: { [key: string]: string };\n timestamp?: string;\n}\n\n/** The subset of the KafkaJS producer this adapter calls. */\nexport interface KafkaProducerClient {\n send(record: { topic: string; messages: KafkaProducerMessage[] }): Promise<unknown>;\n}\n\n/** A received record (structural subset of a KafkaJS message). */\nexport interface KafkaIncomingMessage {\n value: Buffer | null;\n headers?: IncomingHeaders;\n offset: string;\n timestamp?: string;\n key?: Buffer | null;\n}\n\n/** The payload KafkaJS passes to `eachMessage`. */\nexport interface EachMessagePayload {\n topic: string;\n partition: number;\n message: KafkaIncomingMessage;\n}\n\n/** The subset of the KafkaJS consumer this adapter calls (manual commit — `autoCommit: false`). */\nexport interface KafkaConsumerClient {\n run(config: { autoCommit?: boolean; eachMessage: (payload: EachMessagePayload) => Promise<void> }): Promise<void>;\n commitOffsets(offsets: { topic: string; partition: number; offset: string }[]): Promise<void>;\n}\n\n// --- Header projection (contract §6.3) -----------------------------------------\n\n/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */\nexport function kafkaHeaders(envelope: Envelope): { [key: string]: string } {\n const headers: { [key: string]: string } = {};\n if (envelope.job) headers[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) headers[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) headers[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) headers[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n if (envelope.meta.lang) headers[\"bq-source-lang\"] = envelope.meta.lang;\n headers[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return headers;\n}\n\n/**\n * Copy inbound record headers for a raw DLQ forward: every value becomes a `Buffer` copy (a string is\n * UTF-8 encoded), array-valued headers keep all their entries, and `undefined` / `null` values are dropped.\n */\nfunction copyHeaders(headers: IncomingHeaders | undefined): { [key: string]: Buffer | Buffer[] } {\n const out: { [key: string]: Buffer | Buffer[] } = {};\n if (!headers) return out;\n const copy = (v: Buffer | string): Buffer => (Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from(v, \"utf8\"));\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n out[name] = value.filter((v) => v !== undefined && v !== null).map(copy);\n } else {\n out[name] = copy(value);\n }\n }\n return out;\n}\n\nfunction headerString(headers: IncomingHeaders | undefined, key: string): string | undefined {\n const value = headers?.[key];\n // A null header value is \"no header\" (broker-bindings.md §6.7): never stringified to \"null\".\n if (value === undefined || value === null) return undefined;\n const v = Array.isArray(value) ? value[0] : value;\n if (v === undefined || v === null) return undefined;\n return Buffer.isBuffer(v) ? v.toString(\"utf8\") : String(v);\n}\n\nfunction headerInt(headers: IncomingHeaders | undefined, key: string, fallback: number): number {\n const s = headerString(headers, key);\n if (s === undefined) return fallback;\n const n = Number.parseInt(s.trim(), 10);\n return Number.isNaN(n) ? fallback : n;\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq-schema-version` header when it is present, non-blank and not exactly the supported\n * version (compared untrimmed), else `undefined` (the record may be decoded).\n */\nfunction unsupportedSchemaVersion(message: KafkaIncomingMessage): string | undefined {\n const value = headerString(message.headers, \"bq-schema-version\");\n if (value === undefined || BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\n/**\n * Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer\n * values. Returns an empty object when there are none. Both the contract `bq-*` headers and any\n * out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it\n * knows.\n */\nexport function headersOf(message: KafkaIncomingMessage): HeaderCarrier {\n const headers = message.headers;\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headerString(headers, key);\n if (value !== undefined && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Retry / delay topology (contract §6.4–§6.5) -------------------------------\n\n/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */\nexport interface Tier {\n topic: string;\n delayMs: number;\n}\n\n/**\n * The SDK-owned retry/delay topology for one work topic: tiered delay topics\n * `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no\n * tiers configured raises rather than silently dropping.\n */\nexport class RetryTopics {\n readonly tiers: Tier[];\n readonly dlqTopic: string | null;\n\n constructor(\n readonly workTopic: string,\n delaysMs: number[] = [],\n dlqTopic: string | null | undefined = undefined,\n ) {\n this.tiers = [...delaysMs]\n .sort((a, b) => a - b)\n .map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));\n this.dlqTopic = dlqTopic === undefined ? `${workTopic}.dlq` : dlqTopic;\n }\n\n hasTiers(): boolean {\n return this.tiers.length > 0;\n }\n\n /** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */\n tierForDelay(delayMs: number): Tier {\n this.requireTiers();\n for (const tier of this.tiers) {\n if (tier.delayMs >= delayMs) return tier;\n }\n throw new BabelQueueError(\n `Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1]!.delayMs}ms).`,\n );\n }\n\n /** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */\n tierForAttempt(attempt: number): Tier {\n this.requireTiers();\n return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)]!;\n }\n\n private requireTiers(): void {\n if (!this.tiers.length) {\n throw new BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);\n }\n }\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link KafkaPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`\n * headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */\nexport class KafkaPublisher {\n private constructor(\n private readonly producer: KafkaProducerClient,\n private readonly workTopic: string,\n private readonly retryTopics?: RetryTopics,\n ) {}\n\n /** A publisher onto `topic` with no retry topics (a delay raises). */\n static create(producer: KafkaProducerClient, topic: string): KafkaPublisher {\n return new KafkaPublisher(producer, topic);\n }\n\n /** A publisher onto the topology's work topic, with delay routed via its retry tiers. */\n static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher {\n return new KafkaPublisher(producer, retryTopics.workTopic, retryTopics);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });\n if (options.delayMs != null && options.delayMs > 0) {\n if (!this.retryTopics) {\n throw new BabelQueueError(\"Kafka has no native delayed delivery; a delay requires retry topics (none configured).\");\n }\n const tier = this.retryTopics.tierForDelay(options.delayMs);\n await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope, options.headers);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(\n topic: string,\n envelope: Envelope,\n extra?: HeaderCarrier | null,\n delayMs?: number,\n originalTopic?: string,\n ): Promise<void> {\n // The contract bq-* headers are seeded first, then the out-of-band riders are merged beside\n // them without clobbering (the contract wins a collision).\n const headers = mergeInto(kafkaHeaders(envelope), extra);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band\n * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to\n * `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span\n * (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link KafkaConsumer}. */\nexport interface KafkaConsumerOptions {\n /** The producer used to republish retry/DLQ records (required for retry/DLQ). */\n producer?: KafkaProducerClient;\n /** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */\n retryTopics?: RetryTopics;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a poison record, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;\n}\n\n/**\n * Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,\n * routed to the handler for its URN (read from `bq-job`), and its offset committed only after\n * the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with\n * `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`\n * block. The `bq-attempts` header is the authoritative counter (the body is the fallback).\n */\nexport class KafkaConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly consumer: KafkaConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: KafkaConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */\n async run(): Promise<void> {\n await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });\n }\n\n /** Route + settle one record (process-then-commit). Exposed for testing. */\n async handle(payload: EachMessagePayload): Promise<void> {\n const { message } = payload;\n\n // Version gate (§6.7): decided on the header alone, before the value is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n this.report(\n new BabelQueueError(`Rejected a record from Kafka with unsupported bq-schema-version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`),\n null,\n payload,\n );\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n this.report(decodeError, null, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // A non-conformant / poison envelope may be missing the fields needed to annotate, so\n // forward the raw record to the DLQ rather than building a dead_letter block.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Kafka.\"), envelope, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const urn = headerString(message.headers, \"bq-job\") ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(payload, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n await this.commit(payload);\n } catch (error) {\n this.report(error, envelope, payload);\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError before this; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers);\n }\n\n private async deadLetterRaw(payload: EachMessagePayload): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return;\n this.requireProducer();\n const { message } = payload;\n // Forward the original record verbatim (value, key, headers) so the DLQ copy keeps its provenance\n // (Java reference; the .NET consumer forwards value + headers). Only the timestamp is re-stamped.\n const forwarded: KafkaProducerMessage = {\n value: message.value ?? Buffer.alloc(0),\n // KafkaJS accepts Buffer / array header values; the published structural type stays string-only.\n headers: copyHeaders(message.headers) as unknown as KafkaProducerMessage[\"headers\"],\n timestamp: String(Date.now()),\n };\n if (message.key !== undefined && message.key !== null) forwarded.key = Buffer.from(message.key);\n await this.options.producer!.send({ topic: dlq, messages: [forwarded] });\n }\n\n private async produce(topic: string, envelope: Envelope, headers: { [key: string]: string }): Promise<void> {\n this.requireProducer();\n await this.options.producer!.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }],\n });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n this.options.onError?.(error, envelope, payload);\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` header map **without clobbering**\n * the contract headers in `base` (they win a key collision) and dropping blanks. The result is\n * `base`, 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 Kafka adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed, **process-then-commit** consumer over\n * Apache Kafka (KafkaJS), so a Kafka-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * Kafka has **no native** per-message ack, delayed delivery, dead-letter queue, or delivery\n * counter — this adapter absorbs all four in the binding layer (the envelope stays\n * `schema_version: 1`): the envelope JSON is the record **value**; the contract fields are\n * mirrored onto `bq-` headers (route on `bq-job` without decoding the body) and the record\n * timestamp mirrors `meta.created_at`; **`bq-attempts` is the authoritative retry counter**;\n * consume is process-then-commit (manual commit, at-least-once); retry/delay use SDK-owned\n * tiered retry topics `<topic>.retry.<n>`; terminal failures go to an opt-in `<topic>.dlq`.\n *\n * This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you\n * provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter\n * structurally).\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 Kafka **record headers** beside the contract `bq-*` headers —\n * the contract headers win a key collision (merge-not-clobber), and blanks are dropped. On consume\n * the inbound record headers are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope (the record value) is never touched.\n *\n * **Version gate (§6.7).** Before decoding the record value the consumer reads the `bq-schema-version`\n * header. A missing or blank value (empty or only ASCII whitespace) follows the normal decode path;\n * any other value is compared, untrimmed, to the supported `\"1\"`, and a record that does not match is\n * rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and\n * headers) forwarded to the DLQ (when configured) and the offset committed.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Kafka shapes (a structural subset of kafkajs) ---------------------\n\n/** Incoming record headers (KafkaJS decodes header values to Buffers). */\nexport type IncomingHeaders = { [key: string]: Buffer | string | (Buffer | string)[] | null | undefined };\n\n/** A record to produce (structural subset of a KafkaJS message). */\nexport interface KafkaProducerMessage {\n key?: Buffer | string | null;\n value: Buffer | string;\n /**\n * Header values are strings for records the adapter builds. Raw DLQ forwarding (gate rejection,\n * decode error, non-conformant envelope) instead passes the inbound values through as\n * `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.\n */\n headers?: { [key: string]: string };\n timestamp?: string;\n}\n\n/** The subset of the KafkaJS producer this adapter calls. */\nexport interface KafkaProducerClient {\n send(record: { topic: string; messages: KafkaProducerMessage[] }): Promise<unknown>;\n}\n\n/** A received record (structural subset of a KafkaJS message). */\nexport interface KafkaIncomingMessage {\n value: Buffer | null;\n headers?: IncomingHeaders;\n offset: string;\n timestamp?: string;\n key?: Buffer | null;\n}\n\n/** The payload KafkaJS passes to `eachMessage`. */\nexport interface EachMessagePayload {\n topic: string;\n partition: number;\n message: KafkaIncomingMessage;\n}\n\n/** The subset of the KafkaJS consumer this adapter calls (manual commit — `autoCommit: false`). */\nexport interface KafkaConsumerClient {\n run(config: { autoCommit?: boolean; eachMessage: (payload: EachMessagePayload) => Promise<void> }): Promise<void>;\n commitOffsets(offsets: { topic: string; partition: number; offset: string }[]): Promise<void>;\n}\n\n// --- Header projection (contract §6.3) -----------------------------------------\n\n/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */\nexport function kafkaHeaders(envelope: Envelope): { [key: string]: string } {\n const headers: { [key: string]: string } = {};\n if (envelope.job) headers[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) headers[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) headers[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) headers[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n if (envelope.meta.lang) headers[\"bq-source-lang\"] = envelope.meta.lang;\n headers[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return headers;\n}\n\n/**\n * Copy inbound record headers for a raw DLQ forward: every value becomes a `Buffer` copy (a string is\n * UTF-8 encoded), array-valued headers keep all their entries, and `undefined` / `null` values are dropped.\n */\nfunction copyHeaders(headers: IncomingHeaders | undefined): { [key: string]: Buffer | Buffer[] } {\n const out: { [key: string]: Buffer | Buffer[] } = {};\n if (!headers) return out;\n const copy = (v: Buffer | string): Buffer => (Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from(v, \"utf8\"));\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n out[name] = value.filter((v) => v !== undefined && v !== null).map(copy);\n } else {\n out[name] = copy(value);\n }\n }\n return out;\n}\n\nfunction headerString(headers: IncomingHeaders | undefined, key: string): string | undefined {\n const value = headers?.[key];\n // A null header value is \"no header\" (broker-bindings.md §6.7): never stringified to \"null\".\n if (value === undefined || value === null) return undefined;\n const v = Array.isArray(value) ? value[0] : value;\n if (v === undefined || v === null) return undefined;\n return Buffer.isBuffer(v) ? v.toString(\"utf8\") : String(v);\n}\n\nfunction headerInt(headers: IncomingHeaders | undefined, key: string, fallback: number): number {\n const s = headerString(headers, key);\n if (s === undefined) return fallback;\n const n = Number.parseInt(s.trim(), 10);\n return Number.isNaN(n) ? fallback : n;\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq-schema-version` header when it is present, non-blank and not exactly the supported\n * version (compared untrimmed), else `undefined` (the record may be decoded).\n */\nfunction unsupportedSchemaVersion(message: KafkaIncomingMessage): string | undefined {\n const value = headerString(message.headers, \"bq-schema-version\");\n if (value === undefined || BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\n/**\n * Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer\n * values. Returns an empty object when there are none. Both the contract `bq-*` headers and any\n * out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it\n * knows.\n */\nexport function headersOf(message: KafkaIncomingMessage): HeaderCarrier {\n const headers = message.headers;\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headerString(headers, key);\n if (value !== undefined && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Retry / delay topology (contract §6.4–§6.5) -------------------------------\n\n/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */\nexport interface Tier {\n topic: string;\n delayMs: number;\n}\n\n/**\n * The SDK-owned retry/delay topology for one work topic: tiered delay topics\n * `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no\n * tiers configured raises rather than silently dropping.\n */\nexport class RetryTopics {\n readonly tiers: Tier[];\n readonly dlqTopic: string | null;\n\n constructor(\n readonly workTopic: string,\n delaysMs: number[] = [],\n dlqTopic: string | null | undefined = undefined,\n ) {\n this.tiers = [...delaysMs]\n .sort((a, b) => a - b)\n .map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));\n this.dlqTopic = dlqTopic === undefined ? `${workTopic}.dlq` : dlqTopic;\n }\n\n hasTiers(): boolean {\n return this.tiers.length > 0;\n }\n\n /** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */\n tierForDelay(delayMs: number): Tier {\n this.requireTiers();\n for (const tier of this.tiers) {\n if (tier.delayMs >= delayMs) return tier;\n }\n throw new BabelQueueError(\n `Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1]!.delayMs}ms).`,\n );\n }\n\n /** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */\n tierForAttempt(attempt: number): Tier {\n this.requireTiers();\n return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)]!;\n }\n\n private requireTiers(): void {\n if (!this.tiers.length) {\n throw new BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);\n }\n }\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link KafkaPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`\n * headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */\nexport class KafkaPublisher {\n private constructor(\n private readonly producer: KafkaProducerClient,\n private readonly workTopic: string,\n private readonly retryTopics?: RetryTopics,\n ) {}\n\n /** A publisher onto `topic` with no retry topics (a delay raises). */\n static create(producer: KafkaProducerClient, topic: string): KafkaPublisher {\n return new KafkaPublisher(producer, topic);\n }\n\n /** A publisher onto the topology's work topic, with delay routed via its retry tiers. */\n static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher {\n return new KafkaPublisher(producer, retryTopics.workTopic, retryTopics);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });\n if (options.delayMs != null && options.delayMs > 0) {\n if (!this.retryTopics) {\n throw new BabelQueueError(\"Kafka has no native delayed delivery; a delay requires retry topics (none configured).\");\n }\n const tier = this.retryTopics.tierForDelay(options.delayMs);\n await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope, options.headers);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(\n topic: string,\n envelope: Envelope,\n extra?: HeaderCarrier | null,\n delayMs?: number,\n originalTopic?: string,\n ): Promise<void> {\n // The contract bq-* headers are seeded first, then the out-of-band riders are merged beside\n // them without clobbering (the contract wins a collision).\n const headers = mergeInto(kafkaHeaders(envelope), extra);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- DLQ redrive IO (ADR-0026, §6.5 \"Record key on republish\") -------------------\n\n/**\n * A DLQ record reserved for {@link KafkaRedriveIO}: the structural shape `@babelqueue/core`'s\n * `RedriveMessage` expects (`body` + `ack`), plus the consumed record's `key` — `null`/absent for a\n * keyless record, a (possibly empty) `Buffer` otherwise.\n */\nexport interface KafkaRedriveMessage {\n body: string;\n key?: Buffer | null;\n ack(): Promise<void>;\n}\n\n/** Options for {@link KafkaRedriveIO}. */\nexport interface KafkaRedriveIOOptions {\n /** The producer used for every write the redrive makes (redrive write + restore to the DLQ). */\n producer: KafkaProducerClient;\n /**\n * Reserve the next record from `queue` (the DLQ topic), or `null` when it is empty. Kafka consumption\n * is push-based in KafkaJS, so the caller supplies the pull (a bounded fetch with manual commit) and\n * its `ack` commits the offset. Return the record's `key` so the redrive can carry it.\n */\n pop(queue: string): Promise<KafkaRedriveMessage | null>;\n}\n\n/**\n * A `@babelqueue/core` `RedriveIO` over Kafka that carries the DLQ record key byte-for-byte through the\n * redrive (contract §6.5): the redrive write back to the work topic and the restore write back to the\n * DLQ both go through {@link KafkaRedriveIO.publishFrom}, which copies the reserved record's key — a null\n * key stays null (no key on the record), an empty key stays empty — and never synthesises one. Partition\n * equality is not guaranteed (client partitioners differ). The §6.3 header projection is rebuilt from\n * the body being written (`bq-attempts` therefore reads 0 for a reset envelope); a body that is not a\n * canonical envelope (an undecodable record being restored) is written verbatim with no `bq-*` headers.\n *\n * Plain {@link KafkaRedriveIO.publish} writes a keyless record. `publishWithHeaders` is not offered, so\n * `bypass` is a no-op on this transport (as in the Go reference's keyed path).\n */\nexport class KafkaRedriveIO {\n constructor(private readonly options: KafkaRedriveIOOptions) {}\n\n pop(queue: string): Promise<KafkaRedriveMessage | null> {\n return this.options.pop(queue);\n }\n\n /** Append an already-encoded body to `queue` as a keyless record. */\n async publish(queue: string, body: string): Promise<void> {\n await this.send(queue, body, null);\n }\n\n /** Append `body` to `queue`, carrying the key of the reserved record `from` byte-for-byte. */\n async publishFrom(queue: string, body: string, from: KafkaRedriveMessage): Promise<void> {\n await this.send(queue, body, from.key ?? null);\n }\n\n private async send(queue: string, body: string, key: Buffer | null): Promise<void> {\n const record: KafkaProducerMessage = { value: body };\n const decoded = EnvelopeCodec.decode(body);\n if (EnvelopeCodec.accepts(decoded)) {\n record.headers = kafkaHeaders(decoded);\n record.timestamp = String(decoded.meta.created_at);\n }\n if (key !== null) record.key = Buffer.from(key);\n await this.options.producer.send({ topic: queue, messages: [record] });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band\n * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to\n * `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span\n * (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link KafkaConsumer}. */\nexport interface KafkaConsumerOptions {\n /** The producer used to republish retry/DLQ records (required for retry/DLQ). */\n producer?: KafkaProducerClient;\n /** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */\n retryTopics?: RetryTopics;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a poison record, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;\n}\n\n/**\n * Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,\n * routed to the handler for its URN (read from `bq-job`), and its offset committed only after\n * the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with\n * `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`\n * block. The `bq-attempts` header is the authoritative counter (the body is the fallback).\n */\nexport class KafkaConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly consumer: KafkaConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: KafkaConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */\n async run(): Promise<void> {\n await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });\n }\n\n /** Route + settle one record (process-then-commit). Exposed for testing. */\n async handle(payload: EachMessagePayload): Promise<void> {\n const { message } = payload;\n\n // Version gate (§6.7): decided on the header alone, before the value is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const rejected = new BabelQueueError(\n `Rejected a record from Kafka with unsupported bq-schema-version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(rejected, null, payload);\n }\n return;\n }\n\n const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(decodeError, null, payload);\n }\n return;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // A non-conformant / poison envelope may be missing the fields needed to annotate, so\n // forward the raw record to the DLQ rather than building a dead_letter block.\n const rejected = new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Kafka.\");\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(rejected, envelope, payload);\n }\n return;\n }\n\n const urn = headerString(message.headers, \"bq-job\") ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(payload, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n await this.commit(payload);\n } catch (error) {\n try {\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n } finally {\n this.report(error, envelope, payload);\n }\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError afterwards; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers, payload.message.key);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers, payload.message.key);\n }\n\n private async deadLetterRaw(payload: EachMessagePayload): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return;\n this.requireProducer();\n const { message } = payload;\n // Forward the original record verbatim (value, key, headers) so the DLQ copy keeps its provenance\n // (Java reference). Only the timestamp is re-stamped.\n const forwarded: KafkaProducerMessage = {\n value: message.value ?? Buffer.alloc(0),\n // KafkaJS accepts Buffer / array header values; the published structural type stays string-only.\n headers: copyHeaders(message.headers) as unknown as KafkaProducerMessage[\"headers\"],\n timestamp: String(Date.now()),\n };\n if (message.key !== undefined && message.key !== null) forwarded.key = Buffer.from(message.key);\n await this.options.producer!.send({ topic: dlq, messages: [forwarded] });\n }\n\n /**\n * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays\n * empty); a null/absent key is left off the record, never synthesised. Partition equality is not\n * guaranteed.\n */\n private async produce(\n topic: string,\n envelope: Envelope,\n headers: { [key: string]: string },\n key?: Buffer | null,\n ): Promise<void> {\n this.requireProducer();\n const record: KafkaProducerMessage = { value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) };\n if (key !== undefined && key !== null) record.key = Buffer.from(key);\n await this.options.producer!.send({ topic, messages: [record] });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n /**\n * Hand a failure to the user's `onError` callback. Callers invoke this only AFTER the record has been\n * settled (dead-lettered / retried and committed). A callback that throws synchronously, or returns a\n * promise that later rejects (an `async` function is assignable to the `void` callback type), is\n * swallowed here so user code can never bypass settlement or stop the consumer.\n */\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n try {\n const returned: unknown = this.options.onError?.(error, envelope, payload);\n if (isThenable(returned)) {\n // An async callback's rejection would otherwise surface as an unhandled rejection.\n returned.then(undefined, () => undefined);\n }\n } catch {\n // user callback failure must not escape the consume loop\n }\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` header map **without clobbering**\n * the contract headers in `base` (they win a key collision) and dropping blanks. The result is\n * `base`, 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelqueue/kafka",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Apache Kafka adapter for BabelQueue — canonical-envelope publisher and a URN-routed, process-then-commit consumer (SDK-owned retry topics + DLQ).",
5
5
  "keywords": [
6
6
  "babelqueue",
@@ -51,7 +51,7 @@
51
51
  "test": "node --import tsx --test test/*.test.ts"
52
52
  },
53
53
  "dependencies": {
54
- "@babelqueue/core": "^1.4.0"
54
+ "@babelqueue/core": "^1.9.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "kafkajs": "^2.0.0"