@babelqueue/kafka 1.0.0 → 1.1.2
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 +24 -0
- package/dist/index.cjs +79 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -5
- package/dist/index.d.ts +41 -5
- package/dist/index.js +79 -12
- package/dist/index.js.map +1 -1
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -61,6 +61,12 @@ tier (requires `KafkaPublisher.withRetryTopics`); on a plain publisher a delay t
|
|
|
61
61
|
throwing handler republishes to the next `<topic>.retry.<n>` tier with `bq-attempts + 1`, then
|
|
62
62
|
commits; once `maxTries` is reached it goes to `<topic>.dlq` with a `dead_letter` block.
|
|
63
63
|
|
|
64
|
+
**Version gate (§6.7).** Before the record value is decoded the consumer reads the
|
|
65
|
+
`bq-schema-version` header. A missing or blank value (empty or only ASCII whitespace) follows the
|
|
66
|
+
normal decode path; any other value is compared as-is (never trimmed) to `"1"`. A record that does
|
|
67
|
+
not match is rejected undecoded as poison: reported to `onError`, forwarded raw to `<topic>.dlq`
|
|
68
|
+
(when configured) and its offset committed.
|
|
69
|
+
|
|
64
70
|
## Contract mapping (§6)
|
|
65
71
|
|
|
66
72
|
| Envelope | Apache Kafka |
|
|
@@ -81,6 +87,24 @@ All header values are UTF-8 strings (integers as decimal strings, e.g. `"1"`). T
|
|
|
81
87
|
producer/consumer are injected, so the unit tests use fakes — no Kafka, no broker. Dual
|
|
82
88
|
ESM + CJS.
|
|
83
89
|
|
|
90
|
+
## OpenTelemetry tracing (ADR-0028)
|
|
91
|
+
|
|
92
|
+
Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
|
|
93
|
+
[`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
|
|
94
|
+
`@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
|
|
95
|
+
native Kafka record headers, beside the contract `bq-*` headers (the contract wins a key collision). On consume, the consumer surfaces a delivered message's headers to the handler's third argument (and a `headersOf(...)` extractor reads them back),
|
|
96
|
+
so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { trace } from "@opentelemetry/api";
|
|
100
|
+
import { publish as tracedPublish } from "@babelqueue/core/otel";
|
|
101
|
+
import type { HeaderCarrier } from "@babelqueue/core";
|
|
102
|
+
|
|
103
|
+
const headers: HeaderCarrier = {};
|
|
104
|
+
await tracedPublish(trace.getTracer("orders"), urn, data,
|
|
105
|
+
() => adapterPublish(urn, data, { headers }), { headers });
|
|
106
|
+
```
|
|
107
|
+
|
|
84
108
|
## License
|
|
85
109
|
|
|
86
110
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -23,10 +23,32 @@ __export(index_exports, {
|
|
|
23
23
|
KafkaConsumer: () => KafkaConsumer,
|
|
24
24
|
KafkaPublisher: () => KafkaPublisher,
|
|
25
25
|
RetryTopics: () => RetryTopics,
|
|
26
|
+
headersOf: () => headersOf,
|
|
26
27
|
kafkaHeaders: () => kafkaHeaders
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(index_exports);
|
|
29
30
|
var import_core = require("@babelqueue/core");
|
|
31
|
+
|
|
32
|
+
// src/headers.ts
|
|
33
|
+
function sanitizeHeaders(headers) {
|
|
34
|
+
const out = {};
|
|
35
|
+
if (!headers) return out;
|
|
36
|
+
for (const key of Object.keys(headers)) {
|
|
37
|
+
const value = headers[key];
|
|
38
|
+
if (key === "" || value == null || value === "") continue;
|
|
39
|
+
out[key] = value;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function mergeInto(base, extra) {
|
|
44
|
+
for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {
|
|
45
|
+
if (key in base) continue;
|
|
46
|
+
base[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return base;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/index.ts
|
|
30
52
|
function kafkaHeaders(envelope) {
|
|
31
53
|
const headers = {};
|
|
32
54
|
if (envelope.job) headers["bq-job"] = envelope.job;
|
|
@@ -37,11 +59,25 @@ function kafkaHeaders(envelope) {
|
|
|
37
59
|
headers["bq-attempts"] = String(envelope.attempts ?? 0);
|
|
38
60
|
return headers;
|
|
39
61
|
}
|
|
62
|
+
function copyHeaders(headers) {
|
|
63
|
+
const out = {};
|
|
64
|
+
if (!headers) return out;
|
|
65
|
+
const copy = (v) => Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from(v, "utf8");
|
|
66
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
67
|
+
if (value === void 0 || value === null) continue;
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
out[name] = value.filter((v) => v !== void 0 && v !== null).map(copy);
|
|
70
|
+
} else {
|
|
71
|
+
out[name] = copy(value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
40
76
|
function headerString(headers, key) {
|
|
41
77
|
const value = headers?.[key];
|
|
42
|
-
if (value === void 0) return void 0;
|
|
78
|
+
if (value === void 0 || value === null) return void 0;
|
|
43
79
|
const v = Array.isArray(value) ? value[0] : value;
|
|
44
|
-
if (v === void 0) return void 0;
|
|
80
|
+
if (v === void 0 || v === null) return void 0;
|
|
45
81
|
return Buffer.isBuffer(v) ? v.toString("utf8") : String(v);
|
|
46
82
|
}
|
|
47
83
|
function headerInt(headers, key, fallback) {
|
|
@@ -50,6 +86,22 @@ function headerInt(headers, key, fallback) {
|
|
|
50
86
|
const n = Number.parseInt(s.trim(), 10);
|
|
51
87
|
return Number.isNaN(n) ? fallback : n;
|
|
52
88
|
}
|
|
89
|
+
var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
|
|
90
|
+
function unsupportedSchemaVersion(message) {
|
|
91
|
+
const value = headerString(message.headers, "bq-schema-version");
|
|
92
|
+
if (value === void 0 || BLANK_ASCII.test(value)) return void 0;
|
|
93
|
+
return value === String(import_core.SCHEMA_VERSION) ? void 0 : value;
|
|
94
|
+
}
|
|
95
|
+
function headersOf(message) {
|
|
96
|
+
const headers = message.headers;
|
|
97
|
+
const out = {};
|
|
98
|
+
if (!headers) return out;
|
|
99
|
+
for (const key of Object.keys(headers)) {
|
|
100
|
+
const value = headerString(headers, key);
|
|
101
|
+
if (value !== void 0 && value !== "") out[key] = value;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
53
105
|
var RetryTopics = class {
|
|
54
106
|
constructor(workTopic, delaysMs = [], dlqTopic = void 0) {
|
|
55
107
|
this.workTopic = workTopic;
|
|
@@ -108,14 +160,14 @@ var KafkaPublisher = class _KafkaPublisher {
|
|
|
108
160
|
throw new import_core.BabelQueueError("Kafka has no native delayed delivery; a delay requires retry topics (none configured).");
|
|
109
161
|
}
|
|
110
162
|
const tier = this.retryTopics.tierForDelay(options.delayMs);
|
|
111
|
-
await this.sendRecord(tier.topic, envelope, options.delayMs, this.workTopic);
|
|
163
|
+
await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);
|
|
112
164
|
} else {
|
|
113
|
-
await this.sendRecord(this.workTopic, envelope);
|
|
165
|
+
await this.sendRecord(this.workTopic, envelope, options.headers);
|
|
114
166
|
}
|
|
115
167
|
return envelope.meta.id;
|
|
116
168
|
}
|
|
117
|
-
async sendRecord(topic, envelope, delayMs, originalTopic) {
|
|
118
|
-
const headers = kafkaHeaders(envelope);
|
|
169
|
+
async sendRecord(topic, envelope, extra, delayMs, originalTopic) {
|
|
170
|
+
const headers = mergeInto(kafkaHeaders(envelope), extra);
|
|
119
171
|
if (delayMs != null) headers["bq-delay"] = String(delayMs);
|
|
120
172
|
if (originalTopic != null) headers["bq-original-topic"] = originalTopic;
|
|
121
173
|
await this.producer.send({
|
|
@@ -144,6 +196,17 @@ var KafkaConsumer = class {
|
|
|
144
196
|
/** Route + settle one record (process-then-commit). Exposed for testing. */
|
|
145
197
|
async handle(payload) {
|
|
146
198
|
const { message } = payload;
|
|
199
|
+
const unsupported = unsupportedSchemaVersion(message);
|
|
200
|
+
if (unsupported !== void 0) {
|
|
201
|
+
this.report(
|
|
202
|
+
new import_core.BabelQueueError(`Rejected a record from Kafka with unsupported bq-schema-version "${unsupported}" (supported: ${import_core.SCHEMA_VERSION}).`),
|
|
203
|
+
null,
|
|
204
|
+
payload
|
|
205
|
+
);
|
|
206
|
+
await this.deadLetterRaw(payload);
|
|
207
|
+
await this.commit(payload);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
147
210
|
const raw = message.value ? message.value.toString("utf8") : "";
|
|
148
211
|
let envelope;
|
|
149
212
|
try {
|
|
@@ -167,7 +230,7 @@ var KafkaConsumer = class {
|
|
|
167
230
|
return;
|
|
168
231
|
}
|
|
169
232
|
try {
|
|
170
|
-
await handler(envelope, message);
|
|
233
|
+
await handler(envelope, message, headersOf(message));
|
|
171
234
|
await this.commit(payload);
|
|
172
235
|
} catch (error) {
|
|
173
236
|
this.report(error, envelope, payload);
|
|
@@ -242,10 +305,14 @@ var KafkaConsumer = class {
|
|
|
242
305
|
if (!dlq) return;
|
|
243
306
|
this.requireProducer();
|
|
244
307
|
const { message } = payload;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
308
|
+
const forwarded = {
|
|
309
|
+
value: message.value ?? Buffer.alloc(0),
|
|
310
|
+
// KafkaJS accepts Buffer / array header values; the published structural type stays string-only.
|
|
311
|
+
headers: copyHeaders(message.headers),
|
|
312
|
+
timestamp: String(Date.now())
|
|
313
|
+
};
|
|
314
|
+
if (message.key !== void 0 && message.key !== null) forwarded.key = Buffer.from(message.key);
|
|
315
|
+
await this.options.producer.send({ topic: dlq, messages: [forwarded] });
|
|
249
316
|
}
|
|
250
317
|
async produce(topic, envelope, headers) {
|
|
251
318
|
this.requireProducer();
|
|
@@ -276,6 +343,7 @@ var KafkaConsumer = class {
|
|
|
276
343
|
KafkaConsumer,
|
|
277
344
|
KafkaPublisher,
|
|
278
345
|
RetryTopics,
|
|
346
|
+
headersOf,
|
|
279
347
|
kafkaHeaders
|
|
280
348
|
});
|
|
281
349
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.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\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\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)[] | 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 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\nfunction headerString(headers: IncomingHeaders | undefined, key: string): string | undefined {\n const value = headers?.[key];\n if (value === undefined) return undefined;\n const v = Array.isArray(value) ? value[0] : value;\n if (v === undefined) return undefined;\n return Buffer.isBuffer(v) ? v.toString(\"utf8\") : String(v);\n}\n\nfunction headerInt(headers: IncomingHeaders | undefined, key: string, fallback: number): number {\n const s = headerString(headers, key);\n if (s === undefined) return fallback;\n const n = Number.parseInt(s.trim(), 10);\n return Number.isNaN(n) ? fallback : n;\n}\n\n// --- 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\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.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(topic: string, envelope: Envelope, delayMs?: number, originalTopic?: string): Promise<void> {\n const headers = kafkaHeaders(envelope);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw Kafka message. */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage) => 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 const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n this.report(decodeError, null, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // A non-conformant / poison envelope may be missing the fields needed to annotate, so\n // forward the raw record to the DLQ rather than building a dead_letter block.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Kafka.\"), envelope, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const urn = headerString(message.headers, \"bq-job\") ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(payload, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message);\n await this.commit(payload);\n } catch (error) {\n this.report(error, envelope, payload);\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError before this; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers);\n }\n\n private async deadLetterRaw(payload: EachMessagePayload): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return;\n this.requireProducer();\n const { message } = payload;\n await this.options.producer!.send({\n topic: dlq,\n messages: [{ value: message.value ?? Buffer.alloc(0), timestamp: String(Date.now()) }],\n });\n }\n\n private async produce(topic: string, envelope: Envelope, headers: { [key: string]: string }): Promise<void> {\n this.requireProducer();\n await this.options.producer!.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }],\n });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n this.options.onError?.(error, envelope, payload);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache Kafka adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed, **process-then-commit** consumer over\n * Apache Kafka (KafkaJS), so a Kafka-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * Kafka has **no native** per-message ack, delayed delivery, dead-letter queue, or delivery\n * counter — this adapter absorbs all four in the binding layer (the envelope stays\n * `schema_version: 1`): the envelope JSON is the record **value**; the contract fields are\n * mirrored onto `bq-` headers (route on `bq-job` without decoding the body) and the record\n * timestamp mirrors `meta.created_at`; **`bq-attempts` is the authoritative retry counter**;\n * consume is process-then-commit (manual commit, at-least-once); retry/delay use SDK-owned\n * tiered retry topics `<topic>.retry.<n>`; terminal failures go to an opt-in `<topic>.dlq`.\n *\n * This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you\n * provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter\n * structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional Kafka **record headers** beside the contract `bq-*` headers —\n * the contract headers win a key collision (merge-not-clobber), and blanks are dropped. On consume\n * the inbound record headers are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope (the record value) is never touched.\n *\n * **Version gate (§6.7).** Before decoding the record value the consumer reads the `bq-schema-version`\n * header. A missing or blank value (empty or only ASCII whitespace) follows the normal decode path;\n * any other value is compared, untrimmed, to the supported `\"1\"`, and a record that does not match is\n * rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and\n * headers) forwarded to the DLQ (when configured) and the offset committed.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Kafka shapes (a structural subset of kafkajs) ---------------------\n\n/** Incoming record headers (KafkaJS decodes header values to Buffers). */\nexport type IncomingHeaders = { [key: string]: Buffer | string | (Buffer | string)[] | null | undefined };\n\n/** A record to produce (structural subset of a KafkaJS message). */\nexport interface KafkaProducerMessage {\n key?: Buffer | string | null;\n value: Buffer | string;\n /**\n * Header values are strings for records the adapter builds. Raw DLQ forwarding (gate rejection,\n * decode error, non-conformant envelope) instead passes the inbound values through as\n * `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.\n */\n headers?: { [key: string]: string };\n timestamp?: string;\n}\n\n/** The subset of the KafkaJS producer this adapter calls. */\nexport interface KafkaProducerClient {\n send(record: { topic: string; messages: KafkaProducerMessage[] }): Promise<unknown>;\n}\n\n/** A received record (structural subset of a KafkaJS message). */\nexport interface KafkaIncomingMessage {\n value: Buffer | null;\n headers?: IncomingHeaders;\n offset: string;\n timestamp?: string;\n key?: Buffer | null;\n}\n\n/** The payload KafkaJS passes to `eachMessage`. */\nexport interface EachMessagePayload {\n topic: string;\n partition: number;\n message: KafkaIncomingMessage;\n}\n\n/** The subset of the KafkaJS consumer this adapter calls (manual commit — `autoCommit: false`). */\nexport interface KafkaConsumerClient {\n run(config: { autoCommit?: boolean; eachMessage: (payload: EachMessagePayload) => Promise<void> }): Promise<void>;\n commitOffsets(offsets: { topic: string; partition: number; offset: string }[]): Promise<void>;\n}\n\n// --- Header projection (contract §6.3) -----------------------------------------\n\n/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */\nexport function kafkaHeaders(envelope: Envelope): { [key: string]: string } {\n const headers: { [key: string]: string } = {};\n if (envelope.job) headers[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) headers[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) headers[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) headers[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n if (envelope.meta.lang) headers[\"bq-source-lang\"] = envelope.meta.lang;\n headers[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return headers;\n}\n\n/**\n * Copy inbound record headers for a raw DLQ forward: every value becomes a `Buffer` copy (a string is\n * UTF-8 encoded), array-valued headers keep all their entries, and `undefined` / `null` values are dropped.\n */\nfunction copyHeaders(headers: IncomingHeaders | undefined): { [key: string]: Buffer | Buffer[] } {\n const out: { [key: string]: Buffer | Buffer[] } = {};\n if (!headers) return out;\n const copy = (v: Buffer | string): Buffer => (Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from(v, \"utf8\"));\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n out[name] = value.filter((v) => v !== undefined && v !== null).map(copy);\n } else {\n out[name] = copy(value);\n }\n }\n return out;\n}\n\nfunction headerString(headers: IncomingHeaders | undefined, key: string): string | undefined {\n const value = headers?.[key];\n // A null header value is \"no header\" (broker-bindings.md §6.7): never stringified to \"null\".\n if (value === undefined || value === null) return undefined;\n const v = Array.isArray(value) ? value[0] : value;\n if (v === undefined || v === null) return undefined;\n return Buffer.isBuffer(v) ? v.toString(\"utf8\") : String(v);\n}\n\nfunction headerInt(headers: IncomingHeaders | undefined, key: string, fallback: number): number {\n const s = headerString(headers, key);\n if (s === undefined) return fallback;\n const n = Number.parseInt(s.trim(), 10);\n return Number.isNaN(n) ? fallback : n;\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq-schema-version` header when it is present, non-blank and not exactly the supported\n * version (compared untrimmed), else `undefined` (the record may be decoded).\n */\nfunction unsupportedSchemaVersion(message: KafkaIncomingMessage): string | undefined {\n const value = headerString(message.headers, \"bq-schema-version\");\n if (value === undefined || BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\n/**\n * Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer\n * values. Returns an empty object when there are none. Both the contract `bq-*` headers and any\n * out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it\n * knows.\n */\nexport function headersOf(message: KafkaIncomingMessage): HeaderCarrier {\n const headers = message.headers;\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headerString(headers, key);\n if (value !== undefined && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Retry / delay topology (contract §6.4–§6.5) -------------------------------\n\n/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */\nexport interface Tier {\n topic: string;\n delayMs: number;\n}\n\n/**\n * The SDK-owned retry/delay topology for one work topic: tiered delay topics\n * `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no\n * tiers configured raises rather than silently dropping.\n */\nexport class RetryTopics {\n readonly tiers: Tier[];\n readonly dlqTopic: string | null;\n\n constructor(\n readonly workTopic: string,\n delaysMs: number[] = [],\n dlqTopic: string | null | undefined = undefined,\n ) {\n this.tiers = [...delaysMs]\n .sort((a, b) => a - b)\n .map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));\n this.dlqTopic = dlqTopic === undefined ? `${workTopic}.dlq` : dlqTopic;\n }\n\n hasTiers(): boolean {\n return this.tiers.length > 0;\n }\n\n /** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */\n tierForDelay(delayMs: number): Tier {\n this.requireTiers();\n for (const tier of this.tiers) {\n if (tier.delayMs >= delayMs) return tier;\n }\n throw new BabelQueueError(\n `Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1]!.delayMs}ms).`,\n );\n }\n\n /** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */\n tierForAttempt(attempt: number): Tier {\n this.requireTiers();\n return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)]!;\n }\n\n private requireTiers(): void {\n if (!this.tiers.length) {\n throw new BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);\n }\n }\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link KafkaPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`\n * headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */\nexport class KafkaPublisher {\n private constructor(\n private readonly producer: KafkaProducerClient,\n private readonly workTopic: string,\n private readonly retryTopics?: RetryTopics,\n ) {}\n\n /** A publisher onto `topic` with no retry topics (a delay raises). */\n static create(producer: KafkaProducerClient, topic: string): KafkaPublisher {\n return new KafkaPublisher(producer, topic);\n }\n\n /** A publisher onto the topology's work topic, with delay routed via its retry tiers. */\n static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher {\n return new KafkaPublisher(producer, retryTopics.workTopic, retryTopics);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });\n if (options.delayMs != null && options.delayMs > 0) {\n if (!this.retryTopics) {\n throw new BabelQueueError(\"Kafka has no native delayed delivery; a delay requires retry topics (none configured).\");\n }\n const tier = this.retryTopics.tierForDelay(options.delayMs);\n await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope, options.headers);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(\n topic: string,\n envelope: Envelope,\n extra?: HeaderCarrier | null,\n delayMs?: number,\n originalTopic?: string,\n ): Promise<void> {\n // The contract bq-* headers are seeded first, then the out-of-band riders are merged beside\n // them without clobbering (the contract wins a collision).\n const headers = mergeInto(kafkaHeaders(envelope), extra);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band\n * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to\n * `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span\n * (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link KafkaConsumer}. */\nexport interface KafkaConsumerOptions {\n /** The producer used to republish retry/DLQ records (required for retry/DLQ). */\n producer?: KafkaProducerClient;\n /** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */\n retryTopics?: RetryTopics;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a poison record, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;\n}\n\n/**\n * Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,\n * routed to the handler for its URN (read from `bq-job`), and its offset committed only after\n * the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with\n * `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`\n * block. The `bq-attempts` header is the authoritative counter (the body is the fallback).\n */\nexport class KafkaConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly consumer: KafkaConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: KafkaConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */\n async run(): Promise<void> {\n await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });\n }\n\n /** Route + settle one record (process-then-commit). Exposed for testing. */\n async handle(payload: EachMessagePayload): Promise<void> {\n const { message } = payload;\n\n // Version gate (§6.7): decided on the header alone, before the value is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n this.report(\n new BabelQueueError(`Rejected a record from Kafka with unsupported bq-schema-version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`),\n null,\n payload,\n );\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n this.report(decodeError, null, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // A non-conformant / poison envelope may be missing the fields needed to annotate, so\n // forward the raw record to the DLQ rather than building a dead_letter block.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Kafka.\"), envelope, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const urn = headerString(message.headers, \"bq-job\") ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(payload, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n await this.commit(payload);\n } catch (error) {\n this.report(error, envelope, payload);\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError before this; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers);\n }\n\n private async deadLetterRaw(payload: EachMessagePayload): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return;\n this.requireProducer();\n const { message } = payload;\n // Forward the original record verbatim (value, key, headers) so the DLQ copy keeps its provenance\n // (Java reference; the .NET consumer forwards value + headers). Only the timestamp is re-stamped.\n const forwarded: KafkaProducerMessage = {\n value: message.value ?? Buffer.alloc(0),\n // KafkaJS accepts Buffer / array header values; the published structural type stays string-only.\n headers: copyHeaders(message.headers) as unknown as KafkaProducerMessage[\"headers\"],\n timestamp: String(Date.now()),\n };\n if (message.key !== undefined && message.key !== null) forwarded.key = Buffer.from(message.key);\n await this.options.producer!.send({ topic: dlq, messages: [forwarded] });\n }\n\n private async produce(topic: string, envelope: Envelope, headers: { [key: string]: string }): Promise<void> {\n this.requireProducer();\n await this.options.producer!.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }],\n });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n this.options.onError?.(error, envelope, payload);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Merge out-of-band `extra` headers onto an already-built `base` header map **without clobbering**\n * the contract headers in `base` (they win a key collision) and dropping blanks. The result is\n * `base`, mutated in place.\n */\nexport function mergeInto(base: { [key: string]: string }, extra: HeaderCarrier | null | undefined): { [key: string]: string } {\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {\n if (key in base) continue; // the contract header already there wins\n base[key] = value;\n }\n return base;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
1
|
+
import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Apache Kafka adapter for BabelQueue.
|
|
@@ -18,16 +18,34 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
|
18
18
|
* This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you
|
|
19
19
|
* provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter
|
|
20
20
|
* structurally).
|
|
21
|
+
*
|
|
22
|
+
* **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
|
|
23
|
+
* span linkage) rides as additional Kafka **record headers** beside the contract `bq-*` headers —
|
|
24
|
+
* the contract headers win a key collision (merge-not-clobber), and blanks are dropped. On consume
|
|
25
|
+
* the inbound record headers are surfaced as a `Record<string,string>` so the core's `otel` extract
|
|
26
|
+
* sees the `traceparent` and links the consumer span as a child. A header-less publish is
|
|
27
|
+
* byte-identical. GR-1: the wire envelope (the record value) is never touched.
|
|
28
|
+
*
|
|
29
|
+
* **Version gate (§6.7).** Before decoding the record value the consumer reads the `bq-schema-version`
|
|
30
|
+
* header. A missing or blank value (empty or only ASCII whitespace) follows the normal decode path;
|
|
31
|
+
* any other value is compared, untrimmed, to the supported `"1"`, and a record that does not match is
|
|
32
|
+
* rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and
|
|
33
|
+
* headers) forwarded to the DLQ (when configured) and the offset committed.
|
|
21
34
|
*/
|
|
22
35
|
|
|
23
36
|
/** Incoming record headers (KafkaJS decodes header values to Buffers). */
|
|
24
37
|
type IncomingHeaders = {
|
|
25
|
-
[key: string]: Buffer | string | (Buffer | string)[] | undefined;
|
|
38
|
+
[key: string]: Buffer | string | (Buffer | string)[] | null | undefined;
|
|
26
39
|
};
|
|
27
40
|
/** A record to produce (structural subset of a KafkaJS message). */
|
|
28
41
|
interface KafkaProducerMessage {
|
|
29
42
|
key?: Buffer | string | null;
|
|
30
43
|
value: Buffer | string;
|
|
44
|
+
/**
|
|
45
|
+
* Header values are strings for records the adapter builds. Raw DLQ forwarding (gate rejection,
|
|
46
|
+
* decode error, non-conformant envelope) instead passes the inbound values through as
|
|
47
|
+
* `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.
|
|
48
|
+
*/
|
|
31
49
|
headers?: {
|
|
32
50
|
[key: string]: string;
|
|
33
51
|
};
|
|
@@ -70,6 +88,13 @@ interface KafkaConsumerClient {
|
|
|
70
88
|
declare function kafkaHeaders(envelope: Envelope): {
|
|
71
89
|
[key: string]: string;
|
|
72
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer
|
|
93
|
+
* values. Returns an empty object when there are none. Both the contract `bq-*` headers and any
|
|
94
|
+
* out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it
|
|
95
|
+
* knows.
|
|
96
|
+
*/
|
|
97
|
+
declare function headersOf(message: KafkaIncomingMessage): HeaderCarrier;
|
|
73
98
|
/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */
|
|
74
99
|
interface Tier {
|
|
75
100
|
topic: string;
|
|
@@ -97,6 +122,12 @@ interface PublishOptions {
|
|
|
97
122
|
traceId?: string;
|
|
98
123
|
/** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */
|
|
99
124
|
delayMs?: number;
|
|
125
|
+
/**
|
|
126
|
+
* Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`
|
|
127
|
+
* headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
|
|
128
|
+
* The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.
|
|
129
|
+
*/
|
|
130
|
+
headers?: HeaderCarrier;
|
|
100
131
|
}
|
|
101
132
|
/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */
|
|
102
133
|
declare class KafkaPublisher {
|
|
@@ -112,8 +143,13 @@ declare class KafkaPublisher {
|
|
|
112
143
|
publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
|
|
113
144
|
private sendRecord;
|
|
114
145
|
}
|
|
115
|
-
/**
|
|
116
|
-
|
|
146
|
+
/**
|
|
147
|
+
* A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band
|
|
148
|
+
* {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to
|
|
149
|
+
* `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span
|
|
150
|
+
* (ADR-0028).
|
|
151
|
+
*/
|
|
152
|
+
type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
|
|
117
153
|
/** A map of URN → handler. */
|
|
118
154
|
type BabelHandlers = Record<string, BabelHandler>;
|
|
119
155
|
/** Options for {@link KafkaConsumer}. */
|
|
@@ -160,4 +196,4 @@ declare class KafkaConsumer {
|
|
|
160
196
|
private report;
|
|
161
197
|
}
|
|
162
198
|
|
|
163
|
-
export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, type PublishOptions, RetryTopics, type Tier, kafkaHeaders };
|
|
199
|
+
export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
1
|
+
import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Apache Kafka adapter for BabelQueue.
|
|
@@ -18,16 +18,34 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
|
18
18
|
* This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you
|
|
19
19
|
* provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter
|
|
20
20
|
* structurally).
|
|
21
|
+
*
|
|
22
|
+
* **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
|
|
23
|
+
* span linkage) rides as additional Kafka **record headers** beside the contract `bq-*` headers —
|
|
24
|
+
* the contract headers win a key collision (merge-not-clobber), and blanks are dropped. On consume
|
|
25
|
+
* the inbound record headers are surfaced as a `Record<string,string>` so the core's `otel` extract
|
|
26
|
+
* sees the `traceparent` and links the consumer span as a child. A header-less publish is
|
|
27
|
+
* byte-identical. GR-1: the wire envelope (the record value) is never touched.
|
|
28
|
+
*
|
|
29
|
+
* **Version gate (§6.7).** Before decoding the record value the consumer reads the `bq-schema-version`
|
|
30
|
+
* header. A missing or blank value (empty or only ASCII whitespace) follows the normal decode path;
|
|
31
|
+
* any other value is compared, untrimmed, to the supported `"1"`, and a record that does not match is
|
|
32
|
+
* rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and
|
|
33
|
+
* headers) forwarded to the DLQ (when configured) and the offset committed.
|
|
21
34
|
*/
|
|
22
35
|
|
|
23
36
|
/** Incoming record headers (KafkaJS decodes header values to Buffers). */
|
|
24
37
|
type IncomingHeaders = {
|
|
25
|
-
[key: string]: Buffer | string | (Buffer | string)[] | undefined;
|
|
38
|
+
[key: string]: Buffer | string | (Buffer | string)[] | null | undefined;
|
|
26
39
|
};
|
|
27
40
|
/** A record to produce (structural subset of a KafkaJS message). */
|
|
28
41
|
interface KafkaProducerMessage {
|
|
29
42
|
key?: Buffer | string | null;
|
|
30
43
|
value: Buffer | string;
|
|
44
|
+
/**
|
|
45
|
+
* Header values are strings for records the adapter builds. Raw DLQ forwarding (gate rejection,
|
|
46
|
+
* decode error, non-conformant envelope) instead passes the inbound values through as
|
|
47
|
+
* `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.
|
|
48
|
+
*/
|
|
31
49
|
headers?: {
|
|
32
50
|
[key: string]: string;
|
|
33
51
|
};
|
|
@@ -70,6 +88,13 @@ interface KafkaConsumerClient {
|
|
|
70
88
|
declare function kafkaHeaders(envelope: Envelope): {
|
|
71
89
|
[key: string]: string;
|
|
72
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer
|
|
93
|
+
* values. Returns an empty object when there are none. Both the contract `bq-*` headers and any
|
|
94
|
+
* out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it
|
|
95
|
+
* knows.
|
|
96
|
+
*/
|
|
97
|
+
declare function headersOf(message: KafkaIncomingMessage): HeaderCarrier;
|
|
73
98
|
/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */
|
|
74
99
|
interface Tier {
|
|
75
100
|
topic: string;
|
|
@@ -97,6 +122,12 @@ interface PublishOptions {
|
|
|
97
122
|
traceId?: string;
|
|
98
123
|
/** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */
|
|
99
124
|
delayMs?: number;
|
|
125
|
+
/**
|
|
126
|
+
* Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`
|
|
127
|
+
* headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
|
|
128
|
+
* The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.
|
|
129
|
+
*/
|
|
130
|
+
headers?: HeaderCarrier;
|
|
100
131
|
}
|
|
101
132
|
/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */
|
|
102
133
|
declare class KafkaPublisher {
|
|
@@ -112,8 +143,13 @@ declare class KafkaPublisher {
|
|
|
112
143
|
publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
|
|
113
144
|
private sendRecord;
|
|
114
145
|
}
|
|
115
|
-
/**
|
|
116
|
-
|
|
146
|
+
/**
|
|
147
|
+
* A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band
|
|
148
|
+
* {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to
|
|
149
|
+
* `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span
|
|
150
|
+
* (ADR-0028).
|
|
151
|
+
*/
|
|
152
|
+
type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
|
|
117
153
|
/** A map of URN → handler. */
|
|
118
154
|
type BabelHandlers = Record<string, BabelHandler>;
|
|
119
155
|
/** Options for {@link KafkaConsumer}. */
|
|
@@ -160,4 +196,4 @@ declare class KafkaConsumer {
|
|
|
160
196
|
private report;
|
|
161
197
|
}
|
|
162
198
|
|
|
163
|
-
export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, type PublishOptions, RetryTopics, type Tier, kafkaHeaders };
|
|
199
|
+
export { type BabelHandler, type BabelHandlers, type EachMessagePayload, type IncomingHeaders, KafkaConsumer, type KafkaConsumerClient, type KafkaConsumerOptions, type KafkaIncomingMessage, type KafkaProducerClient, type KafkaProducerMessage, KafkaPublisher, type PublishOptions, RetryTopics, type Tier, headersOf, kafkaHeaders };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
|
|
2
|
+
import { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
|
|
3
|
+
|
|
4
|
+
// src/headers.ts
|
|
5
|
+
function sanitizeHeaders(headers) {
|
|
6
|
+
const out = {};
|
|
7
|
+
if (!headers) return out;
|
|
8
|
+
for (const key of Object.keys(headers)) {
|
|
9
|
+
const value = headers[key];
|
|
10
|
+
if (key === "" || value == null || value === "") continue;
|
|
11
|
+
out[key] = value;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
function mergeInto(base, extra) {
|
|
16
|
+
for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {
|
|
17
|
+
if (key in base) continue;
|
|
18
|
+
base[key] = value;
|
|
19
|
+
}
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/index.ts
|
|
3
24
|
function kafkaHeaders(envelope) {
|
|
4
25
|
const headers = {};
|
|
5
26
|
if (envelope.job) headers["bq-job"] = envelope.job;
|
|
@@ -10,11 +31,25 @@ function kafkaHeaders(envelope) {
|
|
|
10
31
|
headers["bq-attempts"] = String(envelope.attempts ?? 0);
|
|
11
32
|
return headers;
|
|
12
33
|
}
|
|
34
|
+
function copyHeaders(headers) {
|
|
35
|
+
const out = {};
|
|
36
|
+
if (!headers) return out;
|
|
37
|
+
const copy = (v) => Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from(v, "utf8");
|
|
38
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
39
|
+
if (value === void 0 || value === null) continue;
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
out[name] = value.filter((v) => v !== void 0 && v !== null).map(copy);
|
|
42
|
+
} else {
|
|
43
|
+
out[name] = copy(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
13
48
|
function headerString(headers, key) {
|
|
14
49
|
const value = headers?.[key];
|
|
15
|
-
if (value === void 0) return void 0;
|
|
50
|
+
if (value === void 0 || value === null) return void 0;
|
|
16
51
|
const v = Array.isArray(value) ? value[0] : value;
|
|
17
|
-
if (v === void 0) return void 0;
|
|
52
|
+
if (v === void 0 || v === null) return void 0;
|
|
18
53
|
return Buffer.isBuffer(v) ? v.toString("utf8") : String(v);
|
|
19
54
|
}
|
|
20
55
|
function headerInt(headers, key, fallback) {
|
|
@@ -23,6 +58,22 @@ function headerInt(headers, key, fallback) {
|
|
|
23
58
|
const n = Number.parseInt(s.trim(), 10);
|
|
24
59
|
return Number.isNaN(n) ? fallback : n;
|
|
25
60
|
}
|
|
61
|
+
var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
|
|
62
|
+
function unsupportedSchemaVersion(message) {
|
|
63
|
+
const value = headerString(message.headers, "bq-schema-version");
|
|
64
|
+
if (value === void 0 || BLANK_ASCII.test(value)) return void 0;
|
|
65
|
+
return value === String(SCHEMA_VERSION) ? void 0 : value;
|
|
66
|
+
}
|
|
67
|
+
function headersOf(message) {
|
|
68
|
+
const headers = message.headers;
|
|
69
|
+
const out = {};
|
|
70
|
+
if (!headers) return out;
|
|
71
|
+
for (const key of Object.keys(headers)) {
|
|
72
|
+
const value = headerString(headers, key);
|
|
73
|
+
if (value !== void 0 && value !== "") out[key] = value;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
26
77
|
var RetryTopics = class {
|
|
27
78
|
constructor(workTopic, delaysMs = [], dlqTopic = void 0) {
|
|
28
79
|
this.workTopic = workTopic;
|
|
@@ -81,14 +132,14 @@ var KafkaPublisher = class _KafkaPublisher {
|
|
|
81
132
|
throw new BabelQueueError("Kafka has no native delayed delivery; a delay requires retry topics (none configured).");
|
|
82
133
|
}
|
|
83
134
|
const tier = this.retryTopics.tierForDelay(options.delayMs);
|
|
84
|
-
await this.sendRecord(tier.topic, envelope, options.delayMs, this.workTopic);
|
|
135
|
+
await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);
|
|
85
136
|
} else {
|
|
86
|
-
await this.sendRecord(this.workTopic, envelope);
|
|
137
|
+
await this.sendRecord(this.workTopic, envelope, options.headers);
|
|
87
138
|
}
|
|
88
139
|
return envelope.meta.id;
|
|
89
140
|
}
|
|
90
|
-
async sendRecord(topic, envelope, delayMs, originalTopic) {
|
|
91
|
-
const headers = kafkaHeaders(envelope);
|
|
141
|
+
async sendRecord(topic, envelope, extra, delayMs, originalTopic) {
|
|
142
|
+
const headers = mergeInto(kafkaHeaders(envelope), extra);
|
|
92
143
|
if (delayMs != null) headers["bq-delay"] = String(delayMs);
|
|
93
144
|
if (originalTopic != null) headers["bq-original-topic"] = originalTopic;
|
|
94
145
|
await this.producer.send({
|
|
@@ -117,6 +168,17 @@ var KafkaConsumer = class {
|
|
|
117
168
|
/** Route + settle one record (process-then-commit). Exposed for testing. */
|
|
118
169
|
async handle(payload) {
|
|
119
170
|
const { message } = payload;
|
|
171
|
+
const unsupported = unsupportedSchemaVersion(message);
|
|
172
|
+
if (unsupported !== void 0) {
|
|
173
|
+
this.report(
|
|
174
|
+
new BabelQueueError(`Rejected a record from Kafka with unsupported bq-schema-version "${unsupported}" (supported: ${SCHEMA_VERSION}).`),
|
|
175
|
+
null,
|
|
176
|
+
payload
|
|
177
|
+
);
|
|
178
|
+
await this.deadLetterRaw(payload);
|
|
179
|
+
await this.commit(payload);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
120
182
|
const raw = message.value ? message.value.toString("utf8") : "";
|
|
121
183
|
let envelope;
|
|
122
184
|
try {
|
|
@@ -140,7 +202,7 @@ var KafkaConsumer = class {
|
|
|
140
202
|
return;
|
|
141
203
|
}
|
|
142
204
|
try {
|
|
143
|
-
await handler(envelope, message);
|
|
205
|
+
await handler(envelope, message, headersOf(message));
|
|
144
206
|
await this.commit(payload);
|
|
145
207
|
} catch (error) {
|
|
146
208
|
this.report(error, envelope, payload);
|
|
@@ -215,10 +277,14 @@ var KafkaConsumer = class {
|
|
|
215
277
|
if (!dlq) return;
|
|
216
278
|
this.requireProducer();
|
|
217
279
|
const { message } = payload;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
280
|
+
const forwarded = {
|
|
281
|
+
value: message.value ?? Buffer.alloc(0),
|
|
282
|
+
// KafkaJS accepts Buffer / array header values; the published structural type stays string-only.
|
|
283
|
+
headers: copyHeaders(message.headers),
|
|
284
|
+
timestamp: String(Date.now())
|
|
285
|
+
};
|
|
286
|
+
if (message.key !== void 0 && message.key !== null) forwarded.key = Buffer.from(message.key);
|
|
287
|
+
await this.options.producer.send({ topic: dlq, messages: [forwarded] });
|
|
222
288
|
}
|
|
223
289
|
async produce(topic, envelope, headers) {
|
|
224
290
|
this.requireProducer();
|
|
@@ -248,6 +314,7 @@ export {
|
|
|
248
314
|
KafkaConsumer,
|
|
249
315
|
KafkaPublisher,
|
|
250
316
|
RetryTopics,
|
|
317
|
+
headersOf,
|
|
251
318
|
kafkaHeaders
|
|
252
319
|
};
|
|
253
320
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.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\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\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)[] | 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 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\nfunction headerString(headers: IncomingHeaders | undefined, key: string): string | undefined {\n const value = headers?.[key];\n if (value === undefined) return undefined;\n const v = Array.isArray(value) ? value[0] : value;\n if (v === undefined) return undefined;\n return Buffer.isBuffer(v) ? v.toString(\"utf8\") : String(v);\n}\n\nfunction headerInt(headers: IncomingHeaders | undefined, key: string, fallback: number): number {\n const s = headerString(headers, key);\n if (s === undefined) return fallback;\n const n = Number.parseInt(s.trim(), 10);\n return Number.isNaN(n) ? fallback : n;\n}\n\n// --- 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\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.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(topic: string, envelope: Envelope, delayMs?: number, originalTopic?: string): Promise<void> {\n const headers = kafkaHeaders(envelope);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw Kafka message. */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage) => 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 const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n this.report(decodeError, null, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // A non-conformant / poison envelope may be missing the fields needed to annotate, so\n // forward the raw record to the DLQ rather than building a dead_letter block.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Kafka.\"), envelope, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const urn = headerString(message.headers, \"bq-job\") ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(payload, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message);\n await this.commit(payload);\n } catch (error) {\n this.report(error, envelope, payload);\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError before this; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers);\n }\n\n private async deadLetterRaw(payload: EachMessagePayload): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return;\n this.requireProducer();\n const { message } = payload;\n await this.options.producer!.send({\n topic: dlq,\n messages: [{ value: message.value ?? Buffer.alloc(0), timestamp: String(Date.now()) }],\n });\n }\n\n private async produce(topic: string, envelope: Envelope, headers: { [key: string]: string }): Promise<void> {\n this.requireProducer();\n await this.options.producer!.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }],\n });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n this.options.onError?.(error, envelope, payload);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache Kafka adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed, **process-then-commit** consumer over\n * Apache Kafka (KafkaJS), so a Kafka-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * Kafka has **no native** per-message ack, delayed delivery, dead-letter queue, or delivery\n * counter — this adapter absorbs all four in the binding layer (the envelope stays\n * `schema_version: 1`): the envelope JSON is the record **value**; the contract fields are\n * mirrored onto `bq-` headers (route on `bq-job` without decoding the body) and the record\n * timestamp mirrors `meta.created_at`; **`bq-attempts` is the authoritative retry counter**;\n * consume is process-then-commit (manual commit, at-least-once); retry/delay use SDK-owned\n * tiered retry topics `<topic>.retry.<n>`; terminal failures go to an opt-in `<topic>.dlq`.\n *\n * This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you\n * provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter\n * structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional Kafka **record headers** beside the contract `bq-*` headers —\n * the contract headers win a key collision (merge-not-clobber), and blanks are dropped. On consume\n * the inbound record headers are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope (the record value) is never touched.\n *\n * **Version gate (§6.7).** Before decoding the record value the consumer reads the `bq-schema-version`\n * header. A missing or blank value (empty or only ASCII whitespace) follows the normal decode path;\n * any other value is compared, untrimmed, to the supported `\"1\"`, and a record that does not match is\n * rejected undecoded down the poison path — reported to `onError`, the raw record (value, key and\n * headers) forwarded to the DLQ (when configured) and the offset committed.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Kafka shapes (a structural subset of kafkajs) ---------------------\n\n/** Incoming record headers (KafkaJS decodes header values to Buffers). */\nexport type IncomingHeaders = { [key: string]: Buffer | string | (Buffer | string)[] | null | undefined };\n\n/** A record to produce (structural subset of a KafkaJS message). */\nexport interface KafkaProducerMessage {\n key?: Buffer | string | null;\n value: Buffer | string;\n /**\n * Header values are strings for records the adapter builds. Raw DLQ forwarding (gate rejection,\n * decode error, non-conformant envelope) instead passes the inbound values through as\n * `Buffer | Buffer[]` (KafkaJS `IHeaders`); a custom producer wrapper must tolerate both.\n */\n headers?: { [key: string]: string };\n timestamp?: string;\n}\n\n/** The subset of the KafkaJS producer this adapter calls. */\nexport interface KafkaProducerClient {\n send(record: { topic: string; messages: KafkaProducerMessage[] }): Promise<unknown>;\n}\n\n/** A received record (structural subset of a KafkaJS message). */\nexport interface KafkaIncomingMessage {\n value: Buffer | null;\n headers?: IncomingHeaders;\n offset: string;\n timestamp?: string;\n key?: Buffer | null;\n}\n\n/** The payload KafkaJS passes to `eachMessage`. */\nexport interface EachMessagePayload {\n topic: string;\n partition: number;\n message: KafkaIncomingMessage;\n}\n\n/** The subset of the KafkaJS consumer this adapter calls (manual commit — `autoCommit: false`). */\nexport interface KafkaConsumerClient {\n run(config: { autoCommit?: boolean; eachMessage: (payload: EachMessagePayload) => Promise<void> }): Promise<void>;\n commitOffsets(offsets: { topic: string; partition: number; offset: string }[]): Promise<void>;\n}\n\n// --- Header projection (contract §6.3) -----------------------------------------\n\n/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */\nexport function kafkaHeaders(envelope: Envelope): { [key: string]: string } {\n const headers: { [key: string]: string } = {};\n if (envelope.job) headers[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) headers[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) headers[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) headers[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n if (envelope.meta.lang) headers[\"bq-source-lang\"] = envelope.meta.lang;\n headers[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return headers;\n}\n\n/**\n * Copy inbound record headers for a raw DLQ forward: every value becomes a `Buffer` copy (a string is\n * UTF-8 encoded), array-valued headers keep all their entries, and `undefined` / `null` values are dropped.\n */\nfunction copyHeaders(headers: IncomingHeaders | undefined): { [key: string]: Buffer | Buffer[] } {\n const out: { [key: string]: Buffer | Buffer[] } = {};\n if (!headers) return out;\n const copy = (v: Buffer | string): Buffer => (Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from(v, \"utf8\"));\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n out[name] = value.filter((v) => v !== undefined && v !== null).map(copy);\n } else {\n out[name] = copy(value);\n }\n }\n return out;\n}\n\nfunction headerString(headers: IncomingHeaders | undefined, key: string): string | undefined {\n const value = headers?.[key];\n // A null header value is \"no header\" (broker-bindings.md §6.7): never stringified to \"null\".\n if (value === undefined || value === null) return undefined;\n const v = Array.isArray(value) ? value[0] : value;\n if (v === undefined || v === null) return undefined;\n return Buffer.isBuffer(v) ? v.toString(\"utf8\") : String(v);\n}\n\nfunction headerInt(headers: IncomingHeaders | undefined, key: string, fallback: number): number {\n const s = headerString(headers, key);\n if (s === undefined) return fallback;\n const n = Number.parseInt(s.trim(), 10);\n return Number.isNaN(n) ? fallback : n;\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq-schema-version` header when it is present, non-blank and not exactly the supported\n * version (compared untrimmed), else `undefined` (the record may be decoded).\n */\nfunction unsupportedSchemaVersion(message: KafkaIncomingMessage): string | undefined {\n const value = headerString(message.headers, \"bq-schema-version\");\n if (value === undefined || BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\n/**\n * Map an inbound record's headers onto a flat {@link HeaderCarrier}, stringifying KafkaJS's Buffer\n * values. Returns an empty object when there are none. Both the contract `bq-*` headers and any\n * out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract reads only the keys it\n * knows.\n */\nexport function headersOf(message: KafkaIncomingMessage): HeaderCarrier {\n const headers = message.headers;\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headerString(headers, key);\n if (value !== undefined && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Retry / delay topology (contract §6.4–§6.5) -------------------------------\n\n/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */\nexport interface Tier {\n topic: string;\n delayMs: number;\n}\n\n/**\n * The SDK-owned retry/delay topology for one work topic: tiered delay topics\n * `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no\n * tiers configured raises rather than silently dropping.\n */\nexport class RetryTopics {\n readonly tiers: Tier[];\n readonly dlqTopic: string | null;\n\n constructor(\n readonly workTopic: string,\n delaysMs: number[] = [],\n dlqTopic: string | null | undefined = undefined,\n ) {\n this.tiers = [...delaysMs]\n .sort((a, b) => a - b)\n .map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));\n this.dlqTopic = dlqTopic === undefined ? `${workTopic}.dlq` : dlqTopic;\n }\n\n hasTiers(): boolean {\n return this.tiers.length > 0;\n }\n\n /** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */\n tierForDelay(delayMs: number): Tier {\n this.requireTiers();\n for (const tier of this.tiers) {\n if (tier.delayMs >= delayMs) return tier;\n }\n throw new BabelQueueError(\n `Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1]!.delayMs}ms).`,\n );\n }\n\n /** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */\n tierForAttempt(attempt: number): Tier {\n this.requireTiers();\n return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)]!;\n }\n\n private requireTiers(): void {\n if (!this.tiers.length) {\n throw new BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);\n }\n }\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link KafkaPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as Kafka record headers beside the contract `bq-*`\n * headers (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract headers win a key collision; an empty/omitted carrier leaves the record unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */\nexport class KafkaPublisher {\n private constructor(\n private readonly producer: KafkaProducerClient,\n private readonly workTopic: string,\n private readonly retryTopics?: RetryTopics,\n ) {}\n\n /** A publisher onto `topic` with no retry topics (a delay raises). */\n static create(producer: KafkaProducerClient, topic: string): KafkaPublisher {\n return new KafkaPublisher(producer, topic);\n }\n\n /** A publisher onto the topology's work topic, with delay routed via its retry tiers. */\n static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher {\n return new KafkaPublisher(producer, retryTopics.workTopic, retryTopics);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });\n if (options.delayMs != null && options.delayMs > 0) {\n if (!this.retryTopics) {\n throw new BabelQueueError(\"Kafka has no native delayed delivery; a delay requires retry topics (none configured).\");\n }\n const tier = this.retryTopics.tierForDelay(options.delayMs);\n await this.sendRecord(tier.topic, envelope, options.headers, options.delayMs, this.workTopic);\n } else {\n await this.sendRecord(this.workTopic, envelope, options.headers);\n }\n return envelope.meta.id;\n }\n\n private async sendRecord(\n topic: string,\n envelope: Envelope,\n extra?: HeaderCarrier | null,\n delayMs?: number,\n originalTopic?: string,\n ): Promise<void> {\n // The contract bq-* headers are seeded first, then the out-of-band riders are merged beside\n // them without clobbering (the contract wins a collision).\n const headers = mergeInto(kafkaHeaders(envelope), extra);\n if (delayMs != null) headers[\"bq-delay\"] = String(delayMs);\n if (originalTopic != null) headers[\"bq-original-topic\"] = originalTopic;\n await this.producer.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }],\n });\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Kafka message, and the out-of-band\n * {@link HeaderCarrier} read from the record headers (empty when there are none). Pass `headers` to\n * `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the producer span\n * (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link KafkaConsumer}. */\nexport interface KafkaConsumerOptions {\n /** The producer used to republish retry/DLQ records (required for retry/DLQ). */\n producer?: KafkaProducerClient;\n /** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */\n retryTopics?: RetryTopics;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a poison record, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;\n}\n\n/**\n * Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,\n * routed to the handler for its URN (read from `bq-job`), and its offset committed only after\n * the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with\n * `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`\n * block. The `bq-attempts` header is the authoritative counter (the body is the fallback).\n */\nexport class KafkaConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly consumer: KafkaConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: KafkaConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */\n async run(): Promise<void> {\n await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });\n }\n\n /** Route + settle one record (process-then-commit). Exposed for testing. */\n async handle(payload: EachMessagePayload): Promise<void> {\n const { message } = payload;\n\n // Version gate (§6.7): decided on the header alone, before the value is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n this.report(\n new BabelQueueError(`Rejected a record from Kafka with unsupported bq-schema-version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`),\n null,\n payload,\n );\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const raw = message.value ? message.value.toString(\"utf8\") : \"\";\n\n let envelope: IncomingEnvelope;\n try {\n envelope = this.reconcile(EnvelopeCodec.decode(raw), message);\n } catch (decodeError) {\n this.report(decodeError, null, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n // A non-conformant / poison envelope may be missing the fields needed to annotate, so\n // forward the raw record to the DLQ rather than building a dead_letter block.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Kafka.\"), envelope, payload);\n await this.deadLetterRaw(payload);\n await this.commit(payload);\n return;\n }\n\n const urn = headerString(message.headers, \"bq-job\") ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(payload, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n await this.commit(payload);\n } catch (error) {\n this.report(error, envelope, payload);\n await this.retryOrDeadLetter(payload, envelope as Envelope, error);\n await this.commit(payload);\n }\n }\n\n private reconcile(envelope: IncomingEnvelope, message: KafkaIncomingMessage): IncomingEnvelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n return { ...envelope, attempts: headerInt(message.headers, \"bq-attempts\", body) };\n }\n\n private async onUnknownUrn(payload: EachMessagePayload, envelope: IncomingEnvelope, urn: string): Promise<void> {\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope as Envelope, payload, \"unknown_urn\");\n await this.commit(payload);\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.republishRetry(payload, envelope as Envelope);\n await this.commit(payload);\n return;\n default: {\n // FAIL: surface and do NOT commit — the record redelivers on the next poll.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, payload);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(payload: EachMessagePayload, envelope: Envelope, error: unknown): Promise<void> {\n const hasTiers = this.options.retryTopics?.hasTiers() ?? false;\n const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;\n if (!hasTiers && !hasDlq) {\n void error; // reported via onError before this; Kafka has nowhere to put the failure\n throw new BabelQueueError(\"Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.\");\n }\n if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {\n await this.republishRetry(payload, envelope);\n } else {\n await this.deadLetter(envelope, payload, \"failed\", error);\n }\n }\n\n private async republishRetry(payload: EachMessagePayload, envelope: Envelope): Promise<void> {\n const topics = this.options.retryTopics;\n if (!topics || !topics.hasTiers()) {\n throw new BabelQueueError(\"Kafka retry/release requires retry topics; none are configured.\");\n }\n const tier = topics.tierForAttempt(envelope.attempts ?? 0);\n const bumped: Envelope = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };\n const headers = kafkaHeaders(bumped);\n headers[\"bq-delay\"] = String(tier.delayMs);\n headers[\"bq-original-topic\"] = this.originalTopic(payload);\n await this.produce(tier.topic, bumped, headers);\n }\n\n private async deadLetter(envelope: Envelope, payload: EachMessagePayload, reason: string, error?: unknown): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return; // dead-lettering disabled → degrade to commit-and-drop\n const original = this.originalTopic(payload);\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n const headers = kafkaHeaders(annotated);\n headers[\"bq-original-topic\"] = original;\n await this.produce(dlq, annotated, headers);\n }\n\n private async deadLetterRaw(payload: EachMessagePayload): Promise<void> {\n const dlq = this.options.retryTopics?.dlqTopic ?? null;\n if (!dlq) return;\n this.requireProducer();\n const { message } = payload;\n // Forward the original record verbatim (value, key, headers) so the DLQ copy keeps its provenance\n // (Java reference; the .NET consumer forwards value + headers). Only the timestamp is re-stamped.\n const forwarded: KafkaProducerMessage = {\n value: message.value ?? Buffer.alloc(0),\n // KafkaJS accepts Buffer / array header values; the published structural type stays string-only.\n headers: copyHeaders(message.headers) as unknown as KafkaProducerMessage[\"headers\"],\n timestamp: String(Date.now()),\n };\n if (message.key !== undefined && message.key !== null) forwarded.key = Buffer.from(message.key);\n await this.options.producer!.send({ topic: dlq, messages: [forwarded] });\n }\n\n private async produce(topic: string, envelope: Envelope, headers: { [key: string]: string }): Promise<void> {\n this.requireProducer();\n await this.options.producer!.send({\n topic,\n messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }],\n });\n }\n\n private async commit(payload: EachMessagePayload): Promise<void> {\n await this.consumer.commitOffsets([\n { topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() },\n ]);\n }\n\n private originalTopic(payload: EachMessagePayload): string {\n return headerString(payload.message.headers, \"bq-original-topic\") ?? payload.topic;\n }\n\n private requireProducer(): void {\n if (!this.options.producer) {\n throw new BabelQueueError(\"This Kafka consumer needs a producer to republish (retry/DLQ).\");\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload): void {\n this.options.onError?.(error, envelope, payload);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Merge out-of-band `extra` headers onto an already-built `base` header map **without clobbering**\n * the contract headers in `base` (they win a key collision) and dropping blanks. The result is\n * `base`, mutated in place.\n */\nexport function mergeInto(base: { [key: string]: string }, extra: HeaderCarrier | null | undefined): { [key: string]: string } {\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {\n if (key in base) continue; // the contract header already there wins\n base[key] = value;\n }\n return base;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@babelqueue/kafka",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Apache Kafka adapter for BabelQueue — canonical-envelope publisher and a URN-routed, process-then-commit consumer (SDK-owned retry topics + DLQ).",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"babelqueue",
|
|
7
|
+
"kafka",
|
|
8
|
+
"apache-kafka",
|
|
9
|
+
"kafkajs",
|
|
10
|
+
"queue",
|
|
11
|
+
"polyglot",
|
|
12
|
+
"messaging"
|
|
13
|
+
],
|
|
6
14
|
"homepage": "https://babelqueue.com",
|
|
7
15
|
"bugs": {
|
|
8
16
|
"url": "https://github.com/BabelQueue/babelqueue-node-adapters/issues"
|
|
@@ -13,7 +21,7 @@
|
|
|
13
21
|
"directory": "packages/kafka"
|
|
14
22
|
},
|
|
15
23
|
"license": "MIT",
|
|
16
|
-
"author": "Muhammet Şafak <info@muhammetsafak.com
|
|
24
|
+
"author": "Muhammet Şafak <info@muhammetsafak.com>",
|
|
17
25
|
"type": "module",
|
|
18
26
|
"exports": {
|
|
19
27
|
".": {
|
|
@@ -30,7 +38,9 @@
|
|
|
30
38
|
"main": "./dist/index.cjs",
|
|
31
39
|
"module": "./dist/index.js",
|
|
32
40
|
"types": "./dist/index.d.ts",
|
|
33
|
-
"files": [
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
34
44
|
"sideEffects": false,
|
|
35
45
|
"engines": {
|
|
36
46
|
"node": ">=18"
|
|
@@ -41,7 +51,7 @@
|
|
|
41
51
|
"test": "node --import tsx --test test/*.test.ts"
|
|
42
52
|
},
|
|
43
53
|
"dependencies": {
|
|
44
|
-
"@babelqueue/core": "^1.
|
|
54
|
+
"@babelqueue/core": "^1.4.0"
|
|
45
55
|
},
|
|
46
56
|
"peerDependencies": {
|
|
47
57
|
"kafkajs": "^2.0.0"
|