@babelqueue/kafka 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/dist/index.cjs +281 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +163 -0
- package/dist/index.d.ts +163 -0
- package/dist/index.js +253 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @babelqueue/kafka
|
|
2
|
+
|
|
3
|
+
Apache Kafka adapter for [BabelQueue](https://babelqueue.com) — a canonical-envelope
|
|
4
|
+
**publisher** and a URN-routed, **process-then-commit** consumer over
|
|
5
|
+
[KafkaJS](https://kafka.js.org), on `@babelqueue/core`. Implements
|
|
6
|
+
[§6 of the broker-bindings contract](https://babelqueue.com/docs/spec/1.x/broker-bindings#apache-kafka),
|
|
7
|
+
so a Kafka-based Node service speaks the same wire contract as the .NET, Java, Python and Go
|
|
8
|
+
SDKs.
|
|
9
|
+
|
|
10
|
+
Kafka has **no native** per-message ack, delayed delivery, dead-letter queue, or delivery
|
|
11
|
+
counter — this adapter absorbs all four in the binding layer (the envelope stays
|
|
12
|
+
`schema_version: 1`): the record **value** is the envelope JSON, the contract fields are
|
|
13
|
+
mirrored onto `bq-` headers (route on `bq-job` without decoding the body), the record
|
|
14
|
+
timestamp mirrors `meta.created_at`, **`bq-attempts` is the authoritative retry counter**,
|
|
15
|
+
consume is process-then-commit (manual commit), retry/delay use SDK-owned tiered retry topics
|
|
16
|
+
`<topic>.retry.<n>`, and terminal failures go to an opt-in `<topic>.dlq`.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm i @babelqueue/kafka kafkajs
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`kafkajs` is an optional peer — **you provide the producer/consumer** (a KafkaJS `Producer` /
|
|
25
|
+
`Consumer` satisfies the adapter structurally).
|
|
26
|
+
|
|
27
|
+
## Use
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { Kafka } from "kafkajs";
|
|
31
|
+
import { KafkaPublisher, KafkaConsumer, RetryTopics } from "@babelqueue/kafka";
|
|
32
|
+
|
|
33
|
+
const kafka = new Kafka({ brokers: ["localhost:9092"] });
|
|
34
|
+
|
|
35
|
+
// produce
|
|
36
|
+
const producer = kafka.producer();
|
|
37
|
+
await producer.connect();
|
|
38
|
+
const id = await KafkaPublisher.create(producer, "orders")
|
|
39
|
+
.publish("urn:babel:orders:created", { order_id: 1042 });
|
|
40
|
+
|
|
41
|
+
// consume (manual commit, process-then-commit)
|
|
42
|
+
const consumer = kafka.consumer({ groupId: "orders-workers" });
|
|
43
|
+
await consumer.connect();
|
|
44
|
+
await consumer.subscribe({ topic: "orders" });
|
|
45
|
+
|
|
46
|
+
const retry = new RetryTopics("orders", [5_000, 60_000]); // orders.retry.1/.2 + orders.dlq
|
|
47
|
+
const babel = new KafkaConsumer(
|
|
48
|
+
consumer,
|
|
49
|
+
{
|
|
50
|
+
"urn:babel:orders:created": async (env, message) => {
|
|
51
|
+
console.log(env.data.order_id, env.trace_id, env.attempts);
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{ producer, retryTopics: retry, maxTries: 3, onError: (err) => console.error(err) },
|
|
55
|
+
);
|
|
56
|
+
await babel.run(); // poll → process → commit (autoCommit: false)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Delayed delivery: `publish(urn, data, { delayMs: 300_000 })` routes to the matching retry
|
|
60
|
+
tier (requires `KafkaPublisher.withRetryTopics`); on a plain publisher a delay throws. A
|
|
61
|
+
throwing handler republishes to the next `<topic>.retry.<n>` tier with `bq-attempts + 1`, then
|
|
62
|
+
commits; once `maxTries` is reached it goes to `<topic>.dlq` with a `dead_letter` block.
|
|
63
|
+
|
|
64
|
+
## Contract mapping (§6)
|
|
65
|
+
|
|
66
|
+
| Envelope | Apache Kafka |
|
|
67
|
+
| :--- | :--- |
|
|
68
|
+
| body | record `value` (byte-identical across SDKs) |
|
|
69
|
+
| `job` (URN) | header `bq-job` (consumer routes on this) |
|
|
70
|
+
| `trace_id` | header `bq-trace-id` |
|
|
71
|
+
| `meta.id` | header `bq-message-id` |
|
|
72
|
+
| `meta.schema_version` | header `bq-schema-version` (`"1"`) |
|
|
73
|
+
| `meta.lang` | header `bq-source-lang` |
|
|
74
|
+
| `meta.created_at` | record `timestamp` (Unix ms) |
|
|
75
|
+
| `attempts` | header `bq-attempts` (**authoritative**; body is the fallback) |
|
|
76
|
+
| reserve / ack | poll → process → **commit offset** (manual) |
|
|
77
|
+
| retry / delay | republish to `<topic>.retry.<n>` (`bq-attempts + 1`) |
|
|
78
|
+
| dead-letter | `<topic>.dlq` + `dead_letter` block |
|
|
79
|
+
|
|
80
|
+
All header values are UTF-8 strings (integers as decimal strings, e.g. `"1"`). The
|
|
81
|
+
producer/consumer are injected, so the unit tests use fakes — no Kafka, no broker. Dual
|
|
82
|
+
ESM + CJS.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
KafkaConsumer: () => KafkaConsumer,
|
|
24
|
+
KafkaPublisher: () => KafkaPublisher,
|
|
25
|
+
RetryTopics: () => RetryTopics,
|
|
26
|
+
kafkaHeaders: () => kafkaHeaders
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var import_core = require("@babelqueue/core");
|
|
30
|
+
function kafkaHeaders(envelope) {
|
|
31
|
+
const headers = {};
|
|
32
|
+
if (envelope.job) headers["bq-job"] = envelope.job;
|
|
33
|
+
if (envelope.trace_id) headers["bq-trace-id"] = envelope.trace_id;
|
|
34
|
+
if (envelope.meta.id) headers["bq-message-id"] = envelope.meta.id;
|
|
35
|
+
if (envelope.meta.schema_version != null) headers["bq-schema-version"] = String(envelope.meta.schema_version);
|
|
36
|
+
if (envelope.meta.lang) headers["bq-source-lang"] = envelope.meta.lang;
|
|
37
|
+
headers["bq-attempts"] = String(envelope.attempts ?? 0);
|
|
38
|
+
return headers;
|
|
39
|
+
}
|
|
40
|
+
function headerString(headers, key) {
|
|
41
|
+
const value = headers?.[key];
|
|
42
|
+
if (value === void 0) return void 0;
|
|
43
|
+
const v = Array.isArray(value) ? value[0] : value;
|
|
44
|
+
if (v === void 0) return void 0;
|
|
45
|
+
return Buffer.isBuffer(v) ? v.toString("utf8") : String(v);
|
|
46
|
+
}
|
|
47
|
+
function headerInt(headers, key, fallback) {
|
|
48
|
+
const s = headerString(headers, key);
|
|
49
|
+
if (s === void 0) return fallback;
|
|
50
|
+
const n = Number.parseInt(s.trim(), 10);
|
|
51
|
+
return Number.isNaN(n) ? fallback : n;
|
|
52
|
+
}
|
|
53
|
+
var RetryTopics = class {
|
|
54
|
+
constructor(workTopic, delaysMs = [], dlqTopic = void 0) {
|
|
55
|
+
this.workTopic = workTopic;
|
|
56
|
+
this.tiers = [...delaysMs].sort((a, b) => a - b).map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));
|
|
57
|
+
this.dlqTopic = dlqTopic === void 0 ? `${workTopic}.dlq` : dlqTopic;
|
|
58
|
+
}
|
|
59
|
+
workTopic;
|
|
60
|
+
tiers;
|
|
61
|
+
dlqTopic;
|
|
62
|
+
hasTiers() {
|
|
63
|
+
return this.tiers.length > 0;
|
|
64
|
+
}
|
|
65
|
+
/** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */
|
|
66
|
+
tierForDelay(delayMs) {
|
|
67
|
+
this.requireTiers();
|
|
68
|
+
for (const tier of this.tiers) {
|
|
69
|
+
if (tier.delayMs >= delayMs) return tier;
|
|
70
|
+
}
|
|
71
|
+
throw new import_core.BabelQueueError(
|
|
72
|
+
`Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1].delayMs}ms).`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
/** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */
|
|
76
|
+
tierForAttempt(attempt) {
|
|
77
|
+
this.requireTiers();
|
|
78
|
+
return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)];
|
|
79
|
+
}
|
|
80
|
+
requireTiers() {
|
|
81
|
+
if (!this.tiers.length) {
|
|
82
|
+
throw new import_core.BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var KafkaPublisher = class _KafkaPublisher {
|
|
87
|
+
constructor(producer, workTopic, retryTopics) {
|
|
88
|
+
this.producer = producer;
|
|
89
|
+
this.workTopic = workTopic;
|
|
90
|
+
this.retryTopics = retryTopics;
|
|
91
|
+
}
|
|
92
|
+
producer;
|
|
93
|
+
workTopic;
|
|
94
|
+
retryTopics;
|
|
95
|
+
/** A publisher onto `topic` with no retry topics (a delay raises). */
|
|
96
|
+
static create(producer, topic) {
|
|
97
|
+
return new _KafkaPublisher(producer, topic);
|
|
98
|
+
}
|
|
99
|
+
/** A publisher onto the topology's work topic, with delay routed via its retry tiers. */
|
|
100
|
+
static withRetryTopics(producer, retryTopics) {
|
|
101
|
+
return new _KafkaPublisher(producer, retryTopics.workTopic, retryTopics);
|
|
102
|
+
}
|
|
103
|
+
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
104
|
+
async publish(urn, data, options = {}) {
|
|
105
|
+
const envelope = import_core.EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });
|
|
106
|
+
if (options.delayMs != null && options.delayMs > 0) {
|
|
107
|
+
if (!this.retryTopics) {
|
|
108
|
+
throw new import_core.BabelQueueError("Kafka has no native delayed delivery; a delay requires retry topics (none configured).");
|
|
109
|
+
}
|
|
110
|
+
const tier = this.retryTopics.tierForDelay(options.delayMs);
|
|
111
|
+
await this.sendRecord(tier.topic, envelope, options.delayMs, this.workTopic);
|
|
112
|
+
} else {
|
|
113
|
+
await this.sendRecord(this.workTopic, envelope);
|
|
114
|
+
}
|
|
115
|
+
return envelope.meta.id;
|
|
116
|
+
}
|
|
117
|
+
async sendRecord(topic, envelope, delayMs, originalTopic) {
|
|
118
|
+
const headers = kafkaHeaders(envelope);
|
|
119
|
+
if (delayMs != null) headers["bq-delay"] = String(delayMs);
|
|
120
|
+
if (originalTopic != null) headers["bq-original-topic"] = originalTopic;
|
|
121
|
+
await this.producer.send({
|
|
122
|
+
topic,
|
|
123
|
+
messages: [{ value: import_core.EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }]
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var KafkaConsumer = class {
|
|
128
|
+
constructor(consumer, handlers, options = {}) {
|
|
129
|
+
this.consumer = consumer;
|
|
130
|
+
this.handlers = handlers;
|
|
131
|
+
this.options = options;
|
|
132
|
+
this.maxTries = options.maxTries ?? 3;
|
|
133
|
+
this.unknownUrn = options.unknownUrn ?? import_core.UnknownUrnStrategy.FAIL;
|
|
134
|
+
}
|
|
135
|
+
consumer;
|
|
136
|
+
handlers;
|
|
137
|
+
options;
|
|
138
|
+
maxTries;
|
|
139
|
+
unknownUrn;
|
|
140
|
+
/** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */
|
|
141
|
+
async run() {
|
|
142
|
+
await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });
|
|
143
|
+
}
|
|
144
|
+
/** Route + settle one record (process-then-commit). Exposed for testing. */
|
|
145
|
+
async handle(payload) {
|
|
146
|
+
const { message } = payload;
|
|
147
|
+
const raw = message.value ? message.value.toString("utf8") : "";
|
|
148
|
+
let envelope;
|
|
149
|
+
try {
|
|
150
|
+
envelope = this.reconcile(import_core.EnvelopeCodec.decode(raw), message);
|
|
151
|
+
} catch (decodeError) {
|
|
152
|
+
this.report(decodeError, null, payload);
|
|
153
|
+
await this.deadLetterRaw(payload);
|
|
154
|
+
await this.commit(payload);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!import_core.EnvelopeCodec.accepts(envelope)) {
|
|
158
|
+
this.report(new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Kafka."), envelope, payload);
|
|
159
|
+
await this.deadLetterRaw(payload);
|
|
160
|
+
await this.commit(payload);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const urn = headerString(message.headers, "bq-job") ?? import_core.EnvelopeCodec.urn(envelope);
|
|
164
|
+
const handler = this.handlers[urn];
|
|
165
|
+
if (!handler) {
|
|
166
|
+
await this.onUnknownUrn(payload, envelope, urn);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
await handler(envelope, message);
|
|
171
|
+
await this.commit(payload);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.report(error, envelope, payload);
|
|
174
|
+
await this.retryOrDeadLetter(payload, envelope, error);
|
|
175
|
+
await this.commit(payload);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
reconcile(envelope, message) {
|
|
179
|
+
const body = typeof envelope.attempts === "number" ? envelope.attempts : 0;
|
|
180
|
+
return { ...envelope, attempts: headerInt(message.headers, "bq-attempts", body) };
|
|
181
|
+
}
|
|
182
|
+
async onUnknownUrn(payload, envelope, urn) {
|
|
183
|
+
switch (this.unknownUrn) {
|
|
184
|
+
case import_core.UnknownUrnStrategy.DELETE:
|
|
185
|
+
await this.commit(payload);
|
|
186
|
+
return;
|
|
187
|
+
case import_core.UnknownUrnStrategy.DEAD_LETTER:
|
|
188
|
+
await this.deadLetter(envelope, payload, "unknown_urn");
|
|
189
|
+
await this.commit(payload);
|
|
190
|
+
return;
|
|
191
|
+
case import_core.UnknownUrnStrategy.RELEASE:
|
|
192
|
+
await this.republishRetry(payload, envelope);
|
|
193
|
+
await this.commit(payload);
|
|
194
|
+
return;
|
|
195
|
+
default: {
|
|
196
|
+
const error = new import_core.UnknownUrnError(urn);
|
|
197
|
+
this.report(error, envelope, payload);
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async retryOrDeadLetter(payload, envelope, error) {
|
|
203
|
+
const hasTiers = this.options.retryTopics?.hasTiers() ?? false;
|
|
204
|
+
const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;
|
|
205
|
+
if (!hasTiers && !hasDlq) {
|
|
206
|
+
void error;
|
|
207
|
+
throw new import_core.BabelQueueError("Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.");
|
|
208
|
+
}
|
|
209
|
+
if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {
|
|
210
|
+
await this.republishRetry(payload, envelope);
|
|
211
|
+
} else {
|
|
212
|
+
await this.deadLetter(envelope, payload, "failed", error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async republishRetry(payload, envelope) {
|
|
216
|
+
const topics = this.options.retryTopics;
|
|
217
|
+
if (!topics || !topics.hasTiers()) {
|
|
218
|
+
throw new import_core.BabelQueueError("Kafka retry/release requires retry topics; none are configured.");
|
|
219
|
+
}
|
|
220
|
+
const tier = topics.tierForAttempt(envelope.attempts ?? 0);
|
|
221
|
+
const bumped = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };
|
|
222
|
+
const headers = kafkaHeaders(bumped);
|
|
223
|
+
headers["bq-delay"] = String(tier.delayMs);
|
|
224
|
+
headers["bq-original-topic"] = this.originalTopic(payload);
|
|
225
|
+
await this.produce(tier.topic, bumped, headers);
|
|
226
|
+
}
|
|
227
|
+
async deadLetter(envelope, payload, reason, error) {
|
|
228
|
+
const dlq = this.options.retryTopics?.dlqTopic ?? null;
|
|
229
|
+
if (!dlq) return;
|
|
230
|
+
const original = this.originalTopic(payload);
|
|
231
|
+
const annotated = (0, import_core.annotate)(envelope, reason, original, {
|
|
232
|
+
attempts: envelope.attempts ?? 0,
|
|
233
|
+
error: error instanceof Error ? error.message : null,
|
|
234
|
+
exception: error instanceof Error ? error.name : null
|
|
235
|
+
});
|
|
236
|
+
const headers = kafkaHeaders(annotated);
|
|
237
|
+
headers["bq-original-topic"] = original;
|
|
238
|
+
await this.produce(dlq, annotated, headers);
|
|
239
|
+
}
|
|
240
|
+
async deadLetterRaw(payload) {
|
|
241
|
+
const dlq = this.options.retryTopics?.dlqTopic ?? null;
|
|
242
|
+
if (!dlq) return;
|
|
243
|
+
this.requireProducer();
|
|
244
|
+
const { message } = payload;
|
|
245
|
+
await this.options.producer.send({
|
|
246
|
+
topic: dlq,
|
|
247
|
+
messages: [{ value: message.value ?? Buffer.alloc(0), timestamp: String(Date.now()) }]
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async produce(topic, envelope, headers) {
|
|
251
|
+
this.requireProducer();
|
|
252
|
+
await this.options.producer.send({
|
|
253
|
+
topic,
|
|
254
|
+
messages: [{ value: import_core.EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }]
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
async commit(payload) {
|
|
258
|
+
await this.consumer.commitOffsets([
|
|
259
|
+
{ topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() }
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
originalTopic(payload) {
|
|
263
|
+
return headerString(payload.message.headers, "bq-original-topic") ?? payload.topic;
|
|
264
|
+
}
|
|
265
|
+
requireProducer() {
|
|
266
|
+
if (!this.options.producer) {
|
|
267
|
+
throw new import_core.BabelQueueError("This Kafka consumer needs a producer to republish (retry/DLQ).");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
report(error, envelope, payload) {
|
|
271
|
+
this.options.onError?.(error, envelope, payload);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
275
|
+
0 && (module.exports = {
|
|
276
|
+
KafkaConsumer,
|
|
277
|
+
KafkaPublisher,
|
|
278
|
+
RetryTopics,
|
|
279
|
+
kafkaHeaders
|
|
280
|
+
});
|
|
281
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apache Kafka adapter for BabelQueue.
|
|
5
|
+
*
|
|
6
|
+
* A canonical-envelope **publisher** and a URN-routed, **process-then-commit** consumer over
|
|
7
|
+
* Apache Kafka (KafkaJS), so a Kafka-based Node service speaks the same contract (envelope
|
|
8
|
+
* shape, URN identity, trace propagation) as the .NET, Java, Python and Go SDKs.
|
|
9
|
+
*
|
|
10
|
+
* Kafka has **no native** per-message ack, delayed delivery, dead-letter queue, or delivery
|
|
11
|
+
* counter — this adapter absorbs all four in the binding layer (the envelope stays
|
|
12
|
+
* `schema_version: 1`): the envelope JSON is the record **value**; the contract fields are
|
|
13
|
+
* mirrored onto `bq-` headers (route on `bq-job` without decoding the body) and the record
|
|
14
|
+
* timestamp mirrors `meta.created_at`; **`bq-attempts` is the authoritative retry counter**;
|
|
15
|
+
* consume is process-then-commit (manual commit, at-least-once); retry/delay use SDK-owned
|
|
16
|
+
* tiered retry topics `<topic>.retry.<n>`; terminal failures go to an opt-in `<topic>.dlq`.
|
|
17
|
+
*
|
|
18
|
+
* This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you
|
|
19
|
+
* provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter
|
|
20
|
+
* structurally).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Incoming record headers (KafkaJS decodes header values to Buffers). */
|
|
24
|
+
type IncomingHeaders = {
|
|
25
|
+
[key: string]: Buffer | string | (Buffer | string)[] | undefined;
|
|
26
|
+
};
|
|
27
|
+
/** A record to produce (structural subset of a KafkaJS message). */
|
|
28
|
+
interface KafkaProducerMessage {
|
|
29
|
+
key?: Buffer | string | null;
|
|
30
|
+
value: Buffer | string;
|
|
31
|
+
headers?: {
|
|
32
|
+
[key: string]: string;
|
|
33
|
+
};
|
|
34
|
+
timestamp?: string;
|
|
35
|
+
}
|
|
36
|
+
/** The subset of the KafkaJS producer this adapter calls. */
|
|
37
|
+
interface KafkaProducerClient {
|
|
38
|
+
send(record: {
|
|
39
|
+
topic: string;
|
|
40
|
+
messages: KafkaProducerMessage[];
|
|
41
|
+
}): Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
/** A received record (structural subset of a KafkaJS message). */
|
|
44
|
+
interface KafkaIncomingMessage {
|
|
45
|
+
value: Buffer | null;
|
|
46
|
+
headers?: IncomingHeaders;
|
|
47
|
+
offset: string;
|
|
48
|
+
timestamp?: string;
|
|
49
|
+
key?: Buffer | null;
|
|
50
|
+
}
|
|
51
|
+
/** The payload KafkaJS passes to `eachMessage`. */
|
|
52
|
+
interface EachMessagePayload {
|
|
53
|
+
topic: string;
|
|
54
|
+
partition: number;
|
|
55
|
+
message: KafkaIncomingMessage;
|
|
56
|
+
}
|
|
57
|
+
/** The subset of the KafkaJS consumer this adapter calls (manual commit — `autoCommit: false`). */
|
|
58
|
+
interface KafkaConsumerClient {
|
|
59
|
+
run(config: {
|
|
60
|
+
autoCommit?: boolean;
|
|
61
|
+
eachMessage: (payload: EachMessagePayload) => Promise<void>;
|
|
62
|
+
}): Promise<void>;
|
|
63
|
+
commitOffsets(offsets: {
|
|
64
|
+
topic: string;
|
|
65
|
+
partition: number;
|
|
66
|
+
offset: string;
|
|
67
|
+
}[]): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */
|
|
70
|
+
declare function kafkaHeaders(envelope: Envelope): {
|
|
71
|
+
[key: string]: string;
|
|
72
|
+
};
|
|
73
|
+
/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */
|
|
74
|
+
interface Tier {
|
|
75
|
+
topic: string;
|
|
76
|
+
delayMs: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The SDK-owned retry/delay topology for one work topic: tiered delay topics
|
|
80
|
+
* `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no
|
|
81
|
+
* tiers configured raises rather than silently dropping.
|
|
82
|
+
*/
|
|
83
|
+
declare class RetryTopics {
|
|
84
|
+
readonly workTopic: string;
|
|
85
|
+
readonly tiers: Tier[];
|
|
86
|
+
readonly dlqTopic: string | null;
|
|
87
|
+
constructor(workTopic: string, delaysMs?: number[], dlqTopic?: string | null | undefined);
|
|
88
|
+
hasTiers(): boolean;
|
|
89
|
+
/** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */
|
|
90
|
+
tierForDelay(delayMs: number): Tier;
|
|
91
|
+
/** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */
|
|
92
|
+
tierForAttempt(attempt: number): Tier;
|
|
93
|
+
private requireTiers;
|
|
94
|
+
}
|
|
95
|
+
/** Options for {@link KafkaPublisher.publish}. */
|
|
96
|
+
interface PublishOptions {
|
|
97
|
+
traceId?: string;
|
|
98
|
+
/** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */
|
|
99
|
+
delayMs?: number;
|
|
100
|
+
}
|
|
101
|
+
/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */
|
|
102
|
+
declare class KafkaPublisher {
|
|
103
|
+
private readonly producer;
|
|
104
|
+
private readonly workTopic;
|
|
105
|
+
private readonly retryTopics?;
|
|
106
|
+
private constructor();
|
|
107
|
+
/** A publisher onto `topic` with no retry topics (a delay raises). */
|
|
108
|
+
static create(producer: KafkaProducerClient, topic: string): KafkaPublisher;
|
|
109
|
+
/** A publisher onto the topology's work topic, with delay routed via its retry tiers. */
|
|
110
|
+
static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher;
|
|
111
|
+
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
112
|
+
publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
|
|
113
|
+
private sendRecord;
|
|
114
|
+
}
|
|
115
|
+
/** A URN handler. Receives the validated envelope and the raw Kafka message. */
|
|
116
|
+
type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage) => unknown | Promise<unknown>;
|
|
117
|
+
/** A map of URN → handler. */
|
|
118
|
+
type BabelHandlers = Record<string, BabelHandler>;
|
|
119
|
+
/** Options for {@link KafkaConsumer}. */
|
|
120
|
+
interface KafkaConsumerOptions {
|
|
121
|
+
/** The producer used to republish retry/DLQ records (required for retry/DLQ). */
|
|
122
|
+
producer?: KafkaProducerClient;
|
|
123
|
+
/** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */
|
|
124
|
+
retryTopics?: RetryTopics;
|
|
125
|
+
/** Attempts before terminal dead-lettering (default 3). */
|
|
126
|
+
maxTries?: number;
|
|
127
|
+
/** Strategy for a URN with no handler (default `fail`). */
|
|
128
|
+
unknownUrn?: string;
|
|
129
|
+
/** Called for a poison record, an unmapped URN, or a throwing handler. */
|
|
130
|
+
onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,
|
|
134
|
+
* routed to the handler for its URN (read from `bq-job`), and its offset committed only after
|
|
135
|
+
* the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with
|
|
136
|
+
* `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`
|
|
137
|
+
* block. The `bq-attempts` header is the authoritative counter (the body is the fallback).
|
|
138
|
+
*/
|
|
139
|
+
declare class KafkaConsumer {
|
|
140
|
+
private readonly consumer;
|
|
141
|
+
private readonly handlers;
|
|
142
|
+
private readonly options;
|
|
143
|
+
private readonly maxTries;
|
|
144
|
+
private readonly unknownUrn;
|
|
145
|
+
constructor(consumer: KafkaConsumerClient, handlers: BabelHandlers, options?: KafkaConsumerOptions);
|
|
146
|
+
/** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */
|
|
147
|
+
run(): Promise<void>;
|
|
148
|
+
/** Route + settle one record (process-then-commit). Exposed for testing. */
|
|
149
|
+
handle(payload: EachMessagePayload): Promise<void>;
|
|
150
|
+
private reconcile;
|
|
151
|
+
private onUnknownUrn;
|
|
152
|
+
private retryOrDeadLetter;
|
|
153
|
+
private republishRetry;
|
|
154
|
+
private deadLetter;
|
|
155
|
+
private deadLetterRaw;
|
|
156
|
+
private produce;
|
|
157
|
+
private commit;
|
|
158
|
+
private originalTopic;
|
|
159
|
+
private requireProducer;
|
|
160
|
+
private report;
|
|
161
|
+
}
|
|
162
|
+
|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apache Kafka adapter for BabelQueue.
|
|
5
|
+
*
|
|
6
|
+
* A canonical-envelope **publisher** and a URN-routed, **process-then-commit** consumer over
|
|
7
|
+
* Apache Kafka (KafkaJS), so a Kafka-based Node service speaks the same contract (envelope
|
|
8
|
+
* shape, URN identity, trace propagation) as the .NET, Java, Python and Go SDKs.
|
|
9
|
+
*
|
|
10
|
+
* Kafka has **no native** per-message ack, delayed delivery, dead-letter queue, or delivery
|
|
11
|
+
* counter — this adapter absorbs all four in the binding layer (the envelope stays
|
|
12
|
+
* `schema_version: 1`): the envelope JSON is the record **value**; the contract fields are
|
|
13
|
+
* mirrored onto `bq-` headers (route on `bq-job` without decoding the body) and the record
|
|
14
|
+
* timestamp mirrors `meta.created_at`; **`bq-attempts` is the authoritative retry counter**;
|
|
15
|
+
* consume is process-then-commit (manual commit, at-least-once); retry/delay use SDK-owned
|
|
16
|
+
* tiered retry topics `<topic>.retry.<n>`; terminal failures go to an opt-in `<topic>.dlq`.
|
|
17
|
+
*
|
|
18
|
+
* This implements §6 of the broker-bindings contract. `kafkajs` is an optional peer — you
|
|
19
|
+
* provide the producer/consumer (a KafkaJS `Producer`/`Consumer` satisfies the adapter
|
|
20
|
+
* structurally).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Incoming record headers (KafkaJS decodes header values to Buffers). */
|
|
24
|
+
type IncomingHeaders = {
|
|
25
|
+
[key: string]: Buffer | string | (Buffer | string)[] | undefined;
|
|
26
|
+
};
|
|
27
|
+
/** A record to produce (structural subset of a KafkaJS message). */
|
|
28
|
+
interface KafkaProducerMessage {
|
|
29
|
+
key?: Buffer | string | null;
|
|
30
|
+
value: Buffer | string;
|
|
31
|
+
headers?: {
|
|
32
|
+
[key: string]: string;
|
|
33
|
+
};
|
|
34
|
+
timestamp?: string;
|
|
35
|
+
}
|
|
36
|
+
/** The subset of the KafkaJS producer this adapter calls. */
|
|
37
|
+
interface KafkaProducerClient {
|
|
38
|
+
send(record: {
|
|
39
|
+
topic: string;
|
|
40
|
+
messages: KafkaProducerMessage[];
|
|
41
|
+
}): Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
/** A received record (structural subset of a KafkaJS message). */
|
|
44
|
+
interface KafkaIncomingMessage {
|
|
45
|
+
value: Buffer | null;
|
|
46
|
+
headers?: IncomingHeaders;
|
|
47
|
+
offset: string;
|
|
48
|
+
timestamp?: string;
|
|
49
|
+
key?: Buffer | null;
|
|
50
|
+
}
|
|
51
|
+
/** The payload KafkaJS passes to `eachMessage`. */
|
|
52
|
+
interface EachMessagePayload {
|
|
53
|
+
topic: string;
|
|
54
|
+
partition: number;
|
|
55
|
+
message: KafkaIncomingMessage;
|
|
56
|
+
}
|
|
57
|
+
/** The subset of the KafkaJS consumer this adapter calls (manual commit — `autoCommit: false`). */
|
|
58
|
+
interface KafkaConsumerClient {
|
|
59
|
+
run(config: {
|
|
60
|
+
autoCommit?: boolean;
|
|
61
|
+
eachMessage: (payload: EachMessagePayload) => Promise<void>;
|
|
62
|
+
}): Promise<void>;
|
|
63
|
+
commitOffsets(offsets: {
|
|
64
|
+
topic: string;
|
|
65
|
+
partition: number;
|
|
66
|
+
offset: string;
|
|
67
|
+
}[]): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/** Project the envelope's contract fields onto Kafka record headers (UTF-8 string values). */
|
|
70
|
+
declare function kafkaHeaders(envelope: Envelope): {
|
|
71
|
+
[key: string]: string;
|
|
72
|
+
};
|
|
73
|
+
/** A single delay tier: the `<topic>.retry.<n>` topic and the delay (ms) it holds for. */
|
|
74
|
+
interface Tier {
|
|
75
|
+
topic: string;
|
|
76
|
+
delayMs: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The SDK-owned retry/delay topology for one work topic: tiered delay topics
|
|
80
|
+
* `<topic>.retry.<n>` (ascending) plus an opt-in `<topic>.dlq`. A delay or release with no
|
|
81
|
+
* tiers configured raises rather than silently dropping.
|
|
82
|
+
*/
|
|
83
|
+
declare class RetryTopics {
|
|
84
|
+
readonly workTopic: string;
|
|
85
|
+
readonly tiers: Tier[];
|
|
86
|
+
readonly dlqTopic: string | null;
|
|
87
|
+
constructor(workTopic: string, delaysMs?: number[], dlqTopic?: string | null | undefined);
|
|
88
|
+
hasTiers(): boolean;
|
|
89
|
+
/** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */
|
|
90
|
+
tierForDelay(delayMs: number): Tier;
|
|
91
|
+
/** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */
|
|
92
|
+
tierForAttempt(attempt: number): Tier;
|
|
93
|
+
private requireTiers;
|
|
94
|
+
}
|
|
95
|
+
/** Options for {@link KafkaPublisher.publish}. */
|
|
96
|
+
interface PublishOptions {
|
|
97
|
+
traceId?: string;
|
|
98
|
+
/** Schedule via a retry tier (requires a {@link RetryTopics}); raises on a plain publisher. */
|
|
99
|
+
delayMs?: number;
|
|
100
|
+
}
|
|
101
|
+
/** Sends canonical-envelope messages to one Kafka work topic with the §6 projection. */
|
|
102
|
+
declare class KafkaPublisher {
|
|
103
|
+
private readonly producer;
|
|
104
|
+
private readonly workTopic;
|
|
105
|
+
private readonly retryTopics?;
|
|
106
|
+
private constructor();
|
|
107
|
+
/** A publisher onto `topic` with no retry topics (a delay raises). */
|
|
108
|
+
static create(producer: KafkaProducerClient, topic: string): KafkaPublisher;
|
|
109
|
+
/** A publisher onto the topology's work topic, with delay routed via its retry tiers. */
|
|
110
|
+
static withRetryTopics(producer: KafkaProducerClient, retryTopics: RetryTopics): KafkaPublisher;
|
|
111
|
+
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
112
|
+
publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
|
|
113
|
+
private sendRecord;
|
|
114
|
+
}
|
|
115
|
+
/** A URN handler. Receives the validated envelope and the raw Kafka message. */
|
|
116
|
+
type BabelHandler = (envelope: Envelope, message: KafkaIncomingMessage) => unknown | Promise<unknown>;
|
|
117
|
+
/** A map of URN → handler. */
|
|
118
|
+
type BabelHandlers = Record<string, BabelHandler>;
|
|
119
|
+
/** Options for {@link KafkaConsumer}. */
|
|
120
|
+
interface KafkaConsumerOptions {
|
|
121
|
+
/** The producer used to republish retry/DLQ records (required for retry/DLQ). */
|
|
122
|
+
producer?: KafkaProducerClient;
|
|
123
|
+
/** The retry/DLQ topology; enables per-record retry, delay, and dead-lettering. */
|
|
124
|
+
retryTopics?: RetryTopics;
|
|
125
|
+
/** Attempts before terminal dead-lettering (default 3). */
|
|
126
|
+
maxTries?: number;
|
|
127
|
+
/** Strategy for a URN with no handler (default `fail`). */
|
|
128
|
+
unknownUrn?: string;
|
|
129
|
+
/** Called for a poison record, an unmapped URN, or a throwing handler. */
|
|
130
|
+
onError?: (error: unknown, envelope: IncomingEnvelope | null, payload: EachMessagePayload) => void;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Consumes a Kafka work topic in process-then-commit mode: each record is decoded, validated,
|
|
134
|
+
* routed to the handler for its URN (read from `bq-job`), and its offset committed only after
|
|
135
|
+
* the handler returns. A throwing handler republishes to a `<topic>.retry.<n>` tier with
|
|
136
|
+
* `bq-attempts + 1`, then commits; terminal failures go to `<topic>.dlq` with a `dead_letter`
|
|
137
|
+
* block. The `bq-attempts` header is the authoritative counter (the body is the fallback).
|
|
138
|
+
*/
|
|
139
|
+
declare class KafkaConsumer {
|
|
140
|
+
private readonly consumer;
|
|
141
|
+
private readonly handlers;
|
|
142
|
+
private readonly options;
|
|
143
|
+
private readonly maxTries;
|
|
144
|
+
private readonly unknownUrn;
|
|
145
|
+
constructor(consumer: KafkaConsumerClient, handlers: BabelHandlers, options?: KafkaConsumerOptions);
|
|
146
|
+
/** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */
|
|
147
|
+
run(): Promise<void>;
|
|
148
|
+
/** Route + settle one record (process-then-commit). Exposed for testing. */
|
|
149
|
+
handle(payload: EachMessagePayload): Promise<void>;
|
|
150
|
+
private reconcile;
|
|
151
|
+
private onUnknownUrn;
|
|
152
|
+
private retryOrDeadLetter;
|
|
153
|
+
private republishRetry;
|
|
154
|
+
private deadLetter;
|
|
155
|
+
private deadLetterRaw;
|
|
156
|
+
private produce;
|
|
157
|
+
private commit;
|
|
158
|
+
private originalTopic;
|
|
159
|
+
private requireProducer;
|
|
160
|
+
private report;
|
|
161
|
+
}
|
|
162
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
|
|
3
|
+
function kafkaHeaders(envelope) {
|
|
4
|
+
const headers = {};
|
|
5
|
+
if (envelope.job) headers["bq-job"] = envelope.job;
|
|
6
|
+
if (envelope.trace_id) headers["bq-trace-id"] = envelope.trace_id;
|
|
7
|
+
if (envelope.meta.id) headers["bq-message-id"] = envelope.meta.id;
|
|
8
|
+
if (envelope.meta.schema_version != null) headers["bq-schema-version"] = String(envelope.meta.schema_version);
|
|
9
|
+
if (envelope.meta.lang) headers["bq-source-lang"] = envelope.meta.lang;
|
|
10
|
+
headers["bq-attempts"] = String(envelope.attempts ?? 0);
|
|
11
|
+
return headers;
|
|
12
|
+
}
|
|
13
|
+
function headerString(headers, key) {
|
|
14
|
+
const value = headers?.[key];
|
|
15
|
+
if (value === void 0) return void 0;
|
|
16
|
+
const v = Array.isArray(value) ? value[0] : value;
|
|
17
|
+
if (v === void 0) return void 0;
|
|
18
|
+
return Buffer.isBuffer(v) ? v.toString("utf8") : String(v);
|
|
19
|
+
}
|
|
20
|
+
function headerInt(headers, key, fallback) {
|
|
21
|
+
const s = headerString(headers, key);
|
|
22
|
+
if (s === void 0) return fallback;
|
|
23
|
+
const n = Number.parseInt(s.trim(), 10);
|
|
24
|
+
return Number.isNaN(n) ? fallback : n;
|
|
25
|
+
}
|
|
26
|
+
var RetryTopics = class {
|
|
27
|
+
constructor(workTopic, delaysMs = [], dlqTopic = void 0) {
|
|
28
|
+
this.workTopic = workTopic;
|
|
29
|
+
this.tiers = [...delaysMs].sort((a, b) => a - b).map((delayMs, i) => ({ topic: `${workTopic}.retry.${i + 1}`, delayMs }));
|
|
30
|
+
this.dlqTopic = dlqTopic === void 0 ? `${workTopic}.dlq` : dlqTopic;
|
|
31
|
+
}
|
|
32
|
+
workTopic;
|
|
33
|
+
tiers;
|
|
34
|
+
dlqTopic;
|
|
35
|
+
hasTiers() {
|
|
36
|
+
return this.tiers.length > 0;
|
|
37
|
+
}
|
|
38
|
+
/** The smallest tier whose delay ≥ `delayMs`; raises if none configured or the delay is too large. */
|
|
39
|
+
tierForDelay(delayMs) {
|
|
40
|
+
this.requireTiers();
|
|
41
|
+
for (const tier of this.tiers) {
|
|
42
|
+
if (tier.delayMs >= delayMs) return tier;
|
|
43
|
+
}
|
|
44
|
+
throw new BabelQueueError(
|
|
45
|
+
`Requested Kafka delay ${delayMs}ms exceeds the largest retry tier (${this.tiers[this.tiers.length - 1].delayMs}ms).`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
/** The tier for a retry at `attempt` (0-based), clamped to the largest; raises if none configured. */
|
|
49
|
+
tierForAttempt(attempt) {
|
|
50
|
+
this.requireTiers();
|
|
51
|
+
return this.tiers[Math.min(Math.max(attempt, 0), this.tiers.length - 1)];
|
|
52
|
+
}
|
|
53
|
+
requireTiers() {
|
|
54
|
+
if (!this.tiers.length) {
|
|
55
|
+
throw new BabelQueueError(`Kafka retry/delay requires retry topics; none are configured for '${this.workTopic}'.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var KafkaPublisher = class _KafkaPublisher {
|
|
60
|
+
constructor(producer, workTopic, retryTopics) {
|
|
61
|
+
this.producer = producer;
|
|
62
|
+
this.workTopic = workTopic;
|
|
63
|
+
this.retryTopics = retryTopics;
|
|
64
|
+
}
|
|
65
|
+
producer;
|
|
66
|
+
workTopic;
|
|
67
|
+
retryTopics;
|
|
68
|
+
/** A publisher onto `topic` with no retry topics (a delay raises). */
|
|
69
|
+
static create(producer, topic) {
|
|
70
|
+
return new _KafkaPublisher(producer, topic);
|
|
71
|
+
}
|
|
72
|
+
/** A publisher onto the topology's work topic, with delay routed via its retry tiers. */
|
|
73
|
+
static withRetryTopics(producer, retryTopics) {
|
|
74
|
+
return new _KafkaPublisher(producer, retryTopics.workTopic, retryTopics);
|
|
75
|
+
}
|
|
76
|
+
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
77
|
+
async publish(urn, data, options = {}) {
|
|
78
|
+
const envelope = EnvelopeCodec.make(urn, data, { queue: this.workTopic, traceId: options.traceId });
|
|
79
|
+
if (options.delayMs != null && options.delayMs > 0) {
|
|
80
|
+
if (!this.retryTopics) {
|
|
81
|
+
throw new BabelQueueError("Kafka has no native delayed delivery; a delay requires retry topics (none configured).");
|
|
82
|
+
}
|
|
83
|
+
const tier = this.retryTopics.tierForDelay(options.delayMs);
|
|
84
|
+
await this.sendRecord(tier.topic, envelope, options.delayMs, this.workTopic);
|
|
85
|
+
} else {
|
|
86
|
+
await this.sendRecord(this.workTopic, envelope);
|
|
87
|
+
}
|
|
88
|
+
return envelope.meta.id;
|
|
89
|
+
}
|
|
90
|
+
async sendRecord(topic, envelope, delayMs, originalTopic) {
|
|
91
|
+
const headers = kafkaHeaders(envelope);
|
|
92
|
+
if (delayMs != null) headers["bq-delay"] = String(delayMs);
|
|
93
|
+
if (originalTopic != null) headers["bq-original-topic"] = originalTopic;
|
|
94
|
+
await this.producer.send({
|
|
95
|
+
topic,
|
|
96
|
+
messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(envelope.meta.created_at) }]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var KafkaConsumer = class {
|
|
101
|
+
constructor(consumer, handlers, options = {}) {
|
|
102
|
+
this.consumer = consumer;
|
|
103
|
+
this.handlers = handlers;
|
|
104
|
+
this.options = options;
|
|
105
|
+
this.maxTries = options.maxTries ?? 3;
|
|
106
|
+
this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;
|
|
107
|
+
}
|
|
108
|
+
consumer;
|
|
109
|
+
handlers;
|
|
110
|
+
options;
|
|
111
|
+
maxTries;
|
|
112
|
+
unknownUrn;
|
|
113
|
+
/** Start the manual-commit consume loop (`autoCommit: false`); resolves when the consumer stops. */
|
|
114
|
+
async run() {
|
|
115
|
+
await this.consumer.run({ autoCommit: false, eachMessage: (payload) => this.handle(payload) });
|
|
116
|
+
}
|
|
117
|
+
/** Route + settle one record (process-then-commit). Exposed for testing. */
|
|
118
|
+
async handle(payload) {
|
|
119
|
+
const { message } = payload;
|
|
120
|
+
const raw = message.value ? message.value.toString("utf8") : "";
|
|
121
|
+
let envelope;
|
|
122
|
+
try {
|
|
123
|
+
envelope = this.reconcile(EnvelopeCodec.decode(raw), message);
|
|
124
|
+
} catch (decodeError) {
|
|
125
|
+
this.report(decodeError, null, payload);
|
|
126
|
+
await this.deadLetterRaw(payload);
|
|
127
|
+
await this.commit(payload);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!EnvelopeCodec.accepts(envelope)) {
|
|
131
|
+
this.report(new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Kafka."), envelope, payload);
|
|
132
|
+
await this.deadLetterRaw(payload);
|
|
133
|
+
await this.commit(payload);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const urn = headerString(message.headers, "bq-job") ?? EnvelopeCodec.urn(envelope);
|
|
137
|
+
const handler = this.handlers[urn];
|
|
138
|
+
if (!handler) {
|
|
139
|
+
await this.onUnknownUrn(payload, envelope, urn);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await handler(envelope, message);
|
|
144
|
+
await this.commit(payload);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.report(error, envelope, payload);
|
|
147
|
+
await this.retryOrDeadLetter(payload, envelope, error);
|
|
148
|
+
await this.commit(payload);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
reconcile(envelope, message) {
|
|
152
|
+
const body = typeof envelope.attempts === "number" ? envelope.attempts : 0;
|
|
153
|
+
return { ...envelope, attempts: headerInt(message.headers, "bq-attempts", body) };
|
|
154
|
+
}
|
|
155
|
+
async onUnknownUrn(payload, envelope, urn) {
|
|
156
|
+
switch (this.unknownUrn) {
|
|
157
|
+
case UnknownUrnStrategy.DELETE:
|
|
158
|
+
await this.commit(payload);
|
|
159
|
+
return;
|
|
160
|
+
case UnknownUrnStrategy.DEAD_LETTER:
|
|
161
|
+
await this.deadLetter(envelope, payload, "unknown_urn");
|
|
162
|
+
await this.commit(payload);
|
|
163
|
+
return;
|
|
164
|
+
case UnknownUrnStrategy.RELEASE:
|
|
165
|
+
await this.republishRetry(payload, envelope);
|
|
166
|
+
await this.commit(payload);
|
|
167
|
+
return;
|
|
168
|
+
default: {
|
|
169
|
+
const error = new UnknownUrnError(urn);
|
|
170
|
+
this.report(error, envelope, payload);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async retryOrDeadLetter(payload, envelope, error) {
|
|
176
|
+
const hasTiers = this.options.retryTopics?.hasTiers() ?? false;
|
|
177
|
+
const hasDlq = (this.options.retryTopics?.dlqTopic ?? null) !== null;
|
|
178
|
+
if (!hasTiers && !hasDlq) {
|
|
179
|
+
void error;
|
|
180
|
+
throw new BabelQueueError("Kafka per-record retry requires retry topics and/or a DLQ; neither is configured.");
|
|
181
|
+
}
|
|
182
|
+
if (hasTiers && (envelope.attempts ?? 0) + 1 < this.maxTries) {
|
|
183
|
+
await this.republishRetry(payload, envelope);
|
|
184
|
+
} else {
|
|
185
|
+
await this.deadLetter(envelope, payload, "failed", error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async republishRetry(payload, envelope) {
|
|
189
|
+
const topics = this.options.retryTopics;
|
|
190
|
+
if (!topics || !topics.hasTiers()) {
|
|
191
|
+
throw new BabelQueueError("Kafka retry/release requires retry topics; none are configured.");
|
|
192
|
+
}
|
|
193
|
+
const tier = topics.tierForAttempt(envelope.attempts ?? 0);
|
|
194
|
+
const bumped = { ...envelope, attempts: (envelope.attempts ?? 0) + 1 };
|
|
195
|
+
const headers = kafkaHeaders(bumped);
|
|
196
|
+
headers["bq-delay"] = String(tier.delayMs);
|
|
197
|
+
headers["bq-original-topic"] = this.originalTopic(payload);
|
|
198
|
+
await this.produce(tier.topic, bumped, headers);
|
|
199
|
+
}
|
|
200
|
+
async deadLetter(envelope, payload, reason, error) {
|
|
201
|
+
const dlq = this.options.retryTopics?.dlqTopic ?? null;
|
|
202
|
+
if (!dlq) return;
|
|
203
|
+
const original = this.originalTopic(payload);
|
|
204
|
+
const annotated = annotate(envelope, reason, original, {
|
|
205
|
+
attempts: envelope.attempts ?? 0,
|
|
206
|
+
error: error instanceof Error ? error.message : null,
|
|
207
|
+
exception: error instanceof Error ? error.name : null
|
|
208
|
+
});
|
|
209
|
+
const headers = kafkaHeaders(annotated);
|
|
210
|
+
headers["bq-original-topic"] = original;
|
|
211
|
+
await this.produce(dlq, annotated, headers);
|
|
212
|
+
}
|
|
213
|
+
async deadLetterRaw(payload) {
|
|
214
|
+
const dlq = this.options.retryTopics?.dlqTopic ?? null;
|
|
215
|
+
if (!dlq) return;
|
|
216
|
+
this.requireProducer();
|
|
217
|
+
const { message } = payload;
|
|
218
|
+
await this.options.producer.send({
|
|
219
|
+
topic: dlq,
|
|
220
|
+
messages: [{ value: message.value ?? Buffer.alloc(0), timestamp: String(Date.now()) }]
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async produce(topic, envelope, headers) {
|
|
224
|
+
this.requireProducer();
|
|
225
|
+
await this.options.producer.send({
|
|
226
|
+
topic,
|
|
227
|
+
messages: [{ value: EnvelopeCodec.encode(envelope), headers, timestamp: String(Date.now()) }]
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async commit(payload) {
|
|
231
|
+
await this.consumer.commitOffsets([
|
|
232
|
+
{ topic: payload.topic, partition: payload.partition, offset: (Number(payload.message.offset) + 1).toString() }
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
originalTopic(payload) {
|
|
236
|
+
return headerString(payload.message.headers, "bq-original-topic") ?? payload.topic;
|
|
237
|
+
}
|
|
238
|
+
requireProducer() {
|
|
239
|
+
if (!this.options.producer) {
|
|
240
|
+
throw new BabelQueueError("This Kafka consumer needs a producer to republish (retry/DLQ).");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
report(error, envelope, payload) {
|
|
244
|
+
this.options.onError?.(error, envelope, payload);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
export {
|
|
248
|
+
KafkaConsumer,
|
|
249
|
+
KafkaPublisher,
|
|
250
|
+
RetryTopics,
|
|
251
|
+
kafkaHeaders
|
|
252
|
+
};
|
|
253
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@babelqueue/kafka",
|
|
3
|
+
"version": "1.0.0",
|
|
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": ["babelqueue", "kafka", "apache-kafka", "kafkajs", "queue", "polyglot", "messaging"],
|
|
6
|
+
"homepage": "https://babelqueue.com",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/BabelQueue/babelqueue-node-adapters/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/BabelQueue/babelqueue-node-adapters.git",
|
|
13
|
+
"directory": "packages/kafka"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Muhammet Şafak <info@muhammetsafak.com.tr>",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/index.d.cts",
|
|
26
|
+
"default": "./dist/index.cjs"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"files": ["dist"],
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "node --import tsx --test test/*.test.ts"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@babelqueue/core": "^1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"kafkajs": "^2.0.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"kafkajs": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|