@babelqueue/kafka 1.1.2 → 1.1.3

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
@@ -285,7 +285,7 @@ var KafkaConsumer = class {
285
285
  const headers = kafkaHeaders(bumped);
286
286
  headers["bq-delay"] = String(tier.delayMs);
287
287
  headers["bq-original-topic"] = this.originalTopic(payload);
288
- await this.produce(tier.topic, bumped, headers);
288
+ await this.produce(tier.topic, bumped, headers, payload.message.key);
289
289
  }
290
290
  async deadLetter(envelope, payload, reason, error) {
291
291
  const dlq = this.options.retryTopics?.dlqTopic ?? null;
@@ -298,7 +298,7 @@ var KafkaConsumer = class {
298
298
  });
299
299
  const headers = kafkaHeaders(annotated);
300
300
  headers["bq-original-topic"] = original;
301
- await this.produce(dlq, annotated, headers);
301
+ await this.produce(dlq, annotated, headers, payload.message.key);
302
302
  }
303
303
  async deadLetterRaw(payload) {
304
304
  const dlq = this.options.retryTopics?.dlqTopic ?? null;
@@ -314,12 +314,16 @@ var KafkaConsumer = class {
314
314
  if (message.key !== void 0 && message.key !== null) forwarded.key = Buffer.from(message.key);
315
315
  await this.options.producer.send({ topic: dlq, messages: [forwarded] });
316
316
  }
317
- async produce(topic, envelope, headers) {
317
+ /**
318
+ * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
319
+ * empty); a null/absent key is left off the record, never synthesised. Partition equality is not
320
+ * guaranteed.
321
+ */
322
+ async produce(topic, envelope, headers, key) {
318
323
  this.requireProducer();
319
- await this.options.producer.send({
320
- topic,
321
- messages: [{ value: import_core.EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }]
322
- });
324
+ const record = { value: import_core.EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) };
325
+ if (key !== void 0 && key !== null) record.key = Buffer.from(key);
326
+ await this.options.producer.send({ topic, messages: [record] });
323
327
  }
324
328
  async commit(payload) {
325
329
  await this.consumer.commitOffsets([
@@ -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\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, 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 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":[]}
package/dist/index.d.cts CHANGED
@@ -189,6 +189,11 @@ declare class KafkaConsumer {
189
189
  private republishRetry;
190
190
  private deadLetter;
191
191
  private deadLetterRaw;
192
+ /**
193
+ * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
194
+ * empty); a null/absent key is left off the record, never synthesised. Partition equality is not
195
+ * guaranteed.
196
+ */
192
197
  private produce;
193
198
  private commit;
194
199
  private originalTopic;
package/dist/index.d.ts CHANGED
@@ -189,6 +189,11 @@ declare class KafkaConsumer {
189
189
  private republishRetry;
190
190
  private deadLetter;
191
191
  private deadLetterRaw;
192
+ /**
193
+ * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
194
+ * empty); a null/absent key is left off the record, never synthesised. Partition equality is not
195
+ * guaranteed.
196
+ */
192
197
  private produce;
193
198
  private commit;
194
199
  private originalTopic;
package/dist/index.js CHANGED
@@ -257,7 +257,7 @@ var KafkaConsumer = class {
257
257
  const headers = kafkaHeaders(bumped);
258
258
  headers["bq-delay"] = String(tier.delayMs);
259
259
  headers["bq-original-topic"] = this.originalTopic(payload);
260
- await this.produce(tier.topic, bumped, headers);
260
+ await this.produce(tier.topic, bumped, headers, payload.message.key);
261
261
  }
262
262
  async deadLetter(envelope, payload, reason, error) {
263
263
  const dlq = this.options.retryTopics?.dlqTopic ?? null;
@@ -270,7 +270,7 @@ var KafkaConsumer = class {
270
270
  });
271
271
  const headers = kafkaHeaders(annotated);
272
272
  headers["bq-original-topic"] = original;
273
- await this.produce(dlq, annotated, headers);
273
+ await this.produce(dlq, annotated, headers, payload.message.key);
274
274
  }
275
275
  async deadLetterRaw(payload) {
276
276
  const dlq = this.options.retryTopics?.dlqTopic ?? null;
@@ -286,12 +286,16 @@ var KafkaConsumer = class {
286
286
  if (message.key !== void 0 && message.key !== null) forwarded.key = Buffer.from(message.key);
287
287
  await this.options.producer.send({ topic: dlq, messages: [forwarded] });
288
288
  }
289
- async produce(topic, envelope, headers) {
289
+ /**
290
+ * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
291
+ * empty); a null/absent key is left off the record, never synthesised. Partition equality is not
292
+ * guaranteed.
293
+ */
294
+ async produce(topic, envelope, headers, key) {
290
295
  this.requireProducer();
291
- await this.options.producer.send({
292
- topic,
293
- messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }]
294
- });
296
+ const record = { value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) };
297
+ if (key !== void 0 && key !== null) record.key = Buffer.from(key);
298
+ await this.options.producer.send({ topic, messages: [record] });
295
299
  }
296
300
  async commit(payload) {
297
301
  await this.consumer.commitOffsets([
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":";AAiCA,SAAS,UAAU,iBAAiB,eAAe,gBAAgB,iBAAiB,0BAA0B;;;AClBvG,SAAS,gBAAgB,SAA0D;AACxF,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,QAAQ,MAAM,SAAS,QAAQ,UAAU,GAAI;AACjD,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;AAOO,SAAS,UAAU,MAAiC,OAAoE;AAC7H,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,gBAAgB,KAAK,CAAC,GAAG;AACjE,QAAI,OAAO,KAAM;AACjB,SAAK,GAAG,IAAI;AAAA,EACd;AACA,SAAO;AACT;;;ADiDO,SAAS,aAAa,UAA+C;AAC1E,QAAM,UAAqC,CAAC;AAC5C,MAAI,SAAS,IAAK,SAAQ,QAAQ,IAAI,SAAS;AAC/C,MAAI,SAAS,SAAU,SAAQ,aAAa,IAAI,SAAS;AACzD,MAAI,SAAS,KAAK,GAAI,SAAQ,eAAe,IAAI,SAAS,KAAK;AAC/D,MAAI,SAAS,KAAK,kBAAkB,KAAM,SAAQ,mBAAmB,IAAI,OAAO,SAAS,KAAK,cAAc;AAC5G,MAAI,SAAS,KAAK,KAAM,SAAQ,gBAAgB,IAAI,SAAS,KAAK;AAClE,UAAQ,aAAa,IAAI,OAAO,SAAS,YAAY,CAAC;AACtD,SAAO;AACT;AAMA,SAAS,YAAY,SAA4E;AAC/F,QAAM,MAA4C,CAAC;AACnD,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,OAAO,CAAC,MAAgC,OAAO,SAAS,CAAC,IAAI,OAAO,KAAK,CAAC,IAAI,OAAO,KAAK,GAAG,MAAM;AACzG,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,QAAI,UAAU,UAAa,UAAU,KAAM;AAC3C,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAI,IAAI,IAAI,MAAM,OAAO,CAAC,MAAM,MAAM,UAAa,MAAM,IAAI,EAAE,IAAI,IAAI;AAAA,IACzE,OAAO;AACL,UAAI,IAAI,IAAI,KAAK,KAAK;AAAA,IACxB;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,SAAsC,KAAiC;AAC3F,QAAM,QAAQ,UAAU,GAAG;AAE3B,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,QAAM,IAAI,MAAM,QAAQ,KAAK,IAAI,MAAM,CAAC,IAAI;AAC5C,MAAI,MAAM,UAAa,MAAM,KAAM,QAAO;AAC1C,SAAO,OAAO,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,IAAI,OAAO,CAAC;AAC3D;AAEA,SAAS,UAAU,SAAsC,KAAa,UAA0B;AAC9F,QAAM,IAAI,aAAa,SAAS,GAAG;AACnC,MAAI,MAAM,OAAW,QAAO;AAC5B,QAAM,IAAI,OAAO,SAAS,EAAE,KAAK,GAAG,EAAE;AACtC,SAAO,OAAO,MAAM,CAAC,IAAI,WAAW;AACtC;AAGA,IAAM,cAAc;AAMpB,SAAS,yBAAyB,SAAmD;AACnF,QAAM,QAAQ,aAAa,QAAQ,SAAS,mBAAmB;AAC/D,MAAI,UAAU,UAAa,YAAY,KAAK,KAAK,EAAG,QAAO;AAC3D,SAAO,UAAU,OAAO,cAAc,IAAI,SAAY;AACxD;AAQO,SAAS,UAAU,SAA8C;AACtE,QAAM,UAAU,QAAQ;AACxB,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,UAAM,QAAQ,aAAa,SAAS,GAAG;AACvC,QAAI,UAAU,UAAa,UAAU,GAAI,KAAI,GAAG,IAAI;AAAA,EACtD;AACA,SAAO;AACT;AAeO,IAAM,cAAN,MAAkB;AAAA,EAIvB,YACW,WACT,WAAqB,CAAC,GACtB,WAAsC,QACtC;AAHS;AAIT,SAAK,QAAQ,CAAC,GAAG,QAAQ,EACtB,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,EACpB,IAAI,CAAC,SAAS,OAAO,EAAE,OAAO,GAAG,SAAS,UAAU,IAAI,CAAC,IAAI,QAAQ,EAAE;AAC1E,SAAK,WAAW,aAAa,SAAY,GAAG,SAAS,SAAS;AAAA,EAChE;AAAA,EARW;AAAA,EAJF;AAAA,EACA;AAAA,EAaT,WAAoB;AAClB,WAAO,KAAK,MAAM,SAAS;AAAA,EAC7B;AAAA;AAAA,EAGA,aAAa,SAAuB;AAClC,SAAK,aAAa;AAClB,eAAW,QAAQ,KAAK,OAAO;AAC7B,UAAI,KAAK,WAAW,QAAS,QAAO;AAAA,IACtC;AACA,UAAM,IAAI;AAAA,MACR,yBAAyB,OAAO,sCAAsC,KAAK,MAAM,KAAK,MAAM,SAAS,CAAC,EAAG,OAAO;AAAA,IAClH;AAAA,EACF;AAAA;AAAA,EAGA,eAAe,SAAuB;AACpC,SAAK,aAAa;AAClB,WAAO,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,SAAS,CAAC,GAAG,KAAK,MAAM,SAAS,CAAC,CAAC;AAAA,EACzE;AAAA,EAEQ,eAAqB;AAC3B,QAAI,CAAC,KAAK,MAAM,QAAQ;AACtB,YAAM,IAAI,gBAAgB,qEAAqE,KAAK,SAAS,IAAI;AAAA,IACnH;AAAA,EACF;AACF;AAkBO,IAAM,iBAAN,MAAM,gBAAe;AAAA,EAClB,YACW,UACA,WACA,aACjB;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EAHgB;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAInB,OAAO,OAAO,UAA+B,OAA+B;AAC1E,WAAO,IAAI,gBAAe,UAAU,KAAK;AAAA,EAC3C;AAAA;AAAA,EAGA,OAAO,gBAAgB,UAA+B,aAA0C;AAC9F,WAAO,IAAI,gBAAe,UAAU,YAAY,WAAW,WAAW;AAAA,EACxE;AAAA;AAAA,EAGA,MAAM,QAAQ,KAAa,MAA+B,UAA0B,CAAC,GAAoB;AACvG,UAAM,WAAW,cAAc,KAAK,KAAK,MAAM,EAAE,OAAO,KAAK,WAAW,SAAS,QAAQ,QAAQ,CAAC;AAClG,QAAI,QAAQ,WAAW,QAAQ,QAAQ,UAAU,GAAG;AAClD,UAAI,CAAC,KAAK,aAAa;AACrB,cAAM,IAAI,gBAAgB,wFAAwF;AAAA,MACpH;AACA,YAAM,OAAO,KAAK,YAAY,aAAa,QAAQ,OAAO;AAC1D,YAAM,KAAK,WAAW,KAAK,OAAO,UAAU,QAAQ,SAAS,QAAQ,SAAS,KAAK,SAAS;AAAA,IAC9F,OAAO;AACL,YAAM,KAAK,WAAW,KAAK,WAAW,UAAU,QAAQ,OAAO;AAAA,IACjE;AACA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA,EAEA,MAAc,WACZ,OACA,UACA,OACA,SACA,eACe;AAGf,UAAM,UAAU,UAAU,aAAa,QAAQ,GAAG,KAAK;AACvD,QAAI,WAAW,KAAM,SAAQ,UAAU,IAAI,OAAO,OAAO;AACzD,QAAI,iBAAiB,KAAM,SAAQ,mBAAmB,IAAI;AAC1D,UAAM,KAAK,SAAS,KAAK;AAAA,MACvB;AAAA,MACA,UAAU,CAAC,EAAE,OAAO,cAAc,OAAO,QAAQ,GAAG,SAAS,WAAW,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;AAAA,IAC5G,CAAC;AAAA,EACH;AACF;AAoCO,IAAM,gBAAN,MAAoB;AAAA,EAIzB,YACmB,UACA,UACA,UAAgC,CAAC,GAClD;AAHiB;AACA;AACA;AAEjB,SAAK,WAAW,QAAQ,YAAY;AACpC,SAAK,aAAa,QAAQ,cAAc,mBAAmB;AAAA,EAC7D;AAAA,EANmB;AAAA,EACA;AAAA,EACA;AAAA,EANF;AAAA,EACA;AAAA;AAAA,EAYjB,MAAM,MAAqB;AACzB,UAAM,KAAK,SAAS,IAAI,EAAE,YAAY,OAAO,aAAa,CAAC,YAAY,KAAK,OAAO,OAAO,EAAE,CAAC;AAAA,EAC/F;AAAA;AAAA,EAGA,MAAM,OAAO,SAA4C;AACvD,UAAM,EAAE,QAAQ,IAAI;AAGpB,UAAM,cAAc,yBAAyB,OAAO;AACpD,QAAI,gBAAgB,QAAW;AAC7B,WAAK;AAAA,QACH,IAAI,gBAAgB,oEAAoE,WAAW,iBAAiB,cAAc,IAAI;AAAA,QACtI;AAAA,QACA;AAAA,MACF;AACA,YAAM,KAAK,cAAc,OAAO;AAChC,YAAM,KAAK,OAAO,OAAO;AACzB;AAAA,IACF;AAEA,UAAM,MAAM,QAAQ,QAAQ,QAAQ,MAAM,SAAS,MAAM,IAAI;AAE7D,QAAI;AACJ,QAAI;AACF,iBAAW,KAAK,UAAU,cAAc,OAAO,GAAG,GAAG,OAAO;AAAA,IAC9D,SAAS,aAAa;AACpB,WAAK,OAAO,aAAa,MAAM,OAAO;AACtC,YAAM,KAAK,cAAc,OAAO;AAChC,YAAM,KAAK,OAAO,OAAO;AACzB;AAAA,IACF;AAEA,QAAI,CAAC,cAAc,QAAQ,QAAQ,GAAG;AAGpC,WAAK,OAAO,IAAI,gBAAgB,2DAA2D,GAAG,UAAU,OAAO;AAC/G,YAAM,KAAK,cAAc,OAAO;AAChC,YAAM,KAAK,OAAO,OAAO;AACzB;AAAA,IACF;AAEA,UAAM,MAAM,aAAa,QAAQ,SAAS,QAAQ,KAAK,cAAc,IAAI,QAAQ;AACjF,UAAM,UAAU,KAAK,SAAS,GAAG;AACjC,QAAI,CAAC,SAAS;AACZ,YAAM,KAAK,aAAa,SAAS,UAAU,GAAG;AAC9C;AAAA,IACF;AAEA,QAAI;AACF,YAAM,QAAQ,UAAU,SAAS,UAAU,OAAO,CAAC;AACnD,YAAM,KAAK,OAAO,OAAO;AAAA,IAC3B,SAAS,OAAO;AACd,WAAK,OAAO,OAAO,UAAU,OAAO;AACpC,YAAM,KAAK,kBAAkB,SAAS,UAAsB,KAAK;AACjE,YAAM,KAAK,OAAO,OAAO;AAAA,IAC3B;AAAA,EACF;AAAA,EAEQ,UAAU,UAA4B,SAAiD;AAC7F,UAAM,OAAO,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AACzE,WAAO,EAAE,GAAG,UAAU,UAAU,UAAU,QAAQ,SAAS,eAAe,IAAI,EAAE;AAAA,EAClF;AAAA,EAEA,MAAc,aAAa,SAA6B,UAA4B,KAA4B;AAC9G,YAAQ,KAAK,YAAY;AAAA,MACvB,KAAK,mBAAmB;AACtB,cAAM,KAAK,OAAO,OAAO;AACzB;AAAA,MACF,KAAK,mBAAmB;AACtB,cAAM,KAAK,WAAW,UAAsB,SAAS,aAAa;AAClE,cAAM,KAAK,OAAO,OAAO;AACzB;AAAA,MACF,KAAK,mBAAmB;AACtB,cAAM,KAAK,eAAe,SAAS,QAAoB;AACvD,cAAM,KAAK,OAAO,OAAO;AACzB;AAAA,MACF,SAAS;AAEP,cAAM,QAAQ,IAAI,gBAAgB,GAAG;AACrC,aAAK,OAAO,OAAO,UAAU,OAAO;AACpC,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,kBAAkB,SAA6B,UAAoB,OAA+B;AAC9G,UAAM,WAAW,KAAK,QAAQ,aAAa,SAAS,KAAK;AACzD,UAAM,UAAU,KAAK,QAAQ,aAAa,YAAY,UAAU;AAChE,QAAI,CAAC,YAAY,CAAC,QAAQ;AACxB,WAAK;AACL,YAAM,IAAI,gBAAgB,mFAAmF;AAAA,IAC/G;AACA,QAAI,aAAa,SAAS,YAAY,KAAK,IAAI,KAAK,UAAU;AAC5D,YAAM,KAAK,eAAe,SAAS,QAAQ;AAAA,IAC7C,OAAO;AACL,YAAM,KAAK,WAAW,UAAU,SAAS,UAAU,KAAK;AAAA,IAC1D;AAAA,EACF;AAAA,EAEA,MAAc,eAAe,SAA6B,UAAmC;AAC3F,UAAM,SAAS,KAAK,QAAQ;AAC5B,QAAI,CAAC,UAAU,CAAC,OAAO,SAAS,GAAG;AACjC,YAAM,IAAI,gBAAgB,iEAAiE;AAAA,IAC7F;AACA,UAAM,OAAO,OAAO,eAAe,SAAS,YAAY,CAAC;AACzD,UAAM,SAAmB,EAAE,GAAG,UAAU,WAAW,SAAS,YAAY,KAAK,EAAE;AAC/E,UAAM,UAAU,aAAa,MAAM;AACnC,YAAQ,UAAU,IAAI,OAAO,KAAK,OAAO;AACzC,YAAQ,mBAAmB,IAAI,KAAK,cAAc,OAAO;AACzD,UAAM,KAAK,QAAQ,KAAK,OAAO,QAAQ,OAAO;AAAA,EAChD;AAAA,EAEA,MAAc,WAAW,UAAoB,SAA6B,QAAgB,OAAgC;AACxH,UAAM,MAAM,KAAK,QAAQ,aAAa,YAAY;AAClD,QAAI,CAAC,IAAK;AACV,UAAM,WAAW,KAAK,cAAc,OAAO;AAC3C,UAAM,YAAY,SAAS,UAAU,QAAQ,UAAU;AAAA,MACrD,UAAU,SAAS,YAAY;AAAA,MAC/B,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAChD,WAAW,iBAAiB,QAAQ,MAAM,OAAO;AAAA,IACnD,CAAC;AACD,UAAM,UAAU,aAAa,SAAS;AACtC,YAAQ,mBAAmB,IAAI;AAC/B,UAAM,KAAK,QAAQ,KAAK,WAAW,OAAO;AAAA,EAC5C;AAAA,EAEA,MAAc,cAAc,SAA4C;AACtE,UAAM,MAAM,KAAK,QAAQ,aAAa,YAAY;AAClD,QAAI,CAAC,IAAK;AACV,SAAK,gBAAgB;AACrB,UAAM,EAAE,QAAQ,IAAI;AAGpB,UAAM,YAAkC;AAAA,MACtC,OAAO,QAAQ,SAAS,OAAO,MAAM,CAAC;AAAA;AAAA,MAEtC,SAAS,YAAY,QAAQ,OAAO;AAAA,MACpC,WAAW,OAAO,KAAK,IAAI,CAAC;AAAA,IAC9B;AACA,QAAI,QAAQ,QAAQ,UAAa,QAAQ,QAAQ,KAAM,WAAU,MAAM,OAAO,KAAK,QAAQ,GAAG;AAC9F,UAAM,KAAK,QAAQ,SAAU,KAAK,EAAE,OAAO,KAAK,UAAU,CAAC,SAAS,EAAE,CAAC;AAAA,EACzE;AAAA,EAEA,MAAc,QAAQ,OAAe,UAAoB,SAAmD;AAC1G,SAAK,gBAAgB;AACrB,UAAM,KAAK,QAAQ,SAAU,KAAK;AAAA,MAChC;AAAA,MACA,UAAU,CAAC,EAAE,OAAO,cAAc,OAAO,QAAQ,GAAG,SAAS,WAAW,OAAO,KAAK,IAAI,CAAC,EAAE,CAAC;AAAA,IAC9F,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,OAAO,SAA4C;AAC/D,UAAM,KAAK,SAAS,cAAc;AAAA,MAChC,EAAE,OAAO,QAAQ,OAAO,WAAW,QAAQ,WAAW,SAAS,OAAO,QAAQ,QAAQ,MAAM,IAAI,GAAG,SAAS,EAAE;AAAA,IAChH,CAAC;AAAA,EACH;AAAA,EAEQ,cAAc,SAAqC;AACzD,WAAO,aAAa,QAAQ,QAAQ,SAAS,mBAAmB,KAAK,QAAQ;AAAA,EAC/E;AAAA,EAEQ,kBAAwB;AAC9B,QAAI,CAAC,KAAK,QAAQ,UAAU;AAC1B,YAAM,IAAI,gBAAgB,gEAAgE;AAAA,IAC5F;AAAA,EACF;AAAA,EAEQ,OAAO,OAAgB,UAAmC,SAAmC;AACnG,SAAK,QAAQ,UAAU,OAAO,UAAU,OAAO;AAAA,EACjD;AACF;","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\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, 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 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelqueue/kafka",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
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",