@babelqueue/kafka 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/index.cjs +446 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -4
- package/dist/index.d.ts +83 -4
- package/dist/index.js +446 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs.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\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq-schema-version` header when it is present, non-blank and not exactly the supported\n * version (compared untrimmed), else `undefined` (the record may be decoded).\n */\nfunction unsupportedSchemaVersion(message: KafkaIncomingMessage): string | undefined {\n const value = headerString(message.headers, \"bq-schema-version\");\n if (value === undefined || BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\n/**\n * Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer\n * values. Returns an empty object when there are none. Both the contract `bq-*` headers and any\n * out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it\n * knows.\n */\nexport function headersOf(message: KafkaIncomingMessage): HeaderCarrier {\n const headers = message.headers;\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headerString(headers, key);\n if (value !== undefined && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Retry / delay topology (contract §6.4–§6.5) -------------------------------\n\n/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */\nexport interface Tier {\n topic: string;\n delayMs: number;\n}\n\n/**\n * The SDK-owned retry/delay topology for one work topic: tiered delay topics\n * `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no\n * tiers configured raises rather than silently dropping.\n */\nexport class RetryTopics {\n readonly tiers: Tier[];\n readonly dlqTopic: string | null;\n\n constructor(\n readonly workTopic: string,\n delaysMs: number[] = [],\n dlqTopic: string | null | undefined = undefined,\n ) {\n this.tiers = [...delaysMs]\n .sort((a, b) => a - b)\n .map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));\n this.dlqTopic = dlqTopic === undefined ? `${workTopic}.dlq` : dlqTopic;\n }\n\n hasTiers(): boolean {\n return this.tiers.length > 0;\n }\n\n /** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */\n tierForDelay(delayMs: number): Tier {\n this.requireTiers();\n for (const tier of this.tiers) {\n if (tier.delayMs >= delayMs) return tier;\n }\n throw new BabelQueueError(\n `Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1]!.delayMs}ms).`,\n );\n }\n\n /** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */\n tierForAttempt(attempt: number): Tier {\n this.requireTiers();\n return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)]!;\n }\n\n private requireTiers(): void {\n if (!this.tiers.length) {\n throw new BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);\n }\n }\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link KafkaPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`\n * headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */\nexport class KafkaPublisher {\n private constructor(\n private readonly producer: KafkaProducerClient,\n private readonly workTopic: string,\n private readonly retryTopics?: RetryTopics,\n ) {}\n\n /** A publisher onto `topic` with no retry topics (a delay raises). */\n static create(producer: KafkaProducerClient, topic: string): KafkaPublisher {\n return new KafkaPublisher(producer, topic);\n }\n\n /** A publisher onto the topology's work topic, with delay routed via its retry tiers. */\n static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher {\n return new KafkaPublisher(producer, retryTopics.workTopic, retryTopics);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });\n if (options.delayMs != null && options.delayMs > 0) {\n if (!this.retryTopics) {\n throw new BabelQueueError(\"Kafka has no native delayed delivery; a delay requires retry topics (none configured).\");\n }\n const tier = this.retryTopics.tierForDelay(options.delayMs);\n await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope, options.headers);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(\n topic: string,\n envelope: Envelope,\n extra?: HeaderCarrier | null,\n delayMs?: number,\n originalTopic?: string,\n ): Promise<void> {\n // The contract bq-* headers are seeded first, then the out-of-band riders are merged beside\n // them without clobbering (the contract wins a collision).\n const headers = mergeInto(kafkaHeaders(envelope), extra);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- DLQ redrive IO (ADR-0026, §6.5 \"Record key on republish\") -------------------\n\n/**\n * A DLQ record reserved for {@link KafkaRedriveIO}: the structural shape `@babelqueue/core`'s\n * `RedriveMessage` expects (`body` + `ack`), plus the consumed record's `key` — `null`/absent for a\n * keyless record, a (possibly empty) `Buffer` otherwise.\n */\nexport interface KafkaRedriveMessage {\n body: string;\n key?: Buffer | null;\n ack(): Promise<void>;\n}\n\n/** Options for {@link KafkaRedriveIO}. */\nexport interface KafkaRedriveIOOptions {\n /** The producer used for every write the redrive makes (redrive write + restore to the DLQ). */\n producer: KafkaProducerClient;\n /**\n * Reserve the next record from `queue` (the DLQ topic), or `null` when it is empty. Kafka consumption\n * is push-based in KafkaJS, so the caller supplies the pull (a bounded fetch with manual commit) and\n * its `ack` commits the offset. Return the record's `key` so the redrive can carry it.\n */\n pop(queue: string): Promise<KafkaRedriveMessage | null>;\n}\n\n/**\n * A `@babelqueue/core` `RedriveIO` over Kafka that carries the DLQ record key byte-for-byte through the\n * redrive (contract §6.5): the redrive write back to the work topic and the restore write back to the\n * DLQ both go through {@link KafkaRedriveIO.publishFrom}, which copies the reserved record's key — a null\n * key stays null (no key on the record), an empty key stays empty — and never synthesises one. Partition\n * equality is not guaranteed (client partitioners differ). The §6.3 header projection is rebuilt from\n * the body being written (`bq-attempts` therefore reads 0 for a reset envelope); a body that is not a\n * canonical envelope (an undecodable record being restored) is written verbatim with no `bq-*` headers.\n *\n * Plain {@link KafkaRedriveIO.publish} writes a keyless record. `publishWithHeaders` is not offered, so\n * `bypass` is a no-op on this transport (as in the Go reference's keyed path).\n */\nexport class KafkaRedriveIO {\n constructor(private readonly options: KafkaRedriveIOOptions) {}\n\n pop(queue: string): Promise<KafkaRedriveMessage | null> {\n return this.options.pop(queue);\n }\n\n /** Append an already-encoded body to `queue` as a keyless record. */\n async publish(queue: string, body: string): Promise<void> {\n await this.send(queue, body, null);\n }\n\n /** Append `body` to `queue`, carrying the key of the reserved record `from` byte-for-byte. */\n async publishFrom(queue: string, body: string, from: KafkaRedriveMessage): Promise<void> {\n await this.send(queue, body, from.key ?? null);\n }\n\n private async send(queue: string, body: string, key: Buffer | null): Promise<void> {\n const record: KafkaProducerMessage = { value: body };\n const decoded = EnvelopeCodec.decode(body);\n if (EnvelopeCodec.accepts(decoded)) {\n record.headers = kafkaHeaders(decoded);\n record.timestamp = String(decoded.meta.created_at);\n }\n if (key !== null) record.key = Buffer.from(key);\n await this.options.producer.send({ topic: queue, messages: [record] });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band\n * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to\n * `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span\n * (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link KafkaConsumer}. */\nexport interface KafkaConsumerOptions {\n /** The producer used to republish retry/DLQ records (required for retry/DLQ). */\n producer?: KafkaProducerClient;\n /** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */\n retryTopics?: RetryTopics;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a poison record, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;\n}\n\n/**\n * Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,\n * routed to the handler for its URN (read from `bq-job`), and its offset committed only after\n * the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with\n * `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`\n * block. The `bq-attempts` header is the authoritative counter (the body is the fallback).\n */\nexport class KafkaConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly consumer: KafkaConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: KafkaConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */\n async run(): Promise<void> {\n await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });\n }\n\n /** Route + settle one record (process-then-commit). Exposed for testing. */\n async handle(payload: EachMessagePayload): Promise<void> {\n const { message } = payload;\n\n // Version gate (§6.7): decided on the header alone, before the value is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const rejected = new BabelQueueError(\n `Rejected a record from Kafka with unsupported bq-schema-version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(rejected, null, payload);\n }\n return;\n }\n\n const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(decodeError, null, payload);\n }\n return;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // A non-conformant / poison envelope may be missing the fields needed to annotate, so\n // forward the raw record to the DLQ rather than building a dead_letter block.\n const rejected = new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Kafka.\");\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(rejected, envelope, payload);\n }\n return;\n }\n\n const urn = headerString(message.headers, \"bq-job\") ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(payload, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n await this.commit(payload);\n } catch (error) {\n try {\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n } finally {\n this.report(error, envelope, payload);\n }\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError afterwards; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers, payload.message.key);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers, payload.message.key);\n }\n\n private async deadLetterRaw(payload: EachMessagePayload): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return;\n this.requireProducer();\n const { message } = payload;\n // Forward the original record verbatim (value, key, headers) so the DLQ copy keeps its provenance\n // (Java reference). Only the timestamp is re-stamped.\n const forwarded: KafkaProducerMessage = {\n value: message.value ?? Buffer.alloc(0),\n // KafkaJS accepts Buffer / array header values; the published structural type stays string-only.\n headers: copyHeaders(message.headers) as unknown as KafkaProducerMessage[\"headers\"],\n timestamp: String(Date.now()),\n };\n if (message.key !== undefined && message.key !== null) forwarded.key = Buffer.from(message.key);\n await this.options.producer!.send({ topic: dlq, messages: [forwarded] });\n }\n\n /**\n * Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays\n * empty); a null/absent key is left off the record, never synthesised. Partition equality is not\n * guaranteed.\n */\n private async produce(\n topic: string,\n envelope: Envelope,\n headers: { [key: string]: string },\n key?: Buffer | null,\n ): Promise<void> {\n this.requireProducer();\n const record: KafkaProducerMessage = { value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) };\n if (key !== undefined && key !== null) record.key = Buffer.from(key);\n await this.options.producer!.send({ topic, messages: [record] });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n /**\n * Hand a failure to the user's `onError` callback. Callers invoke this only AFTER the record has been\n * settled (dead-lettered / retried and committed). A callback that throws synchronously, or returns a\n * promise that later rejects (an `async` function is assignable to the `void` callback type), is\n * swallowed here so user code can never bypass settlement or stop the consumer.\n */\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n try {\n const returned: unknown = this.options.onError?.(error, envelope, payload);\n if (isThenable(returned)) {\n // An async callback's rejection would otherwise surface as an unhandled rejection.\n returned.then(undefined, () => undefined);\n }\n } catch {\n // user callback failure must not escape the consume loop\n }\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Merge out-of-band `extra` headers onto an already-built `base` header map **without clobbering**\n * the contract headers in `base` (they win a key collision) and dropping blanks. The result is\n * `base`, mutated in place.\n */\nexport function mergeInto(base: { [key: string]: string }, extra: HeaderCarrier | null | undefined): { [key: string]: string } {\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {\n if (key in base) continue; // the contract header already there wins\n base[key] = value;\n }\n return base;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/envelope-shape.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 * **Poison bodies (§6.5).** A body that is not exactly one JSON object, whose typed members have the wrong\n * JSON type, or that fails the conformance check is dead-lettered **wrapped**, as the Go reference does: a\n * body that is one JSON object keeps its keys (anything else becomes `{\"raw\": \"<body>\"}`) and gains a\n * `dead_letter` block with `reason: \"poison\"`; the record key is carried over unchanged. Only the\n * version-gate rejection above still forwards the record raw.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { envelopeViolation, isLossyNumberToken } from \"./envelope-shape.js\";\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 (the version-gate\n * rejection only; poison records are wrapped and carry string headers) 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/** The subset of the KafkaJS consumer {@link KafkaRedriveIO} calls to rewind a record (`consumer.seek`). */\nexport interface KafkaSeekClient {\n seek(position: { topic: string; partition: number; offset: string }): void | Promise<void>;\n}\n\n// --- Header projection (contract §6.3) -----------------------------------------\n\n/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */\nexport function kafkaHeaders(envelope: Envelope): { [key: string]: string } {\n const headers: { [key: string]: string } = {};\n if (envelope.job) headers[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) headers[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) headers[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) headers[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n if (envelope.meta.lang) headers[\"bq-source-lang\"] = envelope.meta.lang;\n headers[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return headers;\n}\n\n/**\n * Copy inbound record headers for a raw DLQ forward: every value becomes a `Buffer` copy (a string is\n * UTF-8 encoded), array-valued headers keep all their entries, and `undefined` / `null` values are dropped.\n */\nfunction copyHeaders(headers: IncomingHeaders | undefined): { [key: string]: Buffer | Buffer[] } {\n const out: { [key: string]: Buffer | Buffer[] } = {};\n if (!headers) return out;\n const copy = (v: Buffer | string): Buffer => (Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from(v, \"utf8\"));\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n out[name] = value.filter((v) => v !== undefined && v !== null).map(copy);\n } else {\n out[name] = copy(value);\n }\n }\n return out;\n}\n\nfunction headerString(headers: IncomingHeaders | undefined, key: string): string | undefined {\n const value = headers?.[key];\n // A null header value is \"no header\" (broker-bindings.md §6.7): never stringified to \"null\".\n if (value === undefined || value === null) return undefined;\n const v = Array.isArray(value) ? value[0] : value;\n if (v === undefined || v === null) return undefined;\n return Buffer.isBuffer(v) ? v.toString(\"utf8\") : String(v);\n}\n\nfunction headerInt(headers: IncomingHeaders | undefined, key: string, fallback: number): number {\n const s = headerString(headers, key);\n if (s === undefined) return fallback;\n const n = Number.parseInt(s.trim(), 10);\n return Number.isNaN(n) ? fallback : n;\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq-schema-version` header when it is present, non-blank and not exactly the supported\n * version (compared untrimmed), else `undefined` (the record may be decoded).\n */\nfunction unsupportedSchemaVersion(message: KafkaIncomingMessage): string | undefined {\n const value = headerString(message.headers, \"bq-schema-version\");\n if (value === undefined || BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\n/**\n * Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer\n * values. Returns an empty object when there are none. Both the contract `bq-*` headers and any\n * out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it\n * knows.\n */\nexport function headersOf(message: KafkaIncomingMessage): HeaderCarrier {\n const headers = message.headers;\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headerString(headers, key);\n if (value !== undefined && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Retry / delay topology (contract §6.4–§6.5) -------------------------------\n\n/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */\nexport interface Tier {\n topic: string;\n delayMs: number;\n}\n\n/**\n * The SDK-owned retry/delay topology for one work topic: tiered delay topics\n * `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no\n * tiers configured raises rather than silently dropping.\n */\nexport class RetryTopics {\n readonly tiers: Tier[];\n readonly dlqTopic: string | null;\n\n constructor(\n readonly workTopic: string,\n delaysMs: number[] = [],\n dlqTopic: string | null | undefined = undefined,\n ) {\n this.tiers = [...delaysMs]\n .sort((a, b) => a - b)\n .map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));\n this.dlqTopic = dlqTopic === undefined ? `${workTopic}.dlq` : dlqTopic;\n }\n\n hasTiers(): boolean {\n return this.tiers.length > 0;\n }\n\n /** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */\n tierForDelay(delayMs: number): Tier {\n this.requireTiers();\n for (const tier of this.tiers) {\n if (tier.delayMs >= delayMs) return tier;\n }\n throw new BabelQueueError(\n `Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1]!.delayMs}ms).`,\n );\n }\n\n /** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */\n tierForAttempt(attempt: number): Tier {\n this.requireTiers();\n return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)]!;\n }\n\n private requireTiers(): void {\n if (!this.tiers.length) {\n throw new BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);\n }\n }\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link KafkaPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`\n * headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */\nexport class KafkaPublisher {\n private constructor(\n private readonly producer: KafkaProducerClient,\n private readonly workTopic: string,\n private readonly retryTopics?: RetryTopics,\n ) {}\n\n /** A publisher onto `topic` with no retry topics (a delay raises). */\n static create(producer: KafkaProducerClient, topic: string): KafkaPublisher {\n return new KafkaPublisher(producer, topic);\n }\n\n /** A publisher onto the topology's work topic, with delay routed via its retry tiers. */\n static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher {\n return new KafkaPublisher(producer, retryTopics.workTopic, retryTopics);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });\n if (options.delayMs != null && options.delayMs > 0) {\n if (!this.retryTopics) {\n throw new BabelQueueError(\"Kafka has no native delayed delivery; a delay requires retry topics (none configured).\");\n }\n const tier = this.retryTopics.tierForDelay(options.delayMs);\n await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope, options.headers);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(\n topic: string,\n envelope: Envelope,\n extra?: HeaderCarrier | null,\n delayMs?: number,\n originalTopic?: string,\n ): Promise<void> {\n // The contract bq-* headers are seeded first, then the out-of-band riders are merged beside\n // them without clobbering (the contract wins a collision).\n const headers = mergeInto(kafkaHeaders(envelope), extra);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- DLQ redrive IO (ADR-0026, §6.5 \"Record key on republish\") -------------------\n\n/**\n * A DLQ record reserved for {@link KafkaRedriveIO}: the structural shape `@babelqueue/core`'s\n * `RedriveMessage` expects (`body` + `ack`), plus the consumed record's `key` — `null`/absent for a\n * keyless record, a (possibly empty) `Buffer` otherwise.\n */\nexport interface KafkaRedriveMessage {\n body: string;\n key?: Buffer | null;\n /**\n * The consumed record's value bytes (`null` for a tombstone) and headers. When supplied, a restore write\n * (see {@link KafkaRedriveIO.publishFrom}) re-writes the original record verbatim — value bytes and\n * headers as they were consumed — instead of re-deriving them from `body` (a lossy UTF-8 round trip\n * for a non-UTF-8 value, and a re-projection of the `bq-*` headers).\n */\n value?: Buffer | null;\n headers?: IncomingHeaders;\n /** The record's position, needed by {@link KafkaRedriveIO.rewind}; the offset is a KafkaJS string. */\n topic?: string;\n partition?: number;\n offset?: string;\n ack(): Promise<void>;\n}\n\n/** Options for {@link KafkaRedriveIO}. */\nexport interface KafkaRedriveIOOptions {\n /** The producer used for every write the redrive makes (redrive write + restore to the DLQ). */\n producer: KafkaProducerClient;\n /**\n * Reserve the next record from `queue` (the DLQ topic), or `null` when it is empty. Kafka consumption\n * is push-based in KafkaJS, so the caller supplies the pull (a bounded fetch with manual commit) and\n * its `ack` commits the offset. Return the record's `key` so the redrive can carry it.\n *\n * For a **verbatim restore** (a record written back to the DLQ unchanged: dry run, unselected, undecodable,\n * failed redrive write) also return the consumed record's `topic`, `value` and `headers`: the IO treats a\n * write as a restore only when the reserved message carries `topic` (the queue written to is that topic and\n * the body is unchanged), and then re-writes `value` / `headers` / `key` as they were consumed with a fresh\n * timestamp. A message without `topic` is restored through the republish path (`bq-*` headers re-projected\n * from the body, `meta.created_at` as timestamp); without `value` / `headers` the body / projected headers\n * stand in for them. `topic`, `partition` and `offset` are also what {@link KafkaRedriveIO.rewind} needs.\n */\n pop(queue: string): Promise<KafkaRedriveMessage | null>;\n /**\n * The consumer the DLQ records are fetched with. When given, {@link KafkaRedriveIO.rewind} is offered:\n * when the run stops (a failed restore write, a failed redrive write, a throwing `select` or a failed\n * `ack`) it seeks this consumer back to the first unacknowledged record of every affected partition, so\n * a later (cumulative) offset commit cannot skip a record that was not safely handled. Without it the IO\n * has no `rewind` and a failed restore only leaves the record unacknowledged.\n *\n * The IO only knows the records it handed out. If {@link KafkaRedriveIOOptions.pop} reads ahead (buffers\n * records it has not returned yet), the caller must discard that buffer after a rewind and must not\n * reuse the IO: e.g. `pop` buffered p0@5..7, the run stopped at p0@6 and the seek went back to p0@5; a\n * buffered p0@7 returned and acknowledged afterwards would commit offset 8 and skip 5 and 6.\n */\n consumer?: KafkaSeekClient;\n}\n\n/**\n * A `@babelqueue/core` `RedriveIO` over Kafka that carries the DLQ record key byte-for-byte through the\n * redrive (contract §6.5): the redrive write back to the work topic and the restore write back to the\n * DLQ both go through {@link KafkaRedriveIO.publishFrom}, which copies the reserved record's key — a null\n * key stays null (no key on the record), an empty key stays empty — and never synthesises one. Partition\n * equality is not guaranteed (client partitioners differ). The §6.3 header projection is rebuilt from\n * the body being written (`bq-attempts` therefore reads 0 for a reset envelope); a body that is not a\n * canonical envelope (an undecodable record being restored) is written verbatim with no `bq-*` headers.\n *\n * Plain {@link KafkaRedriveIO.publish} writes a keyless record. `publishWithHeaders` is not offered, so\n * `bypass` is a no-op on this transport (as in the Go reference's keyed path).\n *\n * When `options.consumer` is supplied the IO also offers {@link KafkaRedriveIO.rewind} (core's optional\n * `RedriveIO.rewind`): if the restore write of a record back to the DLQ fails, the consumer is sought\n * back, because a Kafka offset commit is cumulative and a later commit would otherwise skip a record\n * for good. Core's `redrive()` pops the whole batch before processing it, so the consumer position is\n * already past every record handed out and not yet acknowledged; the IO tracks those and seeks every\n * affected partition back to its first unacknowledged record, not only the failed one.\n */\nexport class KafkaRedriveIO {\n /** Seek the consumer back to every unacknowledged record (`from` included unless it was acknowledged already); defined only when a `consumer` was supplied. */\n readonly rewind?: (from: KafkaRedriveMessage) => Promise<void>;\n\n /** Records handed out by {@link KafkaRedriveIO.pop} that carry a position and are not acknowledged yet. */\n private readonly inFlight = new Set<KafkaRedriveMessage>();\n\n /** Records handed out by {@link KafkaRedriveIO.pop} that were acknowledged since; a rewind never re-reads them. */\n private readonly acknowledged = new WeakSet<KafkaRedriveMessage>();\n\n constructor(private readonly options: KafkaRedriveIOOptions) {\n const consumer = options.consumer;\n if (consumer) {\n this.rewind = async (from: KafkaRedriveMessage): Promise<void> => {\n if (from.topic === undefined || from.partition === undefined || from.offset === undefined) {\n throw new Error(\"kafka redrive: cannot rewind a record without topic, partition and offset\");\n }\n // First (lowest) unacknowledged offset per partition: everything still in flight, plus `from` unless it\n // was acknowledged already (a record restored and acked after a failed redrive write is committed past,\n // and seeking back to it would redeliver a copy).\n const pending = new Set<KafkaRedriveMessage>(this.inFlight);\n if (!this.acknowledged.has(from)) pending.add(from);\n const first = new Map<string, { topic: string; partition: number; offset: bigint }>();\n for (const m of pending) {\n if (m.topic === undefined || m.partition === undefined || m.offset === undefined) continue;\n const key = `${m.topic}\\u0000${m.partition}`;\n const offset = BigInt(m.offset);\n const seen = first.get(key);\n if (!seen || offset < seen.offset) first.set(key, { topic: m.topic, partition: m.partition, offset });\n }\n this.inFlight.clear();\n const failures: unknown[] = [];\n for (const { topic, partition, offset } of first.values()) {\n try {\n await consumer.seek({ topic, partition, offset: offset.toString() });\n } catch (err) {\n failures.push(err); // keep rewinding the other partitions\n }\n }\n if (failures.length === 1) throw failures[0];\n if (failures.length > 1) throw new AggregateError(failures, \"kafka redrive: seeking several partitions back failed\");\n };\n }\n }\n\n async pop(queue: string): Promise<KafkaRedriveMessage | null> {\n const message = await this.options.pop(queue);\n if (message && this.rewind && message.topic !== undefined && message.partition !== undefined && message.offset !== undefined) {\n // Hand out a copy and leave the caller's message untouched (it may be frozen or expose accessors). The\n // fields are read one by one: a spread copies own enumerable properties only, so a class instance with\n // prototype getters (`get body()`) would lose them and its DLQ body would be restored as an empty record.\n // The copy forgets the record once it is acknowledged: a rewind must never re-read a committed record.\n const tracked: KafkaRedriveMessage = {\n body: message.body,\n key: message.key,\n value: message.value,\n headers: message.headers,\n topic: message.topic,\n partition: message.partition,\n offset: message.offset,\n ack: async (): Promise<void> => {\n await message.ack();\n this.inFlight.delete(tracked);\n this.acknowledged.add(tracked);\n },\n };\n this.inFlight.add(tracked);\n return tracked;\n }\n return message;\n }\n\n /** Append an already-encoded body to `queue` as a keyless record. */\n async publish(queue: string, body: string): Promise<void> {\n await this.send(queue, body, null);\n }\n\n /**\n * Append `body` to `queue` on behalf of the reserved record `from`. Two modes (Go/Java/.NET parity):\n *\n * - **Restore** — `queue` is the topic `from` was consumed from and `body` is the body `pop` returned for\n * it (redrive writing an unchanged DLQ record back). The original record is re-written verbatim: its\n * value bytes (`from.value`), headers (`from.headers`) and key, with a fresh timestamp so the restored\n * copy does not age out of the DLQ early (never `meta.created_at`). A message that carries no `value`\n * re-writes `body`; one that carries no `headers` falls back to the `bq-*` projection of a decodable body.\n * Needs `from.topic` to be set; without it a restore is indistinguishable from a republish.\n * Header values the consumer decoded as `null` (a header with a -1 length) are not re-written: the\n * restore copy goes through `copyHeaders`, which drops them (a deliberate bound; the Go reference\n * writes them as they are).\n * - **Republish** — any other queue/body (a redrive to the work topic): the `bq-*` header projection and\n * the `meta.created_at` timestamp, plus the key of `from` byte-for-byte (§6.5).\n */\n async publishFrom(queue: string, body: string, from: KafkaRedriveMessage): Promise<void> {\n if (from.topic !== undefined && queue === from.topic && body === from.body) {\n await this.restoreVerbatim(queue, body, from);\n return;\n }\n await this.send(queue, body, from.key ?? null);\n }\n\n private async restoreVerbatim(queue: string, body: string, from: KafkaRedriveMessage): Promise<void> {\n const record: KafkaProducerMessage = {\n // A tombstone stays a tombstone; KafkaJS accepts a null value although the structural type does not.\n value: from.value === undefined ? body : (from.value as unknown as Buffer),\n timestamp: String(Date.now()),\n };\n if (from.headers !== undefined) {\n // KafkaJS accepts Buffer / array header values; the published structural type stays string-only.\n record.headers = copyHeaders(from.headers) as unknown as KafkaProducerMessage[\"headers\"];\n } else {\n const decoded = EnvelopeCodec.decode(body);\n if (EnvelopeCodec.accepts(decoded)) record.headers = kafkaHeaders(decoded);\n }\n if (from.key !== undefined && from.key !== null) record.key = Buffer.from(from.key);\n await this.options.producer.send({ topic: queue, messages: [record] });\n }\n\n private async send(queue: string, body: string, key: Buffer | null): Promise<void> {\n const record: KafkaProducerMessage = { value: body };\n const decoded = EnvelopeCodec.decode(body);\n if (EnvelopeCodec.accepts(decoded)) {\n record.headers = kafkaHeaders(decoded);\n record.timestamp = String(decoded.meta.created_at);\n }\n if (key !== null) record.key = Buffer.from(key);\n await this.options.producer.send({ topic: queue, messages: [record] });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band\n * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to\n * `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span\n * (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link KafkaConsumer}. */\nexport interface KafkaConsumerOptions {\n /** The producer used to republish retry/DLQ records (required for retry/DLQ). */\n producer?: KafkaProducerClient;\n /** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */\n retryTopics?: RetryTopics;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a poison record, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;\n}\n\n// --- Poison handling (contract §6.5) --------------------------------------------\n\n/**\n * Why `raw` is not a decodable BabelQueue body, or `null` when it is: exactly the bodies the Go reference's typed\n * `Decode` rejects (see `envelope-shape.ts`). The core decoder is lenient (it returns `{}` for anything it cannot\n * parse), so the poison condition is explicit here. A well-typed object that merely fails the conformance check\n * is not \"undecodable\".\n */\nfunction decodeFailure(raw: string): BabelQueueError | null {\n const reason = envelopeViolation(raw);\n return reason === null ? null : new BabelQueueError(`Undecodable BabelQueue body: ${reason}`);\n}\n\n/**\n * True when `raw` (already known to be valid JSON) holds a number token a JS double cannot carry without changing\n * its value (see `isLossyNumberToken`). String contents are skipped, so digits inside strings never count.\n */\nfunction hasLossyNumber(raw: string): boolean {\n let i = 0;\n while (i < raw.length) {\n const c = raw[i]!;\n if (c === '\"') {\n i++;\n while (i < raw.length && raw[i] !== '\"') i += raw[i] === \"\\\\\" ? 2 : 1;\n i++;\n } else if (c === \"-\" || (c >= \"0\" && c <= \"9\")) {\n let j = i + 1;\n while (j < raw.length && /[0-9.eE+-]/.test(raw[j]!)) j++;\n if (isLossyNumberToken(raw.slice(i, j))) return true;\n i = j;\n } else {\n i++;\n }\n }\n return false;\n}\n\n/**\n * The poison DLQ record (Go parity): a body that is exactly one JSON object keeps its keys and values, anything\n * else is carried as the string value of `raw`; a `dead_letter` block with reason `poison` replaces any existing\n * one. JSON numbers go through a JS double, so a body holding a number that would not survive the round trip (an\n * integer beyond 2^53, or a literal such as `1e400` that overflows) is carried verbatim as `raw` instead; a `1.0`\n * or `1e2` literal is still re-formatted (`1`, `100`) — the value is preserved, the spelling is not.\n */\nfunction poisonRecord(\n raw: string,\n cause: unknown,\n originalQueue: string,\n attempts: number,\n keepMembers = true,\n): Record<string, unknown> {\n let record: Record<string, unknown> = { raw };\n try {\n if (!keepMembers) throw new Error(\"carry the body verbatim\");\n const parsed: unknown = JSON.parse(raw);\n if (parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed) && !hasLossyNumber(raw)) {\n record = parsed as Record<string, unknown>;\n }\n } catch {\n // not JSON: wrapped as {\"raw\": body}\n }\n delete record.dead_letter;\n record.dead_letter = annotate({ attempts } as unknown as Envelope, \"poison\", originalQueue, {\n attempts,\n error: cause instanceof Error ? cause.message : null,\n exception: cause instanceof Error ? cause.name : null,\n }).dead_letter;\n return record;\n}\n\n/** Contract `bq-*` headers for a poison DLQ record, projected from whatever the body carries, plus the origin. */\nfunction poisonHeaders(record: Record<string, unknown>, originalQueue: string, attempts: number): { [key: string]: string } {\n const headers: { [key: string]: string } = {};\n const str = (v: unknown): string | undefined => (typeof v === \"string\" && v !== \"\" ? v : undefined);\n const meta = record.meta !== null && typeof record.meta === \"object\" ? (record.meta as Record<string, unknown>) : {};\n const job = str(record.job) ?? str(record.urn);\n if (job) headers[\"bq-job\"] = job;\n const trace = str(record.trace_id);\n if (trace) headers[\"bq-trace-id\"] = trace;\n const id = str(meta.id);\n if (id) headers[\"bq-message-id\"] = id;\n if (meta.schema_version != null && typeof meta.schema_version !== \"object\") {\n headers[\"bq-schema-version\"] = String(meta.schema_version);\n }\n const lang = str(meta.lang);\n if (lang) headers[\"bq-source-lang\"] = lang;\n headers[\"bq-attempts\"] = String(attempts);\n headers[\"bq-original-topic\"] = originalQueue;\n return headers;\n}\n\n/**\n * Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,\n * routed to the handler for its URN (read from `bq-job`), and its offset committed only after\n * the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with\n * `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`\n * block. The `bq-attempts` header is the authoritative counter (the body is the fallback).\n */\nexport class KafkaConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly consumer: KafkaConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: KafkaConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */\n async run(): Promise<void> {\n await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });\n }\n\n /** Route + settle one record (process-then-commit). Exposed for testing. */\n async handle(payload: EachMessagePayload): Promise<void> {\n const { message } = payload;\n\n // Version gate (§6.7): decided on the header alone, before the value is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const rejected = new BabelQueueError(\n `Rejected a record from Kafka with unsupported bq-schema-version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n try {\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n } finally {\n this.report(rejected, null, payload);\n }\n return;\n }\n\n const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n // Undecodable body (§6.5): dead-lettered wrapped (reason \"poison\"), key preserved.\n const undecodable = decodeFailure(raw);\n if (undecodable) {\n await this.settlePoison(payload, raw, undecodable, null);\n return;\n }\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n await this.settlePoison(payload, raw, decodeError, null);\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 as Envelope, message, headersOf(message));\n await this.commit(payload);\n } catch (error) {\n try {\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n } finally {\n this.report(error, envelope, payload);\n }\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError afterwards; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers, payload.message.key);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers, payload.message.key);\n }\n\n /** Dead-letter a poison record (wrapped), commit it, then report: settle first so `onError` cannot bypass it. */\n private async settlePoison(\n payload: EachMessagePayload,\n raw: string,\n cause: unknown,\n envelope: IncomingEnvelope | null,\n ): Promise<void> {\n try {\n await this.deadLetterPoison(payload, raw, cause);\n await this.commit(payload);\n } finally {\n this.report(cause, envelope, payload);\n }\n }\n\n private async deadLetterPoison(payload: EachMessagePayload, raw: string, cause: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n this.requireProducer();\n const original = this.originalTopic(payload);\n const attempts = Math.max(headerInt(payload.message.headers, \"bq-attempts\", 0), 0);\n let record = poisonRecord(raw, cause, original, attempts);\n let json: string;\n try {\n json = JSON.stringify(record);\n } catch {\n // A body nested deeper than JSON.stringify's recursion can carry (RangeError): wrap it verbatim under `raw`\n // instead, so the poison record is still parked and never left to wedge the partition.\n record = poisonRecord(raw, cause, original, attempts, false);\n json = JSON.stringify(record);\n }\n const headers = poisonHeaders(record, original, attempts);\n await this.produceRaw(dlq, json, 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 await this.produceRaw(topic, EnvelopeCodec.encode(envelope), headers, key);\n }\n\n private async produceRaw(\n topic: string,\n value: string,\n headers: { [key: string]: string },\n key?: Buffer | null,\n ): Promise<void> {\n this.requireProducer();\n const record: KafkaProducerMessage = { value, headers, timestamp: String(Date.now()) };\n if (key !== undefined && key !== null) record.key = Buffer.from(key);\n await this.options.producer!.send({ topic, messages: [record] });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n /**\n * Hand a failure to the user's `onError` callback. Callers invoke this only AFTER the record has been\n * settled (dead-lettered / retried and committed). A callback that throws synchronously, or returns a\n * promise that later rejects (an `async` function is assignable to the `void` callback type), is\n * swallowed here so user code can never bypass settlement or stop the consumer.\n */\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n try {\n const returned: unknown = this.options.onError?.(error, envelope, payload);\n if (isThenable(returned)) {\n // An async callback's rejection would otherwise surface as an unhandled rejection.\n returned.then(undefined, () => undefined);\n }\n } catch {\n // user callback failure must not escape the consume loop\n }\n }\n}\n","/**\n * The poison trigger set: a body is undecodable when a strict, typed decode into `Envelope` / `Meta` /\n * `DeadLetter` (the Go reference's `babelqueue.Decode`, except that member names are case-sensitive here) rejects it.\n *\n * - the body must be exactly one JSON value (no BOM, trailing tokens or malformed numbers); a bare `null`\n * decodes to an empty envelope, any other non-object is rejected;\n * - `job`, `trace_id`, `meta.id|queue|lang` and `dead_letter.reason|error|exception|original_queue|lang` are\n * string-or-null;\n * - `meta.schema_version|created_at`, `attempts` and `dead_letter.failed_at|attempts` are integer-or-null: an\n * integer literal (no fraction or exponent) that fits int64;\n * - `data`, `meta` and `dead_letter` are object-or-null; numbers inside `data` must be finite doubles;\n * - member names are matched exactly as spelled (case-sensitive, as the contract schema and the core decoder read\n * them) and every occurrence of a duplicated key is checked (`JSON.parse` would keep only the last, so this is a\n * dedicated scanner); any other member, including a differently-cased name or a non-string `urn` alias, is only\n * checked for syntax.\n *\n * A well-typed but non-conformant envelope is not poison (Go's consume path never calls `Accepts`).\n */\n\ntype Kind = \"string\" | \"integer\" | \"object\" | \"data\";\n\nconst MAX_DEPTH = 10_000;\nconst INT64_MIN = -(2n ** 63n);\nconst INT64_MAX = 2n ** 63n - 1n;\n\nconst META: Record<string, Kind> = {\n id: \"string\",\n queue: \"string\",\n lang: \"string\",\n schema_version: \"integer\",\n created_at: \"integer\",\n};\nconst DEAD_LETTER: Record<string, Kind> = {\n reason: \"string\",\n error: \"string\",\n exception: \"string\",\n original_queue: \"string\",\n lang: \"string\",\n failed_at: \"integer\",\n attempts: \"integer\",\n};\nconst TOP: Record<string, Kind> = {\n job: \"string\",\n trace_id: \"string\",\n attempts: \"integer\",\n data: \"data\",\n meta: \"object\",\n dead_letter: \"object\",\n};\n\nclass Reject extends Error {}\n\nfunction reject(message: string): never {\n throw new Reject(message);\n}\n\nconst isWs = (c: string | undefined): boolean => c === \" \" || c === \"\\t\" || c === \"\\n\" || c === \"\\r\";\nconst isDigit = (c: string | undefined): boolean => c !== undefined && c >= \"0\" && c <= \"9\";\n\nclass Scanner {\n private i = 0;\n /** Containers currently open around the cursor (Go counts nesting over the whole document, not per subtree). */\n private depth = 0;\n constructor(private readonly s: string) {}\n\n /** Why the body is undecodable, or `null` when Go's typed decode accepts it. */\n check(): string | null {\n try {\n this.ws();\n if (this.s.startsWith(\"null\", this.i)) {\n this.i += 4;\n } else if (this.s[this.i] === \"{\") {\n this.members(TOP, \"\");\n } else {\n reject(\"expected a JSON object.\");\n }\n this.ws();\n if (this.i < this.s.length) reject(\"unexpected trailing data after the JSON value.\");\n return null;\n } catch (error) {\n if (error instanceof Reject) return error.message;\n throw error;\n }\n }\n\n private ws(): void {\n while (isWs(this.s[this.i])) this.i++;\n }\n\n /** Reads an object whose members are checked against `kinds`; the cursor sits on its `{`. */\n private members(kinds: Record<string, Kind>, path: string): void {\n this.i++; // {\n this.depth++;\n this.ws();\n if (this.s[this.i] === \"}\") {\n this.i++;\n this.depth--;\n return;\n }\n for (;;) {\n this.ws();\n if (this.s[this.i] !== '\"') reject(\"expected an object key.\");\n const key = this.string();\n this.ws();\n if (this.s[this.i] !== \":\") reject(\"expected ':' after an object key.\");\n this.i++;\n this.ws();\n const kind = Object.prototype.hasOwnProperty.call(kinds, key) ? kinds[key] : undefined;\n if (kind === undefined) {\n this.skip(false);\n } else {\n this.typed(kind, path + key, key);\n }\n this.ws();\n const c = this.s[this.i++];\n if (c === \"}\") {\n this.depth--;\n return;\n }\n if (c !== \",\") reject(\"expected ',' or '}' in an object.\");\n }\n }\n\n private typed(kind: Kind, name: string, lower: string): void {\n const c = this.s[this.i];\n if (this.s.startsWith(\"null\", this.i)) {\n this.i += 4;\n return;\n }\n switch (kind) {\n case \"string\":\n if (c !== '\"') reject(`'${name}' must be a string.`);\n this.string();\n return;\n case \"integer\": {\n if (c !== \"-\" && !isDigit(c)) reject(`'${name}' must be an integer.`);\n const token = this.number();\n if (!/^-?[0-9]+$/.test(token)) reject(`'${name}' must be an integer.`);\n const n = BigInt(token);\n if (n < INT64_MIN || n > INT64_MAX) reject(`'${name}' does not fit an int64.`);\n return;\n }\n case \"data\":\n if (c !== \"{\") reject(`'${name}' must be a JSON object.`);\n this.skip(true);\n return;\n case \"object\":\n if (c !== \"{\") reject(`'${name}' must be a JSON object.`);\n this.members(lower === \"meta\" ? META : DEAD_LETTER, name + \".\");\n return;\n }\n }\n\n /** Reads a string literal and returns its decoded value; the cursor sits on the opening quote. */\n private string(): string {\n const start = this.i++;\n for (;;) {\n const c = this.s[this.i];\n if (c === undefined) reject(\"unterminated string.\");\n if (c === '\"') break;\n if (c < \" \") reject(\"control character in a string.\");\n if (c === \"\\\\\") {\n const e = this.s[this.i + 1];\n if (e === \"u\") {\n if (!/^[0-9a-fA-F]{4}$/.test(this.s.slice(this.i + 2, this.i + 6))) reject(\"invalid \\\\u escape.\");\n this.i += 6;\n continue;\n }\n if (e === undefined || !'\"\\\\/bfnrt'.includes(e)) reject(\"invalid escape in a string.\");\n this.i += 2;\n continue;\n }\n this.i++;\n }\n this.i++;\n return JSON.parse(this.s.slice(start, this.i)) as string;\n }\n\n /** Reads a number literal per the JSON grammar and returns its text. */\n private number(): string {\n const start = this.i;\n if (this.s[this.i] === \"-\") this.i++;\n if (this.s[this.i] === \"0\") {\n this.i++;\n } else if (isDigit(this.s[this.i])) {\n while (isDigit(this.s[this.i])) this.i++;\n } else {\n reject(\"invalid number.\");\n }\n if (this.s[this.i] === \".\") {\n this.i++;\n if (!isDigit(this.s[this.i])) reject(\"invalid number.\");\n while (isDigit(this.s[this.i])) this.i++;\n }\n if (this.s[this.i] === \"e\" || this.s[this.i] === \"E\") {\n this.i++;\n if (this.s[this.i] === \"+\" || this.s[this.i] === \"-\") this.i++;\n if (!isDigit(this.s[this.i])) reject(\"invalid number.\");\n while (isDigit(this.s[this.i])) this.i++;\n }\n return this.s.slice(start, this.i);\n }\n\n /** Skips one value iteratively (no recursion, so depth cannot overflow the stack); optionally requires finite numbers. */\n private skip(finite: boolean): void {\n const stack: string[] = [];\n let expectValue = true;\n for (;;) {\n this.ws();\n const c = this.s[this.i];\n if (expectValue) {\n if (c === \"{\" || c === \"[\") {\n if (this.depth + stack.length >= MAX_DEPTH) reject(\"exceeded the maximum nesting depth.\");\n stack.push(c);\n this.i++;\n this.ws();\n if (this.s[this.i] === (c === \"{\" ? \"}\" : \"]\")) {\n this.i++;\n stack.pop();\n expectValue = false;\n } else if (c === \"{\") {\n this.key();\n }\n continue;\n }\n if (c === '\"') {\n this.string();\n } else if (c === \"-\" || isDigit(c)) {\n const token = this.number();\n if (finite && !Number.isFinite(Number(token))) reject(\"a number in 'data' is not a finite double.\");\n } else if (this.s.startsWith(\"true\", this.i)) {\n this.i += 4;\n } else if (this.s.startsWith(\"false\", this.i)) {\n this.i += 5;\n } else if (this.s.startsWith(\"null\", this.i)) {\n this.i += 4;\n } else {\n reject(\"invalid JSON value.\");\n }\n expectValue = false;\n }\n if (stack.length === 0) return;\n const top = stack[stack.length - 1]!;\n this.ws();\n const d = this.s[this.i++];\n if (d === \",\") {\n if (top === \"{\") this.key();\n expectValue = true;\n } else if (d === (top === \"{\" ? \"}\" : \"]\")) {\n stack.pop();\n if (stack.length === 0) return;\n } else {\n reject(\"expected ',' or the end of a container.\");\n }\n }\n }\n\n /** Reads `\"key\" :` inside a skipped object. */\n private key(): void {\n this.ws();\n if (this.s[this.i] !== '\"') reject(\"expected an object key.\");\n this.string();\n this.ws();\n if (this.s[this.i] !== \":\") reject(\"expected ':' after an object key.\");\n this.i++;\n }\n}\n\n/** Why `raw` is not a decodable BabelQueue body, or `null` when Go's typed decode accepts it. */\nexport function envelopeViolation(raw: string): string | null {\n return new Scanner(raw).check();\n}\n\n/**\n * Normal form of a decimal number token: sign, significant digits without leading/trailing zeros, and a decimal\n * exponent, so `1.0`, `1e0` and `10e-1` compare equal. Zero is `0`.\n */\nfunction normalizedDecimal(token: string): string {\n const m = /^(-?)([0-9]+)(?:\\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?$/.exec(token);\n if (!m) return token;\n const fraction = m[3] ?? \"\";\n let digits = (m[2]! + fraction).replace(/^0+/, \"\");\n if (digits === \"\") return \"0\";\n let exponent = Number(m[4] ?? \"0\") - fraction.length;\n const trimmed = digits.replace(/0+$/, \"\");\n exponent += digits.length - trimmed.length;\n digits = trimmed;\n return `${m[1]}${digits}e${exponent}`;\n}\n\n/**\n * True when a JSON number token cannot be carried by a JS double without changing its value: it is not finite\n * once parsed, or the decimal value of the token differs from the decimal value of the double it becomes (an\n * integer beyond 2^53, `9007199254740993.0`, `9007199254740993e0`, `1.2345678901234567890123`). A literal that\n * merely changes spelling (`1.0` -> `1`, `1e2` -> `100`) is not lossy.\n */\nexport function isLossyNumberToken(token: string): boolean {\n const n = Number(token);\n if (!Number.isFinite(n)) return true;\n if (/^-?[0-9]+$/.test(token) && Number.isSafeInteger(n)) return false;\n return normalizedDecimal(token) !== normalizedDecimal(String(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
|
@@ -31,6 +31,12 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
|
31
31
|
* any other value is compared, untrimmed, to the supported `"1"`, and a record that does not match is
|
|
32
32
|
* rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and
|
|
33
33
|
* headers) forwarded to the DLQ (when configured) and the offset committed.
|
|
34
|
+
*
|
|
35
|
+
* **Poison bodies (§6.5).** A body that is not exactly one JSON object, whose typed members have the wrong
|
|
36
|
+
* JSON type, or that fails the conformance check is dead-lettered **wrapped**, as the Go reference does: a
|
|
37
|
+
* body that is one JSON object keeps its keys (anything else becomes `{"raw": "<body>"}`) and gains a
|
|
38
|
+
* `dead_letter` block with `reason: "poison"`; the record key is carried over unchanged. Only the
|
|
39
|
+
* version-gate rejection above still forwards the record raw.
|
|
34
40
|
*/
|
|
35
41
|
|
|
36
42
|
/** Incoming record headers (KafkaJS decodes header values to Buffers). */
|
|
@@ -42,8 +48,8 @@ interface KafkaProducerMessage {
|
|
|
42
48
|
key?: Buffer | string | null;
|
|
43
49
|
value: Buffer | string;
|
|
44
50
|
/**
|
|
45
|
-
* Header values are strings for records the adapter builds. Raw DLQ forwarding (gate
|
|
46
|
-
*
|
|
51
|
+
* Header values are strings for records the adapter builds. Raw DLQ forwarding (the version-gate
|
|
52
|
+
* rejection only; poison records are wrapped and carry string headers) instead passes the inbound values through as
|
|
47
53
|
* `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.
|
|
48
54
|
*/
|
|
49
55
|
headers?: {
|
|
@@ -84,6 +90,14 @@ interface KafkaConsumerClient {
|
|
|
84
90
|
offset: string;
|
|
85
91
|
}[]): Promise<void>;
|
|
86
92
|
}
|
|
93
|
+
/** The subset of the KafkaJS consumer {@link KafkaRedriveIO} calls to rewind a record (`consumer.seek`). */
|
|
94
|
+
interface KafkaSeekClient {
|
|
95
|
+
seek(position: {
|
|
96
|
+
topic: string;
|
|
97
|
+
partition: number;
|
|
98
|
+
offset: string;
|
|
99
|
+
}): void | Promise<void>;
|
|
100
|
+
}
|
|
87
101
|
/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */
|
|
88
102
|
declare function kafkaHeaders(envelope: Envelope): {
|
|
89
103
|
[key: string]: string;
|
|
@@ -151,6 +165,18 @@ declare class KafkaPublisher {
|
|
|
151
165
|
interface KafkaRedriveMessage {
|
|
152
166
|
body: string;
|
|
153
167
|
key?: Buffer | null;
|
|
168
|
+
/**
|
|
169
|
+
* The consumed record's value bytes (`null` for a tombstone) and headers. When supplied, a restore write
|
|
170
|
+
* (see {@link KafkaRedriveIO.publishFrom}) re-writes the original record verbatim — value bytes and
|
|
171
|
+
* headers as they were consumed — instead of re-deriving them from `body` (a lossy UTF-8 round trip
|
|
172
|
+
* for a non-UTF-8 value, and a re-projection of the `bq-*` headers).
|
|
173
|
+
*/
|
|
174
|
+
value?: Buffer | null;
|
|
175
|
+
headers?: IncomingHeaders;
|
|
176
|
+
/** The record's position, needed by {@link KafkaRedriveIO.rewind}; the offset is a KafkaJS string. */
|
|
177
|
+
topic?: string;
|
|
178
|
+
partition?: number;
|
|
179
|
+
offset?: string;
|
|
154
180
|
ack(): Promise<void>;
|
|
155
181
|
}
|
|
156
182
|
/** Options for {@link KafkaRedriveIO}. */
|
|
@@ -161,8 +187,29 @@ interface KafkaRedriveIOOptions {
|
|
|
161
187
|
* Reserve the next record from `queue` (the DLQ topic), or `null` when it is empty. Kafka consumption
|
|
162
188
|
* is push-based in KafkaJS, so the caller supplies the pull (a bounded fetch with manual commit) and
|
|
163
189
|
* its `ack` commits the offset. Return the record's `key` so the redrive can carry it.
|
|
190
|
+
*
|
|
191
|
+
* For a **verbatim restore** (a record written back to the DLQ unchanged: dry run, unselected, undecodable,
|
|
192
|
+
* failed redrive write) also return the consumed record's `topic`, `value` and `headers`: the IO treats a
|
|
193
|
+
* write as a restore only when the reserved message carries `topic` (the queue written to is that topic and
|
|
194
|
+
* the body is unchanged), and then re-writes `value` / `headers` / `key` as they were consumed with a fresh
|
|
195
|
+
* timestamp. A message without `topic` is restored through the republish path (`bq-*` headers re-projected
|
|
196
|
+
* from the body, `meta.created_at` as timestamp); without `value` / `headers` the body / projected headers
|
|
197
|
+
* stand in for them. `topic`, `partition` and `offset` are also what {@link KafkaRedriveIO.rewind} needs.
|
|
164
198
|
*/
|
|
165
199
|
pop(queue: string): Promise<KafkaRedriveMessage | null>;
|
|
200
|
+
/**
|
|
201
|
+
* The consumer the DLQ records are fetched with. When given, {@link KafkaRedriveIO.rewind} is offered:
|
|
202
|
+
* when the run stops (a failed restore write, a failed redrive write, a throwing `select` or a failed
|
|
203
|
+
* `ack`) it seeks this consumer back to the first unacknowledged record of every affected partition, so
|
|
204
|
+
* a later (cumulative) offset commit cannot skip a record that was not safely handled. Without it the IO
|
|
205
|
+
* has no `rewind` and a failed restore only leaves the record unacknowledged.
|
|
206
|
+
*
|
|
207
|
+
* The IO only knows the records it handed out. If {@link KafkaRedriveIOOptions.pop} reads ahead (buffers
|
|
208
|
+
* records it has not returned yet), the caller must discard that buffer after a rewind and must not
|
|
209
|
+
* reuse the IO: e.g. `pop` buffered p0@5..7, the run stopped at p0@6 and the seek went back to p0@5; a
|
|
210
|
+
* buffered p0@7 returned and acknowledged afterwards would commit offset 8 and skip 5 and 6.
|
|
211
|
+
*/
|
|
212
|
+
consumer?: KafkaSeekClient;
|
|
166
213
|
}
|
|
167
214
|
/**
|
|
168
215
|
* A `@babelqueue/core` `RedriveIO` over Kafka that carries the DLQ record key byte-for-byte through the
|
|
@@ -175,15 +222,43 @@ interface KafkaRedriveIOOptions {
|
|
|
175
222
|
*
|
|
176
223
|
* Plain {@link KafkaRedriveIO.publish} writes a keyless record. `publishWithHeaders` is not offered, so
|
|
177
224
|
* `bypass` is a no-op on this transport (as in the Go reference's keyed path).
|
|
225
|
+
*
|
|
226
|
+
* When `options.consumer` is supplied the IO also offers {@link KafkaRedriveIO.rewind} (core's optional
|
|
227
|
+
* `RedriveIO.rewind`): if the restore write of a record back to the DLQ fails, the consumer is sought
|
|
228
|
+
* back, because a Kafka offset commit is cumulative and a later commit would otherwise skip a record
|
|
229
|
+
* for good. Core's `redrive()` pops the whole batch before processing it, so the consumer position is
|
|
230
|
+
* already past every record handed out and not yet acknowledged; the IO tracks those and seeks every
|
|
231
|
+
* affected partition back to its first unacknowledged record, not only the failed one.
|
|
178
232
|
*/
|
|
179
233
|
declare class KafkaRedriveIO {
|
|
180
234
|
private readonly options;
|
|
235
|
+
/** Seek the consumer back to every unacknowledged record (`from` included unless it was acknowledged already); defined only when a `consumer` was supplied. */
|
|
236
|
+
readonly rewind?: (from: KafkaRedriveMessage) => Promise<void>;
|
|
237
|
+
/** Records handed out by {@link KafkaRedriveIO.pop} that carry a position and are not acknowledged yet. */
|
|
238
|
+
private readonly inFlight;
|
|
239
|
+
/** Records handed out by {@link KafkaRedriveIO.pop} that were acknowledged since; a rewind never re-reads them. */
|
|
240
|
+
private readonly acknowledged;
|
|
181
241
|
constructor(options: KafkaRedriveIOOptions);
|
|
182
242
|
pop(queue: string): Promise<KafkaRedriveMessage | null>;
|
|
183
243
|
/** Append an already-encoded body to `queue` as a keyless record. */
|
|
184
244
|
publish(queue: string, body: string): Promise<void>;
|
|
185
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* Append `body` to `queue` on behalf of the reserved record `from`. Two modes (Go/Java/.NET parity):
|
|
247
|
+
*
|
|
248
|
+
* - **Restore** — `queue` is the topic `from` was consumed from and `body` is the body `pop` returned for
|
|
249
|
+
* it (redrive writing an unchanged DLQ record back). The original record is re-written verbatim: its
|
|
250
|
+
* value bytes (`from.value`), headers (`from.headers`) and key, with a fresh timestamp so the restored
|
|
251
|
+
* copy does not age out of the DLQ early (never `meta.created_at`). A message that carries no `value`
|
|
252
|
+
* re-writes `body`; one that carries no `headers` falls back to the `bq-*` projection of a decodable body.
|
|
253
|
+
* Needs `from.topic` to be set; without it a restore is indistinguishable from a republish.
|
|
254
|
+
* Header values the consumer decoded as `null` (a header with a -1 length) are not re-written: the
|
|
255
|
+
* restore copy goes through `copyHeaders`, which drops them (a deliberate bound; the Go reference
|
|
256
|
+
* writes them as they are).
|
|
257
|
+
* - **Republish** — any other queue/body (a redrive to the work topic): the `bq-*` header projection and
|
|
258
|
+
* the `meta.created_at` timestamp, plus the key of `from` byte-for-byte (§6.5).
|
|
259
|
+
*/
|
|
186
260
|
publishFrom(queue: string, body: string, from: KafkaRedriveMessage): Promise<void>;
|
|
261
|
+
private restoreVerbatim;
|
|
187
262
|
private send;
|
|
188
263
|
}
|
|
189
264
|
/**
|
|
@@ -231,6 +306,9 @@ declare class KafkaConsumer {
|
|
|
231
306
|
private retryOrDeadLetter;
|
|
232
307
|
private republishRetry;
|
|
233
308
|
private deadLetter;
|
|
309
|
+
/** Dead-letter a poison record (wrapped), commit it, then report: settle first so `onError` cannot bypass it. */
|
|
310
|
+
private settlePoison;
|
|
311
|
+
private deadLetterPoison;
|
|
234
312
|
private deadLetterRaw;
|
|
235
313
|
/**
|
|
236
314
|
* Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
|
|
@@ -238,6 +316,7 @@ declare class KafkaConsumer {
|
|
|
238
316
|
* guaranteed.
|
|
239
317
|
*/
|
|
240
318
|
private produce;
|
|
319
|
+
private produceRaw;
|
|
241
320
|
private commit;
|
|
242
321
|
private originalTopic;
|
|
243
322
|
private requireProducer;
|
|
@@ -250,4 +329,4 @@ declare class KafkaConsumer {
|
|
|
250
329
|
private report;
|
|
251
330
|
}
|
|
252
331
|
|
|
253
|
-
export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, KafkaRedriveIO, type KafkaRedriveIOOptions, type KafkaRedriveMessage, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
|
|
332
|
+
export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, KafkaRedriveIO, type KafkaRedriveIOOptions, type KafkaRedriveMessage, type KafkaSeekClient, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,12 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
|
31
31
|
* any other value is compared, untrimmed, to the supported `"1"`, and a record that does not match is
|
|
32
32
|
* rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and
|
|
33
33
|
* headers) forwarded to the DLQ (when configured) and the offset committed.
|
|
34
|
+
*
|
|
35
|
+
* **Poison bodies (§6.5).** A body that is not exactly one JSON object, whose typed members have the wrong
|
|
36
|
+
* JSON type, or that fails the conformance check is dead-lettered **wrapped**, as the Go reference does: a
|
|
37
|
+
* body that is one JSON object keeps its keys (anything else becomes `{"raw": "<body>"}`) and gains a
|
|
38
|
+
* `dead_letter` block with `reason: "poison"`; the record key is carried over unchanged. Only the
|
|
39
|
+
* version-gate rejection above still forwards the record raw.
|
|
34
40
|
*/
|
|
35
41
|
|
|
36
42
|
/** Incoming record headers (KafkaJS decodes header values to Buffers). */
|
|
@@ -42,8 +48,8 @@ interface KafkaProducerMessage {
|
|
|
42
48
|
key?: Buffer | string | null;
|
|
43
49
|
value: Buffer | string;
|
|
44
50
|
/**
|
|
45
|
-
* Header values are strings for records the adapter builds. Raw DLQ forwarding (gate
|
|
46
|
-
*
|
|
51
|
+
* Header values are strings for records the adapter builds. Raw DLQ forwarding (the version-gate
|
|
52
|
+
* rejection only; poison records are wrapped and carry string headers) instead passes the inbound values through as
|
|
47
53
|
* `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.
|
|
48
54
|
*/
|
|
49
55
|
headers?: {
|
|
@@ -84,6 +90,14 @@ interface KafkaConsumerClient {
|
|
|
84
90
|
offset: string;
|
|
85
91
|
}[]): Promise<void>;
|
|
86
92
|
}
|
|
93
|
+
/** The subset of the KafkaJS consumer {@link KafkaRedriveIO} calls to rewind a record (`consumer.seek`). */
|
|
94
|
+
interface KafkaSeekClient {
|
|
95
|
+
seek(position: {
|
|
96
|
+
topic: string;
|
|
97
|
+
partition: number;
|
|
98
|
+
offset: string;
|
|
99
|
+
}): void | Promise<void>;
|
|
100
|
+
}
|
|
87
101
|
/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */
|
|
88
102
|
declare function kafkaHeaders(envelope: Envelope): {
|
|
89
103
|
[key: string]: string;
|
|
@@ -151,6 +165,18 @@ declare class KafkaPublisher {
|
|
|
151
165
|
interface KafkaRedriveMessage {
|
|
152
166
|
body: string;
|
|
153
167
|
key?: Buffer | null;
|
|
168
|
+
/**
|
|
169
|
+
* The consumed record's value bytes (`null` for a tombstone) and headers. When supplied, a restore write
|
|
170
|
+
* (see {@link KafkaRedriveIO.publishFrom}) re-writes the original record verbatim — value bytes and
|
|
171
|
+
* headers as they were consumed — instead of re-deriving them from `body` (a lossy UTF-8 round trip
|
|
172
|
+
* for a non-UTF-8 value, and a re-projection of the `bq-*` headers).
|
|
173
|
+
*/
|
|
174
|
+
value?: Buffer | null;
|
|
175
|
+
headers?: IncomingHeaders;
|
|
176
|
+
/** The record's position, needed by {@link KafkaRedriveIO.rewind}; the offset is a KafkaJS string. */
|
|
177
|
+
topic?: string;
|
|
178
|
+
partition?: number;
|
|
179
|
+
offset?: string;
|
|
154
180
|
ack(): Promise<void>;
|
|
155
181
|
}
|
|
156
182
|
/** Options for {@link KafkaRedriveIO}. */
|
|
@@ -161,8 +187,29 @@ interface KafkaRedriveIOOptions {
|
|
|
161
187
|
* Reserve the next record from `queue` (the DLQ topic), or `null` when it is empty. Kafka consumption
|
|
162
188
|
* is push-based in KafkaJS, so the caller supplies the pull (a bounded fetch with manual commit) and
|
|
163
189
|
* its `ack` commits the offset. Return the record's `key` so the redrive can carry it.
|
|
190
|
+
*
|
|
191
|
+
* For a **verbatim restore** (a record written back to the DLQ unchanged: dry run, unselected, undecodable,
|
|
192
|
+
* failed redrive write) also return the consumed record's `topic`, `value` and `headers`: the IO treats a
|
|
193
|
+
* write as a restore only when the reserved message carries `topic` (the queue written to is that topic and
|
|
194
|
+
* the body is unchanged), and then re-writes `value` / `headers` / `key` as they were consumed with a fresh
|
|
195
|
+
* timestamp. A message without `topic` is restored through the republish path (`bq-*` headers re-projected
|
|
196
|
+
* from the body, `meta.created_at` as timestamp); without `value` / `headers` the body / projected headers
|
|
197
|
+
* stand in for them. `topic`, `partition` and `offset` are also what {@link KafkaRedriveIO.rewind} needs.
|
|
164
198
|
*/
|
|
165
199
|
pop(queue: string): Promise<KafkaRedriveMessage | null>;
|
|
200
|
+
/**
|
|
201
|
+
* The consumer the DLQ records are fetched with. When given, {@link KafkaRedriveIO.rewind} is offered:
|
|
202
|
+
* when the run stops (a failed restore write, a failed redrive write, a throwing `select` or a failed
|
|
203
|
+
* `ack`) it seeks this consumer back to the first unacknowledged record of every affected partition, so
|
|
204
|
+
* a later (cumulative) offset commit cannot skip a record that was not safely handled. Without it the IO
|
|
205
|
+
* has no `rewind` and a failed restore only leaves the record unacknowledged.
|
|
206
|
+
*
|
|
207
|
+
* The IO only knows the records it handed out. If {@link KafkaRedriveIOOptions.pop} reads ahead (buffers
|
|
208
|
+
* records it has not returned yet), the caller must discard that buffer after a rewind and must not
|
|
209
|
+
* reuse the IO: e.g. `pop` buffered p0@5..7, the run stopped at p0@6 and the seek went back to p0@5; a
|
|
210
|
+
* buffered p0@7 returned and acknowledged afterwards would commit offset 8 and skip 5 and 6.
|
|
211
|
+
*/
|
|
212
|
+
consumer?: KafkaSeekClient;
|
|
166
213
|
}
|
|
167
214
|
/**
|
|
168
215
|
* A `@babelqueue/core` `RedriveIO` over Kafka that carries the DLQ record key byte-for-byte through the
|
|
@@ -175,15 +222,43 @@ interface KafkaRedriveIOOptions {
|
|
|
175
222
|
*
|
|
176
223
|
* Plain {@link KafkaRedriveIO.publish} writes a keyless record. `publishWithHeaders` is not offered, so
|
|
177
224
|
* `bypass` is a no-op on this transport (as in the Go reference's keyed path).
|
|
225
|
+
*
|
|
226
|
+
* When `options.consumer` is supplied the IO also offers {@link KafkaRedriveIO.rewind} (core's optional
|
|
227
|
+
* `RedriveIO.rewind`): if the restore write of a record back to the DLQ fails, the consumer is sought
|
|
228
|
+
* back, because a Kafka offset commit is cumulative and a later commit would otherwise skip a record
|
|
229
|
+
* for good. Core's `redrive()` pops the whole batch before processing it, so the consumer position is
|
|
230
|
+
* already past every record handed out and not yet acknowledged; the IO tracks those and seeks every
|
|
231
|
+
* affected partition back to its first unacknowledged record, not only the failed one.
|
|
178
232
|
*/
|
|
179
233
|
declare class KafkaRedriveIO {
|
|
180
234
|
private readonly options;
|
|
235
|
+
/** Seek the consumer back to every unacknowledged record (`from` included unless it was acknowledged already); defined only when a `consumer` was supplied. */
|
|
236
|
+
readonly rewind?: (from: KafkaRedriveMessage) => Promise<void>;
|
|
237
|
+
/** Records handed out by {@link KafkaRedriveIO.pop} that carry a position and are not acknowledged yet. */
|
|
238
|
+
private readonly inFlight;
|
|
239
|
+
/** Records handed out by {@link KafkaRedriveIO.pop} that were acknowledged since; a rewind never re-reads them. */
|
|
240
|
+
private readonly acknowledged;
|
|
181
241
|
constructor(options: KafkaRedriveIOOptions);
|
|
182
242
|
pop(queue: string): Promise<KafkaRedriveMessage | null>;
|
|
183
243
|
/** Append an already-encoded body to `queue` as a keyless record. */
|
|
184
244
|
publish(queue: string, body: string): Promise<void>;
|
|
185
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* Append `body` to `queue` on behalf of the reserved record `from`. Two modes (Go/Java/.NET parity):
|
|
247
|
+
*
|
|
248
|
+
* - **Restore** — `queue` is the topic `from` was consumed from and `body` is the body `pop` returned for
|
|
249
|
+
* it (redrive writing an unchanged DLQ record back). The original record is re-written verbatim: its
|
|
250
|
+
* value bytes (`from.value`), headers (`from.headers`) and key, with a fresh timestamp so the restored
|
|
251
|
+
* copy does not age out of the DLQ early (never `meta.created_at`). A message that carries no `value`
|
|
252
|
+
* re-writes `body`; one that carries no `headers` falls back to the `bq-*` projection of a decodable body.
|
|
253
|
+
* Needs `from.topic` to be set; without it a restore is indistinguishable from a republish.
|
|
254
|
+
* Header values the consumer decoded as `null` (a header with a -1 length) are not re-written: the
|
|
255
|
+
* restore copy goes through `copyHeaders`, which drops them (a deliberate bound; the Go reference
|
|
256
|
+
* writes them as they are).
|
|
257
|
+
* - **Republish** — any other queue/body (a redrive to the work topic): the `bq-*` header projection and
|
|
258
|
+
* the `meta.created_at` timestamp, plus the key of `from` byte-for-byte (§6.5).
|
|
259
|
+
*/
|
|
186
260
|
publishFrom(queue: string, body: string, from: KafkaRedriveMessage): Promise<void>;
|
|
261
|
+
private restoreVerbatim;
|
|
187
262
|
private send;
|
|
188
263
|
}
|
|
189
264
|
/**
|
|
@@ -231,6 +306,9 @@ declare class KafkaConsumer {
|
|
|
231
306
|
private retryOrDeadLetter;
|
|
232
307
|
private republishRetry;
|
|
233
308
|
private deadLetter;
|
|
309
|
+
/** Dead-letter a poison record (wrapped), commit it, then report: settle first so `onError` cannot bypass it. */
|
|
310
|
+
private settlePoison;
|
|
311
|
+
private deadLetterPoison;
|
|
234
312
|
private deadLetterRaw;
|
|
235
313
|
/**
|
|
236
314
|
* Republish an envelope. The consumed record's key is carried over byte-for-byte (an empty key stays
|
|
@@ -238,6 +316,7 @@ declare class KafkaConsumer {
|
|
|
238
316
|
* guaranteed.
|
|
239
317
|
*/
|
|
240
318
|
private produce;
|
|
319
|
+
private produceRaw;
|
|
241
320
|
private commit;
|
|
242
321
|
private originalTopic;
|
|
243
322
|
private requireProducer;
|
|
@@ -250,4 +329,4 @@ declare class KafkaConsumer {
|
|
|
250
329
|
private report;
|
|
251
330
|
}
|
|
252
331
|
|
|
253
|
-
export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, KafkaRedriveIO, type KafkaRedriveIOOptions, type KafkaRedriveMessage, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
|
|
332
|
+
export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, KafkaRedriveIO, type KafkaRedriveIOOptions, type KafkaRedriveMessage, type KafkaSeekClient, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
|