@eventferry/kafka 2.0.0 → 3.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/dist/index.cjs CHANGED
@@ -32,10 +32,99 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ConfluentDriver: () => ConfluentDriver,
34
34
  KafkaJsDriver: () => KafkaJsDriver,
35
- KafkaPublisher: () => KafkaPublisher
35
+ KafkaPublisher: () => KafkaPublisher,
36
+ classifyConfluentError: () => classifyConfluentError,
37
+ classifyKafkajsError: () => classifyKafkajsError
36
38
  });
37
39
  module.exports = __toCommonJS(index_exports);
38
40
 
41
+ // src/kafkajs-classifier.ts
42
+ function classifyKafkajsError(err) {
43
+ if (!err || typeof err !== "object") return "retriable";
44
+ const e = err;
45
+ if (e.name === "KafkaJSConnectionError") return "retriable";
46
+ if (e.name === "KafkaJSRequestTimeoutError") return "retriable";
47
+ if (e.name === "KafkaJSNonRetriableError") return "fatal";
48
+ const type = typeof e.type === "string" ? e.type : void 0;
49
+ if (type) {
50
+ if (RETRIABLE_TYPES.has(type)) return "retriable";
51
+ if (POISON_TYPES.has(type)) return "poison";
52
+ if (FATAL_TYPES.has(type)) return "fatal";
53
+ }
54
+ if (typeof e.code === "number") {
55
+ const k = CODE_TO_KIND.get(e.code);
56
+ if (k) return k;
57
+ }
58
+ return "retriable";
59
+ }
60
+ var RETRIABLE_TYPES = /* @__PURE__ */ new Set([
61
+ "NOT_LEADER_FOR_PARTITION",
62
+ "LEADER_NOT_AVAILABLE",
63
+ "UNKNOWN_TOPIC_OR_PARTITION",
64
+ "NETWORK_EXCEPTION",
65
+ "REQUEST_TIMED_OUT",
66
+ "REPLICA_NOT_AVAILABLE",
67
+ "NOT_ENOUGH_REPLICAS",
68
+ "NOT_ENOUGH_REPLICAS_AFTER_APPEND",
69
+ "FENCED_LEADER_EPOCH",
70
+ "UNKNOWN_LEADER_EPOCH",
71
+ "BROKER_NOT_AVAILABLE",
72
+ "COORDINATOR_LOAD_IN_PROGRESS",
73
+ "COORDINATOR_NOT_AVAILABLE"
74
+ ]);
75
+ var POISON_TYPES = /* @__PURE__ */ new Set([
76
+ "CORRUPT_MESSAGE",
77
+ "MESSAGE_TOO_LARGE",
78
+ "INVALID_RECORD",
79
+ "UNSUPPORTED_COMPRESSION_TYPE",
80
+ "INVALID_REQUIRED_ACKS",
81
+ "INVALID_PARTITIONS"
82
+ ]);
83
+ var FATAL_TYPES = /* @__PURE__ */ new Set([
84
+ "INVALID_PRODUCER_EPOCH",
85
+ "PRODUCER_FENCED",
86
+ "TOPIC_AUTHORIZATION_FAILED",
87
+ "CLUSTER_AUTHORIZATION_FAILED",
88
+ "TRANSACTIONAL_ID_AUTHORIZATION_FAILED",
89
+ "SASL_AUTHENTICATION_FAILED",
90
+ "INVALID_TRANSACTION_STATE",
91
+ "UNSUPPORTED_VERSION"
92
+ ]);
93
+ var CODE_TO_KIND = /* @__PURE__ */ new Map([
94
+ [2, "poison"],
95
+ // CORRUPT_MESSAGE
96
+ [3, "retriable"],
97
+ // UNKNOWN_TOPIC_OR_PARTITION
98
+ [5, "retriable"],
99
+ // LEADER_NOT_AVAILABLE
100
+ [6, "retriable"],
101
+ // NOT_LEADER_FOR_PARTITION
102
+ [7, "retriable"],
103
+ // REQUEST_TIMED_OUT
104
+ [9, "retriable"],
105
+ // REPLICA_NOT_AVAILABLE
106
+ [10, "poison"],
107
+ // MESSAGE_TOO_LARGE
108
+ [13, "retriable"],
109
+ // NETWORK_EXCEPTION
110
+ [19, "retriable"],
111
+ // NOT_ENOUGH_REPLICAS
112
+ [29, "fatal"],
113
+ // TOPIC_AUTHORIZATION_FAILED
114
+ [31, "fatal"],
115
+ // CLUSTER_AUTHORIZATION_FAILED
116
+ [47, "fatal"],
117
+ // INVALID_PRODUCER_EPOCH
118
+ [58, "fatal"],
119
+ // SASL_AUTHENTICATION_FAILED
120
+ [74, "retriable"],
121
+ // FENCED_LEADER_EPOCH
122
+ [76, "poison"],
123
+ // UNSUPPORTED_COMPRESSION_TYPE
124
+ [87, "poison"]
125
+ // INVALID_RECORD
126
+ ]);
127
+
39
128
  // src/kafkajs-driver.ts
40
129
  var KafkaJsDriver = class {
41
130
  transactional;
@@ -87,19 +176,27 @@ var KafkaJsDriver = class {
87
176
  return messages.map((m) => ({ recordId: m.recordId, ok: true }));
88
177
  } catch (err) {
89
178
  await txn.abort().catch(() => void 0);
90
- const error = err instanceof Error ? err : new Error(String(err));
91
- return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));
179
+ return failedResults(messages, err);
92
180
  }
93
181
  }
94
182
  try {
95
183
  await this.producer.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });
96
184
  return messages.map((m) => ({ recordId: m.recordId, ok: true }));
97
185
  } catch (err) {
98
- const error = err instanceof Error ? err : new Error(String(err));
99
- return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));
186
+ return failedResults(messages, err);
100
187
  }
101
188
  }
102
189
  };
190
+ function failedResults(messages, err) {
191
+ const error = err instanceof Error ? err : new Error(String(err));
192
+ const errorKind = classifyKafkajsError(err);
193
+ return messages.map((m) => ({
194
+ recordId: m.recordId,
195
+ ok: false,
196
+ error,
197
+ errorKind
198
+ }));
199
+ }
103
200
  function groupByTopic(messages, compression) {
104
201
  const byTopic = /* @__PURE__ */ new Map();
105
202
  for (const m of messages) {
@@ -127,6 +224,104 @@ async function importKafkaJs() {
127
224
  }
128
225
  }
129
226
 
227
+ // src/confluent-classifier.ts
228
+ function classifyConfluentError(err) {
229
+ if (!err || typeof err !== "object") return "retriable";
230
+ const e = err;
231
+ if (typeof e.code === "number") {
232
+ const k = CODE_TO_KIND2.get(e.code);
233
+ if (k) return k;
234
+ }
235
+ if (typeof e.name === "string") {
236
+ const k = NAME_TO_KIND.get(e.name);
237
+ if (k) return k;
238
+ }
239
+ return "retriable";
240
+ }
241
+ var CODE_TO_KIND2 = /* @__PURE__ */ new Map([
242
+ // Library-internal (negative codes)
243
+ [-184, "backpressure"],
244
+ // ERR__QUEUE_FULL — our outbound buffer is full
245
+ [-185, "retriable"],
246
+ // ERR__TIMED_OUT
247
+ [-187, "retriable"],
248
+ // ERR__ALL_BROKERS_DOWN
249
+ [-188, "poison"],
250
+ // ERR__UNKNOWN_TOPIC — topic doesn't exist on broker
251
+ [-190, "poison"],
252
+ // ERR__UNKNOWN_PARTITION
253
+ [-192, "retriable"],
254
+ // ERR__MSG_TIMED_OUT
255
+ [-195, "retriable"],
256
+ // ERR__TRANSPORT
257
+ [-198, "poison"],
258
+ // ERR__BAD_COMPRESSION
259
+ [-144, "fatal"],
260
+ // ERR__FENCED — producer fenced by another with same txn id
261
+ [-150, "fatal"],
262
+ // ERR__FATAL — unrecoverable librdkafka error
263
+ [-169, "fatal"],
264
+ // ERR__AUTHENTICATION
265
+ [-181, "fatal"],
266
+ // ERR__SSL
267
+ [-196, "retriable"],
268
+ // ERR__FAIL — catch-all, safe-default to retriable
269
+ // Wire-protocol (non-negative codes — Kafka error-code registry)
270
+ [2, "poison"],
271
+ // CORRUPT_MESSAGE
272
+ [3, "retriable"],
273
+ // UNKNOWN_TOPIC_OR_PARTITION
274
+ [5, "retriable"],
275
+ // LEADER_NOT_AVAILABLE
276
+ [6, "retriable"],
277
+ // NOT_LEADER_FOR_PARTITION
278
+ [7, "retriable"],
279
+ // REQUEST_TIMED_OUT
280
+ [9, "retriable"],
281
+ // REPLICA_NOT_AVAILABLE
282
+ [10, "poison"],
283
+ // MESSAGE_TOO_LARGE
284
+ [13, "retriable"],
285
+ // NETWORK_EXCEPTION
286
+ [19, "retriable"],
287
+ // NOT_ENOUGH_REPLICAS
288
+ [29, "fatal"],
289
+ // TOPIC_AUTHORIZATION_FAILED
290
+ [31, "fatal"],
291
+ // CLUSTER_AUTHORIZATION_FAILED
292
+ [47, "fatal"],
293
+ // INVALID_PRODUCER_EPOCH
294
+ [58, "fatal"],
295
+ // SASL_AUTHENTICATION_FAILED
296
+ [74, "retriable"],
297
+ // FENCED_LEADER_EPOCH
298
+ [76, "poison"],
299
+ // UNSUPPORTED_COMPRESSION_TYPE
300
+ [87, "poison"],
301
+ // INVALID_RECORD
302
+ [89, "quota"]
303
+ // THROTTLING_QUOTA_EXCEEDED
304
+ ]);
305
+ var NAME_TO_KIND = /* @__PURE__ */ new Map([
306
+ ["ERR__QUEUE_FULL", "backpressure"],
307
+ ["ERR__FENCED", "fatal"],
308
+ ["ERR__FATAL", "fatal"],
309
+ ["ERR__AUTHENTICATION", "fatal"],
310
+ ["ERR__SSL", "fatal"],
311
+ ["ERR__UNKNOWN_TOPIC", "poison"],
312
+ ["ERR__UNKNOWN_PARTITION", "poison"],
313
+ ["ERR__BAD_COMPRESSION", "poison"],
314
+ ["ERR_TOPIC_AUTHORIZATION_FAILED", "fatal"],
315
+ ["ERR_CLUSTER_AUTHORIZATION_FAILED", "fatal"],
316
+ ["ERR_INVALID_PRODUCER_EPOCH", "fatal"],
317
+ ["ERR_SASL_AUTHENTICATION_FAILED", "fatal"],
318
+ ["ERR_CORRUPT_MESSAGE", "poison"],
319
+ ["ERR_MSG_SIZE_TOO_LARGE", "poison"],
320
+ ["ERR_INVALID_RECORD", "poison"],
321
+ ["ERR_UNSUPPORTED_COMPRESSION_TYPE", "poison"],
322
+ ["ERR_THROTTLING_QUOTA_EXCEEDED", "quota"]
323
+ ]);
324
+
130
325
  // src/confluent-driver.ts
131
326
  var ConfluentDriver = class {
132
327
  transactional;
@@ -193,19 +388,27 @@ var ConfluentDriver = class {
193
388
  return messages.map((m) => ({ recordId: m.recordId, ok: true }));
194
389
  } catch (err) {
195
390
  await txn.abort().catch(() => void 0);
196
- const error = err instanceof Error ? err : new Error(String(err));
197
- return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));
391
+ return failedResults2(messages, err);
198
392
  }
199
393
  }
200
394
  try {
201
395
  await doSends(this.producer);
202
396
  return messages.map((m) => ({ recordId: m.recordId, ok: true }));
203
397
  } catch (err) {
204
- const error = err instanceof Error ? err : new Error(String(err));
205
- return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));
398
+ return failedResults2(messages, err);
206
399
  }
207
400
  }
208
401
  };
402
+ function failedResults2(messages, err) {
403
+ const error = err instanceof Error ? err : new Error(String(err));
404
+ const errorKind = classifyConfluentError(err);
405
+ return messages.map((m) => ({
406
+ recordId: m.recordId,
407
+ ok: false,
408
+ error,
409
+ errorKind
410
+ }));
411
+ }
209
412
  function groupByTopic2(messages) {
210
413
  const byTopic = /* @__PURE__ */ new Map();
211
414
  for (const m of messages) {
@@ -282,6 +485,8 @@ function selectDriver(opts) {
282
485
  0 && (module.exports = {
283
486
  ConfluentDriver,
284
487
  KafkaJsDriver,
285
- KafkaPublisher
488
+ KafkaPublisher,
489
+ classifyConfluentError,
490
+ classifyKafkajsError
286
491
  });
287
492
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/kafkajs-driver.ts","../src/confluent-driver.ts","../src/publisher.ts"],"sourcesContent":["export * from \"./driver.js\";\nexport * from \"./kafkajs-driver.js\";\nexport * from \"./confluent-driver.js\";\nexport * from \"./publisher.js\";\n","import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\n\n// Loosely-typed structural references to the kafkajs API so this file\n// compiles without kafkajs installed (it's an optional peer dep).\ninterface KjsProducer {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n // kafkajs: sendBatch takes { topicMessages }, send takes a single { topic, messages }.\n sendBatch(args: unknown): Promise<unknown>;\n transaction(): Promise<KjsTransaction>;\n}\ninterface KjsTransaction {\n sendBatch(args: unknown): Promise<unknown>;\n commit(): Promise<void>;\n abort(): Promise<void>;\n}\ninterface KjsKafka {\n producer(args?: unknown): KjsProducer;\n}\n\nexport interface KafkaJsDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {}\n\n/**\n * Driver backed by the pure-JS `kafkajs` client. Simple, zero native deps.\n */\nexport class KafkaJsDriver implements KafkaDriver {\n readonly transactional: boolean;\n private producer: KjsProducer | null = null;\n private readonly opts: KafkaJsDriverOptions;\n\n constructor(opts: KafkaJsDriverOptions) {\n this.opts = opts;\n this.transactional = opts.transactional ?? false;\n if (this.transactional && !opts.transactionalId) {\n throw new Error(\n \"KafkaJsDriver: transactionalId is required when transactional=true\",\n );\n }\n }\n\n async connect(): Promise<void> {\n this.producer = await this.createProducer();\n await this.producer.connect();\n }\n\n /**\n * Construct the underlying kafkajs producer. Overridable as a test seam so\n * the send/transaction logic can be exercised without a real broker.\n */\n protected async createProducer(): Promise<KjsProducer> {\n const mod = await importKafkaJs();\n const kafka: KjsKafka = new mod.Kafka({\n clientId: this.opts.clientId ?? \"eventferry\",\n brokers: this.opts.brokers,\n ssl: this.opts.ssl,\n sasl: this.opts.sasl,\n });\n return kafka.producer({\n idempotent: this.opts.idempotent ?? true,\n maxInFlightRequests: this.transactional ? 1 : undefined,\n transactionalId: this.transactional ? this.opts.transactionalId : undefined,\n });\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n async sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (!this.producer) throw new Error(\"KafkaJsDriver not connected\");\n const topicMessages = groupByTopic(messages, this.opts.compression);\n\n if (this.transactional) {\n const txn = await this.producer.transaction();\n try {\n await txn.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });\n await txn.commit();\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n await txn.abort().catch(() => undefined);\n const error = err instanceof Error ? err : new Error(String(err));\n return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));\n }\n }\n\n try {\n await this.producer.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));\n }\n }\n}\n\nfunction groupByTopic(messages: PublishableMessage[], compression?: string) {\n const byTopic = new Map<string, unknown[]>();\n for (const m of messages) {\n const arr = byTopic.get(m.topic) ?? [];\n arr.push({\n key: m.key,\n value: m.value,\n headers: m.headers,\n });\n byTopic.set(m.topic, arr);\n }\n return [...byTopic.entries()].map(([topic, msgs]) => ({\n topic,\n messages: msgs,\n ...(compression && compression !== \"none\" ? { compression } : {}),\n }));\n}\n\nasync function importKafkaJs(): Promise<{ Kafka: new (cfg: unknown) => KjsKafka }> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"kafkajs\")) as any;\n } catch {\n throw new Error(\n 'Driver \"kafkajs\" selected but the \"kafkajs\" package is not installed. Run: npm i kafkajs',\n );\n }\n}\n","import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\n\n// Structural typing of the confluent KafkaJS-compatible API surface so this\n// file compiles without the optional native dep installed.\ninterface CkProducer {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n send(args: unknown): Promise<unknown>;\n transaction(): Promise<CkTransaction>;\n}\ninterface CkTransaction {\n send(args: unknown): Promise<unknown>;\n commit(): Promise<void>;\n abort(): Promise<void>;\n}\ninterface CkKafka {\n producer(args?: unknown): CkProducer;\n}\n\nexport interface ConfluentDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {}\n\n/**\n * Driver backed by `@confluentinc/kafka-javascript` (librdkafka wrapper).\n * Higher throughput; uses the KafkaJS-compatible promisified API so the\n * adapter mirrors the kafkajs driver closely.\n */\nexport class ConfluentDriver implements KafkaDriver {\n readonly transactional: boolean;\n private producer: CkProducer | null = null;\n private readonly opts: ConfluentDriverOptions;\n\n constructor(opts: ConfluentDriverOptions) {\n this.opts = opts;\n this.transactional = opts.transactional ?? false;\n if (this.transactional && !opts.transactionalId) {\n throw new Error(\n \"ConfluentDriver: transactionalId is required when transactional=true\",\n );\n }\n }\n\n async connect(): Promise<void> {\n this.producer = await this.createProducer();\n await this.producer.connect();\n }\n\n /**\n * Construct the underlying confluent producer. Overridable as a test seam so\n * the send/transaction logic can be exercised without a real broker.\n */\n protected async createProducer(): Promise<CkProducer> {\n const mod = await importConfluent();\n const kafka: CkKafka = new mod.KafkaJS.Kafka({\n kafkaJS: {\n clientId: this.opts.clientId ?? \"eventferry\",\n brokers: this.opts.brokers,\n ssl: this.opts.ssl,\n sasl: this.opts.sasl,\n },\n });\n return kafka.producer({\n kafkaJS: {\n idempotent: this.opts.idempotent ?? true,\n ...(this.transactional\n ? { transactionalId: this.opts.transactionalId }\n : {}),\n },\n });\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n async sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (!this.producer) throw new Error(\"ConfluentDriver not connected\");\n const topicMessages = groupByTopic(messages);\n const acks = this.opts.acks ?? -1;\n const compression = this.opts.compression;\n\n const doSends = async (target: CkProducer | CkTransaction) => {\n for (const tm of topicMessages) {\n await target.send({\n topic: tm.topic,\n messages: tm.messages,\n acks,\n ...(compression && compression !== \"none\" ? { compression } : {}),\n });\n }\n };\n\n if (this.transactional) {\n const txn = await this.producer.transaction();\n try {\n await doSends(txn);\n await txn.commit();\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n await txn.abort().catch(() => undefined);\n const error = err instanceof Error ? err : new Error(String(err));\n return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));\n }\n }\n\n try {\n await doSends(this.producer);\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));\n }\n }\n}\n\nfunction groupByTopic(messages: PublishableMessage[]) {\n const byTopic = new Map<string, unknown[]>();\n for (const m of messages) {\n const arr = byTopic.get(m.topic) ?? [];\n arr.push({ key: m.key, value: m.value, headers: m.headers });\n byTopic.set(m.topic, arr);\n }\n return [...byTopic.entries()].map(([topic, msgs]) => ({\n topic,\n messages: msgs,\n }));\n}\n\nasync function importConfluent(): Promise<{\n KafkaJS: { Kafka: new (cfg: unknown) => CkKafka };\n}> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"@confluentinc/kafka-javascript\")) as any;\n } catch {\n throw new Error(\n 'Driver \"confluent\" selected but \"@confluentinc/kafka-javascript\" is not installed. Run: npm i @confluentinc/kafka-javascript',\n );\n }\n}\n","import type {\n PublishableMessage,\n Publisher,\n PublishResult,\n} from \"@eventferry/core\";\nimport { ConfluentDriver } from \"./confluent-driver.js\";\nimport type {\n DriverKind,\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\nimport { KafkaJsDriver } from \"./kafkajs-driver.js\";\n\nexport interface KafkaPublisherOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {\n /** Which underlying client to use. Default \"kafkajs\". */\n driver?: DriverKind;\n /**\n * Provide a fully custom driver instance instead of the built-ins.\n * Useful for testing or unsupported clients.\n */\n customDriver?: KafkaDriver;\n}\n\n/**\n * The Publisher the Relay talks to. Wraps a pluggable KafkaDriver and\n * adds dead-letter routing. Works against Kafka and Redpanda identically\n * (Redpanda is Kafka-API compatible).\n */\nexport class KafkaPublisher implements Publisher {\n private readonly driver: KafkaDriver;\n\n constructor(opts: KafkaPublisherOptions) {\n this.driver = opts.customDriver ?? selectDriver(opts);\n }\n\n connect(): Promise<void> {\n return this.driver.connect();\n }\n\n disconnect(): Promise<void> {\n return this.driver.disconnect();\n }\n\n publish(messages: PublishableMessage[]): Promise<PublishResult[]> {\n return this.driver.sendBatch(messages);\n }\n\n /**\n * Send a single dead-lettered message. The message already carries the\n * DLQ topic (the Relay rewrites it), plus the failure reason as a header.\n */\n async publishToDlq(message: PublishableMessage, error: Error): Promise<void> {\n const dlqMessage: PublishableMessage = {\n ...message,\n headers: {\n ...message.headers,\n \"dlq-reason\": error.message,\n \"dlq-original-topic\": message.headers[\"original-topic\"] ?? \"\",\n \"dlq-failed-at\": new Date().toISOString(),\n },\n };\n const [result] = await this.driver.sendBatch([dlqMessage]);\n if (result && !result.ok) {\n throw result.error ?? new Error(\"DLQ publish failed\");\n }\n }\n\n /** Whether the configured driver provides atomic (EOS) batch sends. */\n get transactional(): boolean {\n return this.driver.transactional;\n }\n}\n\nfunction selectDriver(opts: KafkaPublisherOptions): KafkaDriver {\n const kind = opts.driver ?? \"kafkajs\";\n switch (kind) {\n case \"kafkajs\":\n return new KafkaJsDriver(opts);\n case \"confluent\":\n return new ConfluentDriver(opts);\n default:\n throw new Error(`Unknown driver \"${kind}\"`);\n }\n}\n"],"mappings":"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","names":["groupByTopic"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/kafkajs-classifier.ts","../src/kafkajs-driver.ts","../src/confluent-classifier.ts","../src/confluent-driver.ts","../src/publisher.ts"],"sourcesContent":["export * from \"./driver.js\";\nexport * from \"./kafkajs-driver.js\";\nexport * from \"./kafkajs-classifier.js\";\nexport * from \"./confluent-driver.js\";\nexport * from \"./confluent-classifier.js\";\nexport * from \"./publisher.js\";\n","import type { PublishErrorKind } from \"@eventferry/core\";\n\n/**\n * Classify a kafkajs producer error into a {@link PublishErrorKind} so the\n * core relay can decide whether to retry, short-circuit to the DLQ, or pause\n * polling.\n *\n * Mapping verified against `kafkajs/src/errors.js` (v2.x). Protocol error\n * codes match the Kafka Protocol error-code registry. Library-specific\n * subclasses (`KafkaJSRequestTimeoutError`, `KafkaJSConnectionError`,\n * `KafkaJSNonRetriableError`) are matched by the `name` property kafkajs\n * sets on its Error subclasses.\n *\n * Unknown errors fall back to `\"retriable\"` — the safe bias. At worst we\n * retry an error that should have been skipped; in practice we'd rather\n * over-retry than mis-classify a transient blip as terminal.\n */\nexport function classifyKafkajsError(err: unknown): PublishErrorKind {\n if (!err || typeof err !== \"object\") return \"retriable\";\n const e = err as { name?: string; type?: string; code?: number };\n\n // Class-based first — these don't carry a protocol error code.\n if (e.name === \"KafkaJSConnectionError\") return \"retriable\";\n if (e.name === \"KafkaJSRequestTimeoutError\") return \"retriable\";\n if (e.name === \"KafkaJSNonRetriableError\") return \"fatal\";\n\n // Protocol error type (string) — kafkajs's KafkaJSProtocolError exposes\n // both `type` (uppercase string) and `code` (number). Use `type` first\n // for readability and fall back to `code` for codes that lack a stable\n // string label.\n const type = typeof e.type === \"string\" ? e.type : undefined;\n if (type) {\n if (RETRIABLE_TYPES.has(type)) return \"retriable\";\n if (POISON_TYPES.has(type)) return \"poison\";\n if (FATAL_TYPES.has(type)) return \"fatal\";\n }\n\n if (typeof e.code === \"number\") {\n const k = CODE_TO_KIND.get(e.code);\n if (k) return k;\n }\n\n return \"retriable\";\n}\n\nconst RETRIABLE_TYPES = new Set<string>([\n \"NOT_LEADER_FOR_PARTITION\",\n \"LEADER_NOT_AVAILABLE\",\n \"UNKNOWN_TOPIC_OR_PARTITION\",\n \"NETWORK_EXCEPTION\",\n \"REQUEST_TIMED_OUT\",\n \"REPLICA_NOT_AVAILABLE\",\n \"NOT_ENOUGH_REPLICAS\",\n \"NOT_ENOUGH_REPLICAS_AFTER_APPEND\",\n \"FENCED_LEADER_EPOCH\",\n \"UNKNOWN_LEADER_EPOCH\",\n \"BROKER_NOT_AVAILABLE\",\n \"COORDINATOR_LOAD_IN_PROGRESS\",\n \"COORDINATOR_NOT_AVAILABLE\",\n]);\n\nconst POISON_TYPES = new Set<string>([\n \"CORRUPT_MESSAGE\",\n \"MESSAGE_TOO_LARGE\",\n \"INVALID_RECORD\",\n \"UNSUPPORTED_COMPRESSION_TYPE\",\n \"INVALID_REQUIRED_ACKS\",\n \"INVALID_PARTITIONS\",\n]);\n\nconst FATAL_TYPES = new Set<string>([\n \"INVALID_PRODUCER_EPOCH\",\n \"PRODUCER_FENCED\",\n \"TOPIC_AUTHORIZATION_FAILED\",\n \"CLUSTER_AUTHORIZATION_FAILED\",\n \"TRANSACTIONAL_ID_AUTHORIZATION_FAILED\",\n \"SASL_AUTHENTICATION_FAILED\",\n \"INVALID_TRANSACTION_STATE\",\n \"UNSUPPORTED_VERSION\",\n]);\n\n/** Numeric fallback for clusters that only return the wire code. */\nconst CODE_TO_KIND: ReadonlyMap<number, PublishErrorKind> = new Map([\n [2, \"poison\"], // CORRUPT_MESSAGE\n [3, \"retriable\"], // UNKNOWN_TOPIC_OR_PARTITION\n [5, \"retriable\"], // LEADER_NOT_AVAILABLE\n [6, \"retriable\"], // NOT_LEADER_FOR_PARTITION\n [7, \"retriable\"], // REQUEST_TIMED_OUT\n [9, \"retriable\"], // REPLICA_NOT_AVAILABLE\n [10, \"poison\"], // MESSAGE_TOO_LARGE\n [13, \"retriable\"], // NETWORK_EXCEPTION\n [19, \"retriable\"], // NOT_ENOUGH_REPLICAS\n [29, \"fatal\"], // TOPIC_AUTHORIZATION_FAILED\n [31, \"fatal\"], // CLUSTER_AUTHORIZATION_FAILED\n [47, \"fatal\"], // INVALID_PRODUCER_EPOCH\n [58, \"fatal\"], // SASL_AUTHENTICATION_FAILED\n [74, \"retriable\"], // FENCED_LEADER_EPOCH\n [76, \"poison\"], // UNSUPPORTED_COMPRESSION_TYPE\n [87, \"poison\"], // INVALID_RECORD\n]);\n","import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport { classifyKafkajsError } from \"./kafkajs-classifier.js\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\n\n// Loosely-typed structural references to the kafkajs API so this file\n// compiles without kafkajs installed (it's an optional peer dep).\ninterface KjsProducer {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n // kafkajs: sendBatch takes { topicMessages }, send takes a single { topic, messages }.\n sendBatch(args: unknown): Promise<unknown>;\n transaction(): Promise<KjsTransaction>;\n}\ninterface KjsTransaction {\n sendBatch(args: unknown): Promise<unknown>;\n commit(): Promise<void>;\n abort(): Promise<void>;\n}\ninterface KjsKafka {\n producer(args?: unknown): KjsProducer;\n}\n\nexport interface KafkaJsDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {}\n\n/**\n * Driver backed by the pure-JS `kafkajs` client. Simple, zero native deps.\n */\nexport class KafkaJsDriver implements KafkaDriver {\n readonly transactional: boolean;\n private producer: KjsProducer | null = null;\n private readonly opts: KafkaJsDriverOptions;\n\n constructor(opts: KafkaJsDriverOptions) {\n this.opts = opts;\n this.transactional = opts.transactional ?? false;\n if (this.transactional && !opts.transactionalId) {\n throw new Error(\n \"KafkaJsDriver: transactionalId is required when transactional=true\",\n );\n }\n }\n\n async connect(): Promise<void> {\n this.producer = await this.createProducer();\n await this.producer.connect();\n }\n\n /**\n * Construct the underlying kafkajs producer. Overridable as a test seam so\n * the send/transaction logic can be exercised without a real broker.\n */\n protected async createProducer(): Promise<KjsProducer> {\n const mod = await importKafkaJs();\n const kafka: KjsKafka = new mod.Kafka({\n clientId: this.opts.clientId ?? \"eventferry\",\n brokers: this.opts.brokers,\n ssl: this.opts.ssl,\n sasl: this.opts.sasl,\n });\n return kafka.producer({\n idempotent: this.opts.idempotent ?? true,\n maxInFlightRequests: this.transactional ? 1 : undefined,\n transactionalId: this.transactional ? this.opts.transactionalId : undefined,\n });\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n async sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (!this.producer) throw new Error(\"KafkaJsDriver not connected\");\n const topicMessages = groupByTopic(messages, this.opts.compression);\n\n if (this.transactional) {\n const txn = await this.producer.transaction();\n try {\n await txn.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });\n await txn.commit();\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n await txn.abort().catch(() => undefined);\n return failedResults(messages, err);\n }\n }\n\n try {\n await this.producer.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n return failedResults(messages, err);\n }\n }\n}\n\nfunction failedResults(\n messages: PublishableMessage[],\n err: unknown,\n): PublishResult[] {\n const error = err instanceof Error ? err : new Error(String(err));\n const errorKind = classifyKafkajsError(err);\n return messages.map((m) => ({\n recordId: m.recordId,\n ok: false,\n error,\n errorKind,\n }));\n}\n\nfunction groupByTopic(messages: PublishableMessage[], compression?: string) {\n const byTopic = new Map<string, unknown[]>();\n for (const m of messages) {\n const arr = byTopic.get(m.topic) ?? [];\n arr.push({\n key: m.key,\n value: m.value,\n headers: m.headers,\n });\n byTopic.set(m.topic, arr);\n }\n return [...byTopic.entries()].map(([topic, msgs]) => ({\n topic,\n messages: msgs,\n ...(compression && compression !== \"none\" ? { compression } : {}),\n }));\n}\n\nasync function importKafkaJs(): Promise<{ Kafka: new (cfg: unknown) => KjsKafka }> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"kafkajs\")) as any;\n } catch {\n throw new Error(\n 'Driver \"kafkajs\" selected but the \"kafkajs\" package is not installed. Run: npm i kafkajs',\n );\n }\n}\n","import type { PublishErrorKind } from \"@eventferry/core\";\n\n/**\n * Classify a `@confluentinc/kafka-javascript` (librdkafka) producer error\n * into a {@link PublishErrorKind} so the core relay can decide whether to\n * retry, short-circuit to the DLQ, or pause polling.\n *\n * librdkafka exposes errors as numeric `RD_KAFKA_RESP_ERR_*` codes — negative\n * codes are library-internal (transport, queue-full, ssl), non-negative\n * codes are wire-protocol errors that match the Kafka protocol's error-code\n * registry. The confluent driver surfaces these on `err.code` (alongside\n * an `err.name` for the symbolic form).\n *\n * Unknown errors fall back to `\"retriable\"` — the safe bias.\n */\nexport function classifyConfluentError(err: unknown): PublishErrorKind {\n if (!err || typeof err !== \"object\") return \"retriable\";\n const e = err as { code?: number; name?: string };\n\n if (typeof e.code === \"number\") {\n const k = CODE_TO_KIND.get(e.code);\n if (k) return k;\n }\n\n if (typeof e.name === \"string\") {\n const k = NAME_TO_KIND.get(e.name);\n if (k) return k;\n }\n\n return \"retriable\";\n}\n\n/**\n * Authoritative mapping for the most-common librdkafka producer error codes.\n * Sources: `librdkafka/src/rdkafka.h` (`RD_KAFKA_RESP_ERR_*` enum) and the\n * Kafka Protocol error-code registry. Adding a code here is a one-line\n * change — start narrow, broaden as production exposes new codes.\n */\nconst CODE_TO_KIND: ReadonlyMap<number, PublishErrorKind> = new Map([\n // Library-internal (negative codes)\n [-184, \"backpressure\"], // ERR__QUEUE_FULL — our outbound buffer is full\n [-185, \"retriable\"], // ERR__TIMED_OUT\n [-187, \"retriable\"], // ERR__ALL_BROKERS_DOWN\n [-188, \"poison\"], // ERR__UNKNOWN_TOPIC — topic doesn't exist on broker\n [-190, \"poison\"], // ERR__UNKNOWN_PARTITION\n [-192, \"retriable\"], // ERR__MSG_TIMED_OUT\n [-195, \"retriable\"], // ERR__TRANSPORT\n [-198, \"poison\"], // ERR__BAD_COMPRESSION\n [-144, \"fatal\"], // ERR__FENCED — producer fenced by another with same txn id\n [-150, \"fatal\"], // ERR__FATAL — unrecoverable librdkafka error\n [-169, \"fatal\"], // ERR__AUTHENTICATION\n [-181, \"fatal\"], // ERR__SSL\n [-196, \"retriable\"], // ERR__FAIL — catch-all, safe-default to retriable\n\n // Wire-protocol (non-negative codes — Kafka error-code registry)\n [2, \"poison\"], // CORRUPT_MESSAGE\n [3, \"retriable\"], // UNKNOWN_TOPIC_OR_PARTITION\n [5, \"retriable\"], // LEADER_NOT_AVAILABLE\n [6, \"retriable\"], // NOT_LEADER_FOR_PARTITION\n [7, \"retriable\"], // REQUEST_TIMED_OUT\n [9, \"retriable\"], // REPLICA_NOT_AVAILABLE\n [10, \"poison\"], // MESSAGE_TOO_LARGE\n [13, \"retriable\"], // NETWORK_EXCEPTION\n [19, \"retriable\"], // NOT_ENOUGH_REPLICAS\n [29, \"fatal\"], // TOPIC_AUTHORIZATION_FAILED\n [31, \"fatal\"], // CLUSTER_AUTHORIZATION_FAILED\n [47, \"fatal\"], // INVALID_PRODUCER_EPOCH\n [58, \"fatal\"], // SASL_AUTHENTICATION_FAILED\n [74, \"retriable\"], // FENCED_LEADER_EPOCH\n [76, \"poison\"], // UNSUPPORTED_COMPRESSION_TYPE\n [87, \"poison\"], // INVALID_RECORD\n [89, \"quota\"], // THROTTLING_QUOTA_EXCEEDED\n]);\n\n/** Symbolic name fallback for clients that surface `err.name` only. */\nconst NAME_TO_KIND: ReadonlyMap<string, PublishErrorKind> = new Map([\n [\"ERR__QUEUE_FULL\", \"backpressure\"],\n [\"ERR__FENCED\", \"fatal\"],\n [\"ERR__FATAL\", \"fatal\"],\n [\"ERR__AUTHENTICATION\", \"fatal\"],\n [\"ERR__SSL\", \"fatal\"],\n [\"ERR__UNKNOWN_TOPIC\", \"poison\"],\n [\"ERR__UNKNOWN_PARTITION\", \"poison\"],\n [\"ERR__BAD_COMPRESSION\", \"poison\"],\n [\"ERR_TOPIC_AUTHORIZATION_FAILED\", \"fatal\"],\n [\"ERR_CLUSTER_AUTHORIZATION_FAILED\", \"fatal\"],\n [\"ERR_INVALID_PRODUCER_EPOCH\", \"fatal\"],\n [\"ERR_SASL_AUTHENTICATION_FAILED\", \"fatal\"],\n [\"ERR_CORRUPT_MESSAGE\", \"poison\"],\n [\"ERR_MSG_SIZE_TOO_LARGE\", \"poison\"],\n [\"ERR_INVALID_RECORD\", \"poison\"],\n [\"ERR_UNSUPPORTED_COMPRESSION_TYPE\", \"poison\"],\n [\"ERR_THROTTLING_QUOTA_EXCEEDED\", \"quota\"],\n]);\n","import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport { classifyConfluentError } from \"./confluent-classifier.js\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\n\n// Structural typing of the confluent KafkaJS-compatible API surface so this\n// file compiles without the optional native dep installed.\ninterface CkProducer {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n send(args: unknown): Promise<unknown>;\n transaction(): Promise<CkTransaction>;\n}\ninterface CkTransaction {\n send(args: unknown): Promise<unknown>;\n commit(): Promise<void>;\n abort(): Promise<void>;\n}\ninterface CkKafka {\n producer(args?: unknown): CkProducer;\n}\n\nexport interface ConfluentDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {}\n\n/**\n * Driver backed by `@confluentinc/kafka-javascript` (librdkafka wrapper).\n * Higher throughput; uses the KafkaJS-compatible promisified API so the\n * adapter mirrors the kafkajs driver closely.\n */\nexport class ConfluentDriver implements KafkaDriver {\n readonly transactional: boolean;\n private producer: CkProducer | null = null;\n private readonly opts: ConfluentDriverOptions;\n\n constructor(opts: ConfluentDriverOptions) {\n this.opts = opts;\n this.transactional = opts.transactional ?? false;\n if (this.transactional && !opts.transactionalId) {\n throw new Error(\n \"ConfluentDriver: transactionalId is required when transactional=true\",\n );\n }\n }\n\n async connect(): Promise<void> {\n this.producer = await this.createProducer();\n await this.producer.connect();\n }\n\n /**\n * Construct the underlying confluent producer. Overridable as a test seam so\n * the send/transaction logic can be exercised without a real broker.\n */\n protected async createProducer(): Promise<CkProducer> {\n const mod = await importConfluent();\n const kafka: CkKafka = new mod.KafkaJS.Kafka({\n kafkaJS: {\n clientId: this.opts.clientId ?? \"eventferry\",\n brokers: this.opts.brokers,\n ssl: this.opts.ssl,\n sasl: this.opts.sasl,\n },\n });\n return kafka.producer({\n kafkaJS: {\n idempotent: this.opts.idempotent ?? true,\n ...(this.transactional\n ? { transactionalId: this.opts.transactionalId }\n : {}),\n },\n });\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n async sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (!this.producer) throw new Error(\"ConfluentDriver not connected\");\n const topicMessages = groupByTopic(messages);\n const acks = this.opts.acks ?? -1;\n const compression = this.opts.compression;\n\n const doSends = async (target: CkProducer | CkTransaction) => {\n for (const tm of topicMessages) {\n await target.send({\n topic: tm.topic,\n messages: tm.messages,\n acks,\n ...(compression && compression !== \"none\" ? { compression } : {}),\n });\n }\n };\n\n if (this.transactional) {\n const txn = await this.producer.transaction();\n try {\n await doSends(txn);\n await txn.commit();\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n await txn.abort().catch(() => undefined);\n return failedResults(messages, err);\n }\n }\n\n try {\n await doSends(this.producer);\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n return failedResults(messages, err);\n }\n }\n}\n\nfunction failedResults(\n messages: PublishableMessage[],\n err: unknown,\n): PublishResult[] {\n const error = err instanceof Error ? err : new Error(String(err));\n const errorKind = classifyConfluentError(err);\n return messages.map((m) => ({\n recordId: m.recordId,\n ok: false,\n error,\n errorKind,\n }));\n}\n\nfunction groupByTopic(messages: PublishableMessage[]) {\n const byTopic = new Map<string, unknown[]>();\n for (const m of messages) {\n const arr = byTopic.get(m.topic) ?? [];\n arr.push({ key: m.key, value: m.value, headers: m.headers });\n byTopic.set(m.topic, arr);\n }\n return [...byTopic.entries()].map(([topic, msgs]) => ({\n topic,\n messages: msgs,\n }));\n}\n\nasync function importConfluent(): Promise<{\n KafkaJS: { Kafka: new (cfg: unknown) => CkKafka };\n}> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"@confluentinc/kafka-javascript\")) as any;\n } catch {\n throw new Error(\n 'Driver \"confluent\" selected but \"@confluentinc/kafka-javascript\" is not installed. Run: npm i @confluentinc/kafka-javascript',\n );\n }\n}\n","import type {\n PublishableMessage,\n Publisher,\n PublishResult,\n} from \"@eventferry/core\";\nimport { ConfluentDriver } from \"./confluent-driver.js\";\nimport type {\n DriverKind,\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\nimport { KafkaJsDriver } from \"./kafkajs-driver.js\";\n\nexport interface KafkaPublisherOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {\n /** Which underlying client to use. Default \"kafkajs\". */\n driver?: DriverKind;\n /**\n * Provide a fully custom driver instance instead of the built-ins.\n * Useful for testing or unsupported clients.\n */\n customDriver?: KafkaDriver;\n}\n\n/**\n * The Publisher the Relay talks to. Wraps a pluggable KafkaDriver and\n * adds dead-letter routing. Works against Kafka and Redpanda identically\n * (Redpanda is Kafka-API compatible).\n */\nexport class KafkaPublisher implements Publisher {\n private readonly driver: KafkaDriver;\n\n constructor(opts: KafkaPublisherOptions) {\n this.driver = opts.customDriver ?? selectDriver(opts);\n }\n\n connect(): Promise<void> {\n return this.driver.connect();\n }\n\n disconnect(): Promise<void> {\n return this.driver.disconnect();\n }\n\n publish(messages: PublishableMessage[]): Promise<PublishResult[]> {\n return this.driver.sendBatch(messages);\n }\n\n /**\n * Send a single dead-lettered message. The message already carries the\n * DLQ topic (the Relay rewrites it), plus the failure reason as a header.\n */\n async publishToDlq(message: PublishableMessage, error: Error): Promise<void> {\n const dlqMessage: PublishableMessage = {\n ...message,\n headers: {\n ...message.headers,\n \"dlq-reason\": error.message,\n \"dlq-original-topic\": message.headers[\"original-topic\"] ?? \"\",\n \"dlq-failed-at\": new Date().toISOString(),\n },\n };\n const [result] = await this.driver.sendBatch([dlqMessage]);\n if (result && !result.ok) {\n throw result.error ?? new Error(\"DLQ publish failed\");\n }\n }\n\n /** Whether the configured driver provides atomic (EOS) batch sends. */\n get transactional(): boolean {\n return this.driver.transactional;\n }\n}\n\nfunction selectDriver(opts: KafkaPublisherOptions): KafkaDriver {\n const kind = opts.driver ?? \"kafkajs\";\n switch (kind) {\n case \"kafkajs\":\n return new KafkaJsDriver(opts);\n case \"confluent\":\n return new ConfluentDriver(opts);\n default:\n throw new Error(`Unknown driver \"${kind}\"`);\n }\n}\n"],"mappings":"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","names":["CODE_TO_KIND","groupByTopic","failedResults"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { PublishableMessage, PublishResult, Publisher } from '@eventferry/core';
1
+ import { PublishableMessage, PublishResult, PublishErrorKind, Publisher } from '@eventferry/core';
2
2
 
3
3
  /**
4
4
  * Low-level driver contract. Each concrete driver (kafkajs, confluent)
@@ -79,6 +79,23 @@ declare class KafkaJsDriver implements KafkaDriver {
79
79
  sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
80
80
  }
81
81
 
82
+ /**
83
+ * Classify a kafkajs producer error into a {@link PublishErrorKind} so the
84
+ * core relay can decide whether to retry, short-circuit to the DLQ, or pause
85
+ * polling.
86
+ *
87
+ * Mapping verified against `kafkajs/src/errors.js` (v2.x). Protocol error
88
+ * codes match the Kafka Protocol error-code registry. Library-specific
89
+ * subclasses (`KafkaJSRequestTimeoutError`, `KafkaJSConnectionError`,
90
+ * `KafkaJSNonRetriableError`) are matched by the `name` property kafkajs
91
+ * sets on its Error subclasses.
92
+ *
93
+ * Unknown errors fall back to `"retriable"` — the safe bias. At worst we
94
+ * retry an error that should have been skipped; in practice we'd rather
95
+ * over-retry than mis-classify a transient blip as terminal.
96
+ */
97
+ declare function classifyKafkajsError(err: unknown): PublishErrorKind;
98
+
82
99
  interface CkProducer {
83
100
  connect(): Promise<void>;
84
101
  disconnect(): Promise<void>;
@@ -112,6 +129,21 @@ declare class ConfluentDriver implements KafkaDriver {
112
129
  sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
113
130
  }
114
131
 
132
+ /**
133
+ * Classify a `@confluentinc/kafka-javascript` (librdkafka) producer error
134
+ * into a {@link PublishErrorKind} so the core relay can decide whether to
135
+ * retry, short-circuit to the DLQ, or pause polling.
136
+ *
137
+ * librdkafka exposes errors as numeric `RD_KAFKA_RESP_ERR_*` codes — negative
138
+ * codes are library-internal (transport, queue-full, ssl), non-negative
139
+ * codes are wire-protocol errors that match the Kafka protocol's error-code
140
+ * registry. The confluent driver surfaces these on `err.code` (alongside
141
+ * an `err.name` for the symbolic form).
142
+ *
143
+ * Unknown errors fall back to `"retriable"` — the safe bias.
144
+ */
145
+ declare function classifyConfluentError(err: unknown): PublishErrorKind;
146
+
115
147
  interface KafkaPublisherOptions extends KafkaConnectionConfig, ProducerBehaviorConfig {
116
148
  /** Which underlying client to use. Default "kafkajs". */
117
149
  driver?: DriverKind;
@@ -141,4 +173,4 @@ declare class KafkaPublisher implements Publisher {
141
173
  get transactional(): boolean;
142
174
  }
143
175
 
144
- export { ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, KafkaPublisher, type KafkaPublisherOptions, type ProducerBehaviorConfig };
176
+ export { ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, KafkaPublisher, type KafkaPublisherOptions, type ProducerBehaviorConfig, classifyConfluentError, classifyKafkajsError };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PublishableMessage, PublishResult, Publisher } from '@eventferry/core';
1
+ import { PublishableMessage, PublishResult, PublishErrorKind, Publisher } from '@eventferry/core';
2
2
 
3
3
  /**
4
4
  * Low-level driver contract. Each concrete driver (kafkajs, confluent)
@@ -79,6 +79,23 @@ declare class KafkaJsDriver implements KafkaDriver {
79
79
  sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
80
80
  }
81
81
 
82
+ /**
83
+ * Classify a kafkajs producer error into a {@link PublishErrorKind} so the
84
+ * core relay can decide whether to retry, short-circuit to the DLQ, or pause
85
+ * polling.
86
+ *
87
+ * Mapping verified against `kafkajs/src/errors.js` (v2.x). Protocol error
88
+ * codes match the Kafka Protocol error-code registry. Library-specific
89
+ * subclasses (`KafkaJSRequestTimeoutError`, `KafkaJSConnectionError`,
90
+ * `KafkaJSNonRetriableError`) are matched by the `name` property kafkajs
91
+ * sets on its Error subclasses.
92
+ *
93
+ * Unknown errors fall back to `"retriable"` — the safe bias. At worst we
94
+ * retry an error that should have been skipped; in practice we'd rather
95
+ * over-retry than mis-classify a transient blip as terminal.
96
+ */
97
+ declare function classifyKafkajsError(err: unknown): PublishErrorKind;
98
+
82
99
  interface CkProducer {
83
100
  connect(): Promise<void>;
84
101
  disconnect(): Promise<void>;
@@ -112,6 +129,21 @@ declare class ConfluentDriver implements KafkaDriver {
112
129
  sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
113
130
  }
114
131
 
132
+ /**
133
+ * Classify a `@confluentinc/kafka-javascript` (librdkafka) producer error
134
+ * into a {@link PublishErrorKind} so the core relay can decide whether to
135
+ * retry, short-circuit to the DLQ, or pause polling.
136
+ *
137
+ * librdkafka exposes errors as numeric `RD_KAFKA_RESP_ERR_*` codes — negative
138
+ * codes are library-internal (transport, queue-full, ssl), non-negative
139
+ * codes are wire-protocol errors that match the Kafka protocol's error-code
140
+ * registry. The confluent driver surfaces these on `err.code` (alongside
141
+ * an `err.name` for the symbolic form).
142
+ *
143
+ * Unknown errors fall back to `"retriable"` — the safe bias.
144
+ */
145
+ declare function classifyConfluentError(err: unknown): PublishErrorKind;
146
+
115
147
  interface KafkaPublisherOptions extends KafkaConnectionConfig, ProducerBehaviorConfig {
116
148
  /** Which underlying client to use. Default "kafkajs". */
117
149
  driver?: DriverKind;
@@ -141,4 +173,4 @@ declare class KafkaPublisher implements Publisher {
141
173
  get transactional(): boolean;
142
174
  }
143
175
 
144
- export { ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, KafkaPublisher, type KafkaPublisherOptions, type ProducerBehaviorConfig };
176
+ export { ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, KafkaPublisher, type KafkaPublisherOptions, type ProducerBehaviorConfig, classifyConfluentError, classifyKafkajsError };
package/dist/index.js CHANGED
@@ -1,3 +1,90 @@
1
+ // src/kafkajs-classifier.ts
2
+ function classifyKafkajsError(err) {
3
+ if (!err || typeof err !== "object") return "retriable";
4
+ const e = err;
5
+ if (e.name === "KafkaJSConnectionError") return "retriable";
6
+ if (e.name === "KafkaJSRequestTimeoutError") return "retriable";
7
+ if (e.name === "KafkaJSNonRetriableError") return "fatal";
8
+ const type = typeof e.type === "string" ? e.type : void 0;
9
+ if (type) {
10
+ if (RETRIABLE_TYPES.has(type)) return "retriable";
11
+ if (POISON_TYPES.has(type)) return "poison";
12
+ if (FATAL_TYPES.has(type)) return "fatal";
13
+ }
14
+ if (typeof e.code === "number") {
15
+ const k = CODE_TO_KIND.get(e.code);
16
+ if (k) return k;
17
+ }
18
+ return "retriable";
19
+ }
20
+ var RETRIABLE_TYPES = /* @__PURE__ */ new Set([
21
+ "NOT_LEADER_FOR_PARTITION",
22
+ "LEADER_NOT_AVAILABLE",
23
+ "UNKNOWN_TOPIC_OR_PARTITION",
24
+ "NETWORK_EXCEPTION",
25
+ "REQUEST_TIMED_OUT",
26
+ "REPLICA_NOT_AVAILABLE",
27
+ "NOT_ENOUGH_REPLICAS",
28
+ "NOT_ENOUGH_REPLICAS_AFTER_APPEND",
29
+ "FENCED_LEADER_EPOCH",
30
+ "UNKNOWN_LEADER_EPOCH",
31
+ "BROKER_NOT_AVAILABLE",
32
+ "COORDINATOR_LOAD_IN_PROGRESS",
33
+ "COORDINATOR_NOT_AVAILABLE"
34
+ ]);
35
+ var POISON_TYPES = /* @__PURE__ */ new Set([
36
+ "CORRUPT_MESSAGE",
37
+ "MESSAGE_TOO_LARGE",
38
+ "INVALID_RECORD",
39
+ "UNSUPPORTED_COMPRESSION_TYPE",
40
+ "INVALID_REQUIRED_ACKS",
41
+ "INVALID_PARTITIONS"
42
+ ]);
43
+ var FATAL_TYPES = /* @__PURE__ */ new Set([
44
+ "INVALID_PRODUCER_EPOCH",
45
+ "PRODUCER_FENCED",
46
+ "TOPIC_AUTHORIZATION_FAILED",
47
+ "CLUSTER_AUTHORIZATION_FAILED",
48
+ "TRANSACTIONAL_ID_AUTHORIZATION_FAILED",
49
+ "SASL_AUTHENTICATION_FAILED",
50
+ "INVALID_TRANSACTION_STATE",
51
+ "UNSUPPORTED_VERSION"
52
+ ]);
53
+ var CODE_TO_KIND = /* @__PURE__ */ new Map([
54
+ [2, "poison"],
55
+ // CORRUPT_MESSAGE
56
+ [3, "retriable"],
57
+ // UNKNOWN_TOPIC_OR_PARTITION
58
+ [5, "retriable"],
59
+ // LEADER_NOT_AVAILABLE
60
+ [6, "retriable"],
61
+ // NOT_LEADER_FOR_PARTITION
62
+ [7, "retriable"],
63
+ // REQUEST_TIMED_OUT
64
+ [9, "retriable"],
65
+ // REPLICA_NOT_AVAILABLE
66
+ [10, "poison"],
67
+ // MESSAGE_TOO_LARGE
68
+ [13, "retriable"],
69
+ // NETWORK_EXCEPTION
70
+ [19, "retriable"],
71
+ // NOT_ENOUGH_REPLICAS
72
+ [29, "fatal"],
73
+ // TOPIC_AUTHORIZATION_FAILED
74
+ [31, "fatal"],
75
+ // CLUSTER_AUTHORIZATION_FAILED
76
+ [47, "fatal"],
77
+ // INVALID_PRODUCER_EPOCH
78
+ [58, "fatal"],
79
+ // SASL_AUTHENTICATION_FAILED
80
+ [74, "retriable"],
81
+ // FENCED_LEADER_EPOCH
82
+ [76, "poison"],
83
+ // UNSUPPORTED_COMPRESSION_TYPE
84
+ [87, "poison"]
85
+ // INVALID_RECORD
86
+ ]);
87
+
1
88
  // src/kafkajs-driver.ts
2
89
  var KafkaJsDriver = class {
3
90
  transactional;
@@ -49,19 +136,27 @@ var KafkaJsDriver = class {
49
136
  return messages.map((m) => ({ recordId: m.recordId, ok: true }));
50
137
  } catch (err) {
51
138
  await txn.abort().catch(() => void 0);
52
- const error = err instanceof Error ? err : new Error(String(err));
53
- return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));
139
+ return failedResults(messages, err);
54
140
  }
55
141
  }
56
142
  try {
57
143
  await this.producer.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });
58
144
  return messages.map((m) => ({ recordId: m.recordId, ok: true }));
59
145
  } catch (err) {
60
- const error = err instanceof Error ? err : new Error(String(err));
61
- return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));
146
+ return failedResults(messages, err);
62
147
  }
63
148
  }
64
149
  };
150
+ function failedResults(messages, err) {
151
+ const error = err instanceof Error ? err : new Error(String(err));
152
+ const errorKind = classifyKafkajsError(err);
153
+ return messages.map((m) => ({
154
+ recordId: m.recordId,
155
+ ok: false,
156
+ error,
157
+ errorKind
158
+ }));
159
+ }
65
160
  function groupByTopic(messages, compression) {
66
161
  const byTopic = /* @__PURE__ */ new Map();
67
162
  for (const m of messages) {
@@ -89,6 +184,104 @@ async function importKafkaJs() {
89
184
  }
90
185
  }
91
186
 
187
+ // src/confluent-classifier.ts
188
+ function classifyConfluentError(err) {
189
+ if (!err || typeof err !== "object") return "retriable";
190
+ const e = err;
191
+ if (typeof e.code === "number") {
192
+ const k = CODE_TO_KIND2.get(e.code);
193
+ if (k) return k;
194
+ }
195
+ if (typeof e.name === "string") {
196
+ const k = NAME_TO_KIND.get(e.name);
197
+ if (k) return k;
198
+ }
199
+ return "retriable";
200
+ }
201
+ var CODE_TO_KIND2 = /* @__PURE__ */ new Map([
202
+ // Library-internal (negative codes)
203
+ [-184, "backpressure"],
204
+ // ERR__QUEUE_FULL — our outbound buffer is full
205
+ [-185, "retriable"],
206
+ // ERR__TIMED_OUT
207
+ [-187, "retriable"],
208
+ // ERR__ALL_BROKERS_DOWN
209
+ [-188, "poison"],
210
+ // ERR__UNKNOWN_TOPIC — topic doesn't exist on broker
211
+ [-190, "poison"],
212
+ // ERR__UNKNOWN_PARTITION
213
+ [-192, "retriable"],
214
+ // ERR__MSG_TIMED_OUT
215
+ [-195, "retriable"],
216
+ // ERR__TRANSPORT
217
+ [-198, "poison"],
218
+ // ERR__BAD_COMPRESSION
219
+ [-144, "fatal"],
220
+ // ERR__FENCED — producer fenced by another with same txn id
221
+ [-150, "fatal"],
222
+ // ERR__FATAL — unrecoverable librdkafka error
223
+ [-169, "fatal"],
224
+ // ERR__AUTHENTICATION
225
+ [-181, "fatal"],
226
+ // ERR__SSL
227
+ [-196, "retriable"],
228
+ // ERR__FAIL — catch-all, safe-default to retriable
229
+ // Wire-protocol (non-negative codes — Kafka error-code registry)
230
+ [2, "poison"],
231
+ // CORRUPT_MESSAGE
232
+ [3, "retriable"],
233
+ // UNKNOWN_TOPIC_OR_PARTITION
234
+ [5, "retriable"],
235
+ // LEADER_NOT_AVAILABLE
236
+ [6, "retriable"],
237
+ // NOT_LEADER_FOR_PARTITION
238
+ [7, "retriable"],
239
+ // REQUEST_TIMED_OUT
240
+ [9, "retriable"],
241
+ // REPLICA_NOT_AVAILABLE
242
+ [10, "poison"],
243
+ // MESSAGE_TOO_LARGE
244
+ [13, "retriable"],
245
+ // NETWORK_EXCEPTION
246
+ [19, "retriable"],
247
+ // NOT_ENOUGH_REPLICAS
248
+ [29, "fatal"],
249
+ // TOPIC_AUTHORIZATION_FAILED
250
+ [31, "fatal"],
251
+ // CLUSTER_AUTHORIZATION_FAILED
252
+ [47, "fatal"],
253
+ // INVALID_PRODUCER_EPOCH
254
+ [58, "fatal"],
255
+ // SASL_AUTHENTICATION_FAILED
256
+ [74, "retriable"],
257
+ // FENCED_LEADER_EPOCH
258
+ [76, "poison"],
259
+ // UNSUPPORTED_COMPRESSION_TYPE
260
+ [87, "poison"],
261
+ // INVALID_RECORD
262
+ [89, "quota"]
263
+ // THROTTLING_QUOTA_EXCEEDED
264
+ ]);
265
+ var NAME_TO_KIND = /* @__PURE__ */ new Map([
266
+ ["ERR__QUEUE_FULL", "backpressure"],
267
+ ["ERR__FENCED", "fatal"],
268
+ ["ERR__FATAL", "fatal"],
269
+ ["ERR__AUTHENTICATION", "fatal"],
270
+ ["ERR__SSL", "fatal"],
271
+ ["ERR__UNKNOWN_TOPIC", "poison"],
272
+ ["ERR__UNKNOWN_PARTITION", "poison"],
273
+ ["ERR__BAD_COMPRESSION", "poison"],
274
+ ["ERR_TOPIC_AUTHORIZATION_FAILED", "fatal"],
275
+ ["ERR_CLUSTER_AUTHORIZATION_FAILED", "fatal"],
276
+ ["ERR_INVALID_PRODUCER_EPOCH", "fatal"],
277
+ ["ERR_SASL_AUTHENTICATION_FAILED", "fatal"],
278
+ ["ERR_CORRUPT_MESSAGE", "poison"],
279
+ ["ERR_MSG_SIZE_TOO_LARGE", "poison"],
280
+ ["ERR_INVALID_RECORD", "poison"],
281
+ ["ERR_UNSUPPORTED_COMPRESSION_TYPE", "poison"],
282
+ ["ERR_THROTTLING_QUOTA_EXCEEDED", "quota"]
283
+ ]);
284
+
92
285
  // src/confluent-driver.ts
93
286
  var ConfluentDriver = class {
94
287
  transactional;
@@ -155,19 +348,27 @@ var ConfluentDriver = class {
155
348
  return messages.map((m) => ({ recordId: m.recordId, ok: true }));
156
349
  } catch (err) {
157
350
  await txn.abort().catch(() => void 0);
158
- const error = err instanceof Error ? err : new Error(String(err));
159
- return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));
351
+ return failedResults2(messages, err);
160
352
  }
161
353
  }
162
354
  try {
163
355
  await doSends(this.producer);
164
356
  return messages.map((m) => ({ recordId: m.recordId, ok: true }));
165
357
  } catch (err) {
166
- const error = err instanceof Error ? err : new Error(String(err));
167
- return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));
358
+ return failedResults2(messages, err);
168
359
  }
169
360
  }
170
361
  };
362
+ function failedResults2(messages, err) {
363
+ const error = err instanceof Error ? err : new Error(String(err));
364
+ const errorKind = classifyConfluentError(err);
365
+ return messages.map((m) => ({
366
+ recordId: m.recordId,
367
+ ok: false,
368
+ error,
369
+ errorKind
370
+ }));
371
+ }
171
372
  function groupByTopic2(messages) {
172
373
  const byTopic = /* @__PURE__ */ new Map();
173
374
  for (const m of messages) {
@@ -243,6 +444,8 @@ function selectDriver(opts) {
243
444
  export {
244
445
  ConfluentDriver,
245
446
  KafkaJsDriver,
246
- KafkaPublisher
447
+ KafkaPublisher,
448
+ classifyConfluentError,
449
+ classifyKafkajsError
247
450
  };
248
451
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/kafkajs-driver.ts","../src/confluent-driver.ts","../src/publisher.ts"],"sourcesContent":["import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\n\n// Loosely-typed structural references to the kafkajs API so this file\n// compiles without kafkajs installed (it's an optional peer dep).\ninterface KjsProducer {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n // kafkajs: sendBatch takes { topicMessages }, send takes a single { topic, messages }.\n sendBatch(args: unknown): Promise<unknown>;\n transaction(): Promise<KjsTransaction>;\n}\ninterface KjsTransaction {\n sendBatch(args: unknown): Promise<unknown>;\n commit(): Promise<void>;\n abort(): Promise<void>;\n}\ninterface KjsKafka {\n producer(args?: unknown): KjsProducer;\n}\n\nexport interface KafkaJsDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {}\n\n/**\n * Driver backed by the pure-JS `kafkajs` client. Simple, zero native deps.\n */\nexport class KafkaJsDriver implements KafkaDriver {\n readonly transactional: boolean;\n private producer: KjsProducer | null = null;\n private readonly opts: KafkaJsDriverOptions;\n\n constructor(opts: KafkaJsDriverOptions) {\n this.opts = opts;\n this.transactional = opts.transactional ?? false;\n if (this.transactional && !opts.transactionalId) {\n throw new Error(\n \"KafkaJsDriver: transactionalId is required when transactional=true\",\n );\n }\n }\n\n async connect(): Promise<void> {\n this.producer = await this.createProducer();\n await this.producer.connect();\n }\n\n /**\n * Construct the underlying kafkajs producer. Overridable as a test seam so\n * the send/transaction logic can be exercised without a real broker.\n */\n protected async createProducer(): Promise<KjsProducer> {\n const mod = await importKafkaJs();\n const kafka: KjsKafka = new mod.Kafka({\n clientId: this.opts.clientId ?? \"eventferry\",\n brokers: this.opts.brokers,\n ssl: this.opts.ssl,\n sasl: this.opts.sasl,\n });\n return kafka.producer({\n idempotent: this.opts.idempotent ?? true,\n maxInFlightRequests: this.transactional ? 1 : undefined,\n transactionalId: this.transactional ? this.opts.transactionalId : undefined,\n });\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n async sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (!this.producer) throw new Error(\"KafkaJsDriver not connected\");\n const topicMessages = groupByTopic(messages, this.opts.compression);\n\n if (this.transactional) {\n const txn = await this.producer.transaction();\n try {\n await txn.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });\n await txn.commit();\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n await txn.abort().catch(() => undefined);\n const error = err instanceof Error ? err : new Error(String(err));\n return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));\n }\n }\n\n try {\n await this.producer.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));\n }\n }\n}\n\nfunction groupByTopic(messages: PublishableMessage[], compression?: string) {\n const byTopic = new Map<string, unknown[]>();\n for (const m of messages) {\n const arr = byTopic.get(m.topic) ?? [];\n arr.push({\n key: m.key,\n value: m.value,\n headers: m.headers,\n });\n byTopic.set(m.topic, arr);\n }\n return [...byTopic.entries()].map(([topic, msgs]) => ({\n topic,\n messages: msgs,\n ...(compression && compression !== \"none\" ? { compression } : {}),\n }));\n}\n\nasync function importKafkaJs(): Promise<{ Kafka: new (cfg: unknown) => KjsKafka }> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"kafkajs\")) as any;\n } catch {\n throw new Error(\n 'Driver \"kafkajs\" selected but the \"kafkajs\" package is not installed. Run: npm i kafkajs',\n );\n }\n}\n","import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\n\n// Structural typing of the confluent KafkaJS-compatible API surface so this\n// file compiles without the optional native dep installed.\ninterface CkProducer {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n send(args: unknown): Promise<unknown>;\n transaction(): Promise<CkTransaction>;\n}\ninterface CkTransaction {\n send(args: unknown): Promise<unknown>;\n commit(): Promise<void>;\n abort(): Promise<void>;\n}\ninterface CkKafka {\n producer(args?: unknown): CkProducer;\n}\n\nexport interface ConfluentDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {}\n\n/**\n * Driver backed by `@confluentinc/kafka-javascript` (librdkafka wrapper).\n * Higher throughput; uses the KafkaJS-compatible promisified API so the\n * adapter mirrors the kafkajs driver closely.\n */\nexport class ConfluentDriver implements KafkaDriver {\n readonly transactional: boolean;\n private producer: CkProducer | null = null;\n private readonly opts: ConfluentDriverOptions;\n\n constructor(opts: ConfluentDriverOptions) {\n this.opts = opts;\n this.transactional = opts.transactional ?? false;\n if (this.transactional && !opts.transactionalId) {\n throw new Error(\n \"ConfluentDriver: transactionalId is required when transactional=true\",\n );\n }\n }\n\n async connect(): Promise<void> {\n this.producer = await this.createProducer();\n await this.producer.connect();\n }\n\n /**\n * Construct the underlying confluent producer. Overridable as a test seam so\n * the send/transaction logic can be exercised without a real broker.\n */\n protected async createProducer(): Promise<CkProducer> {\n const mod = await importConfluent();\n const kafka: CkKafka = new mod.KafkaJS.Kafka({\n kafkaJS: {\n clientId: this.opts.clientId ?? \"eventferry\",\n brokers: this.opts.brokers,\n ssl: this.opts.ssl,\n sasl: this.opts.sasl,\n },\n });\n return kafka.producer({\n kafkaJS: {\n idempotent: this.opts.idempotent ?? true,\n ...(this.transactional\n ? { transactionalId: this.opts.transactionalId }\n : {}),\n },\n });\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n async sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (!this.producer) throw new Error(\"ConfluentDriver not connected\");\n const topicMessages = groupByTopic(messages);\n const acks = this.opts.acks ?? -1;\n const compression = this.opts.compression;\n\n const doSends = async (target: CkProducer | CkTransaction) => {\n for (const tm of topicMessages) {\n await target.send({\n topic: tm.topic,\n messages: tm.messages,\n acks,\n ...(compression && compression !== \"none\" ? { compression } : {}),\n });\n }\n };\n\n if (this.transactional) {\n const txn = await this.producer.transaction();\n try {\n await doSends(txn);\n await txn.commit();\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n await txn.abort().catch(() => undefined);\n const error = err instanceof Error ? err : new Error(String(err));\n return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));\n }\n }\n\n try {\n await doSends(this.producer);\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n return messages.map((m) => ({ recordId: m.recordId, ok: false, error }));\n }\n }\n}\n\nfunction groupByTopic(messages: PublishableMessage[]) {\n const byTopic = new Map<string, unknown[]>();\n for (const m of messages) {\n const arr = byTopic.get(m.topic) ?? [];\n arr.push({ key: m.key, value: m.value, headers: m.headers });\n byTopic.set(m.topic, arr);\n }\n return [...byTopic.entries()].map(([topic, msgs]) => ({\n topic,\n messages: msgs,\n }));\n}\n\nasync function importConfluent(): Promise<{\n KafkaJS: { Kafka: new (cfg: unknown) => CkKafka };\n}> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"@confluentinc/kafka-javascript\")) as any;\n } catch {\n throw new Error(\n 'Driver \"confluent\" selected but \"@confluentinc/kafka-javascript\" is not installed. Run: npm i @confluentinc/kafka-javascript',\n );\n }\n}\n","import type {\n PublishableMessage,\n Publisher,\n PublishResult,\n} from \"@eventferry/core\";\nimport { ConfluentDriver } from \"./confluent-driver.js\";\nimport type {\n DriverKind,\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\nimport { KafkaJsDriver } from \"./kafkajs-driver.js\";\n\nexport interface KafkaPublisherOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {\n /** Which underlying client to use. Default \"kafkajs\". */\n driver?: DriverKind;\n /**\n * Provide a fully custom driver instance instead of the built-ins.\n * Useful for testing or unsupported clients.\n */\n customDriver?: KafkaDriver;\n}\n\n/**\n * The Publisher the Relay talks to. Wraps a pluggable KafkaDriver and\n * adds dead-letter routing. Works against Kafka and Redpanda identically\n * (Redpanda is Kafka-API compatible).\n */\nexport class KafkaPublisher implements Publisher {\n private readonly driver: KafkaDriver;\n\n constructor(opts: KafkaPublisherOptions) {\n this.driver = opts.customDriver ?? selectDriver(opts);\n }\n\n connect(): Promise<void> {\n return this.driver.connect();\n }\n\n disconnect(): Promise<void> {\n return this.driver.disconnect();\n }\n\n publish(messages: PublishableMessage[]): Promise<PublishResult[]> {\n return this.driver.sendBatch(messages);\n }\n\n /**\n * Send a single dead-lettered message. The message already carries the\n * DLQ topic (the Relay rewrites it), plus the failure reason as a header.\n */\n async publishToDlq(message: PublishableMessage, error: Error): Promise<void> {\n const dlqMessage: PublishableMessage = {\n ...message,\n headers: {\n ...message.headers,\n \"dlq-reason\": error.message,\n \"dlq-original-topic\": message.headers[\"original-topic\"] ?? \"\",\n \"dlq-failed-at\": new Date().toISOString(),\n },\n };\n const [result] = await this.driver.sendBatch([dlqMessage]);\n if (result && !result.ok) {\n throw result.error ?? new Error(\"DLQ publish failed\");\n }\n }\n\n /** Whether the configured driver provides atomic (EOS) batch sends. */\n get transactional(): boolean {\n return this.driver.transactional;\n }\n}\n\nfunction selectDriver(opts: KafkaPublisherOptions): KafkaDriver {\n const kind = opts.driver ?? \"kafkajs\";\n switch (kind) {\n case \"kafkajs\":\n return new KafkaJsDriver(opts);\n case \"confluent\":\n return new ConfluentDriver(opts);\n default:\n throw new Error(`Unknown driver \"${kind}\"`);\n }\n}\n"],"mappings":"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","names":["groupByTopic"]}
1
+ {"version":3,"sources":["../src/kafkajs-classifier.ts","../src/kafkajs-driver.ts","../src/confluent-classifier.ts","../src/confluent-driver.ts","../src/publisher.ts"],"sourcesContent":["import type { PublishErrorKind } from \"@eventferry/core\";\n\n/**\n * Classify a kafkajs producer error into a {@link PublishErrorKind} so the\n * core relay can decide whether to retry, short-circuit to the DLQ, or pause\n * polling.\n *\n * Mapping verified against `kafkajs/src/errors.js` (v2.x). Protocol error\n * codes match the Kafka Protocol error-code registry. Library-specific\n * subclasses (`KafkaJSRequestTimeoutError`, `KafkaJSConnectionError`,\n * `KafkaJSNonRetriableError`) are matched by the `name` property kafkajs\n * sets on its Error subclasses.\n *\n * Unknown errors fall back to `\"retriable\"` — the safe bias. At worst we\n * retry an error that should have been skipped; in practice we'd rather\n * over-retry than mis-classify a transient blip as terminal.\n */\nexport function classifyKafkajsError(err: unknown): PublishErrorKind {\n if (!err || typeof err !== \"object\") return \"retriable\";\n const e = err as { name?: string; type?: string; code?: number };\n\n // Class-based first — these don't carry a protocol error code.\n if (e.name === \"KafkaJSConnectionError\") return \"retriable\";\n if (e.name === \"KafkaJSRequestTimeoutError\") return \"retriable\";\n if (e.name === \"KafkaJSNonRetriableError\") return \"fatal\";\n\n // Protocol error type (string) — kafkajs's KafkaJSProtocolError exposes\n // both `type` (uppercase string) and `code` (number). Use `type` first\n // for readability and fall back to `code` for codes that lack a stable\n // string label.\n const type = typeof e.type === \"string\" ? e.type : undefined;\n if (type) {\n if (RETRIABLE_TYPES.has(type)) return \"retriable\";\n if (POISON_TYPES.has(type)) return \"poison\";\n if (FATAL_TYPES.has(type)) return \"fatal\";\n }\n\n if (typeof e.code === \"number\") {\n const k = CODE_TO_KIND.get(e.code);\n if (k) return k;\n }\n\n return \"retriable\";\n}\n\nconst RETRIABLE_TYPES = new Set<string>([\n \"NOT_LEADER_FOR_PARTITION\",\n \"LEADER_NOT_AVAILABLE\",\n \"UNKNOWN_TOPIC_OR_PARTITION\",\n \"NETWORK_EXCEPTION\",\n \"REQUEST_TIMED_OUT\",\n \"REPLICA_NOT_AVAILABLE\",\n \"NOT_ENOUGH_REPLICAS\",\n \"NOT_ENOUGH_REPLICAS_AFTER_APPEND\",\n \"FENCED_LEADER_EPOCH\",\n \"UNKNOWN_LEADER_EPOCH\",\n \"BROKER_NOT_AVAILABLE\",\n \"COORDINATOR_LOAD_IN_PROGRESS\",\n \"COORDINATOR_NOT_AVAILABLE\",\n]);\n\nconst POISON_TYPES = new Set<string>([\n \"CORRUPT_MESSAGE\",\n \"MESSAGE_TOO_LARGE\",\n \"INVALID_RECORD\",\n \"UNSUPPORTED_COMPRESSION_TYPE\",\n \"INVALID_REQUIRED_ACKS\",\n \"INVALID_PARTITIONS\",\n]);\n\nconst FATAL_TYPES = new Set<string>([\n \"INVALID_PRODUCER_EPOCH\",\n \"PRODUCER_FENCED\",\n \"TOPIC_AUTHORIZATION_FAILED\",\n \"CLUSTER_AUTHORIZATION_FAILED\",\n \"TRANSACTIONAL_ID_AUTHORIZATION_FAILED\",\n \"SASL_AUTHENTICATION_FAILED\",\n \"INVALID_TRANSACTION_STATE\",\n \"UNSUPPORTED_VERSION\",\n]);\n\n/** Numeric fallback for clusters that only return the wire code. */\nconst CODE_TO_KIND: ReadonlyMap<number, PublishErrorKind> = new Map([\n [2, \"poison\"], // CORRUPT_MESSAGE\n [3, \"retriable\"], // UNKNOWN_TOPIC_OR_PARTITION\n [5, \"retriable\"], // LEADER_NOT_AVAILABLE\n [6, \"retriable\"], // NOT_LEADER_FOR_PARTITION\n [7, \"retriable\"], // REQUEST_TIMED_OUT\n [9, \"retriable\"], // REPLICA_NOT_AVAILABLE\n [10, \"poison\"], // MESSAGE_TOO_LARGE\n [13, \"retriable\"], // NETWORK_EXCEPTION\n [19, \"retriable\"], // NOT_ENOUGH_REPLICAS\n [29, \"fatal\"], // TOPIC_AUTHORIZATION_FAILED\n [31, \"fatal\"], // CLUSTER_AUTHORIZATION_FAILED\n [47, \"fatal\"], // INVALID_PRODUCER_EPOCH\n [58, \"fatal\"], // SASL_AUTHENTICATION_FAILED\n [74, \"retriable\"], // FENCED_LEADER_EPOCH\n [76, \"poison\"], // UNSUPPORTED_COMPRESSION_TYPE\n [87, \"poison\"], // INVALID_RECORD\n]);\n","import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport { classifyKafkajsError } from \"./kafkajs-classifier.js\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\n\n// Loosely-typed structural references to the kafkajs API so this file\n// compiles without kafkajs installed (it's an optional peer dep).\ninterface KjsProducer {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n // kafkajs: sendBatch takes { topicMessages }, send takes a single { topic, messages }.\n sendBatch(args: unknown): Promise<unknown>;\n transaction(): Promise<KjsTransaction>;\n}\ninterface KjsTransaction {\n sendBatch(args: unknown): Promise<unknown>;\n commit(): Promise<void>;\n abort(): Promise<void>;\n}\ninterface KjsKafka {\n producer(args?: unknown): KjsProducer;\n}\n\nexport interface KafkaJsDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {}\n\n/**\n * Driver backed by the pure-JS `kafkajs` client. Simple, zero native deps.\n */\nexport class KafkaJsDriver implements KafkaDriver {\n readonly transactional: boolean;\n private producer: KjsProducer | null = null;\n private readonly opts: KafkaJsDriverOptions;\n\n constructor(opts: KafkaJsDriverOptions) {\n this.opts = opts;\n this.transactional = opts.transactional ?? false;\n if (this.transactional && !opts.transactionalId) {\n throw new Error(\n \"KafkaJsDriver: transactionalId is required when transactional=true\",\n );\n }\n }\n\n async connect(): Promise<void> {\n this.producer = await this.createProducer();\n await this.producer.connect();\n }\n\n /**\n * Construct the underlying kafkajs producer. Overridable as a test seam so\n * the send/transaction logic can be exercised without a real broker.\n */\n protected async createProducer(): Promise<KjsProducer> {\n const mod = await importKafkaJs();\n const kafka: KjsKafka = new mod.Kafka({\n clientId: this.opts.clientId ?? \"eventferry\",\n brokers: this.opts.brokers,\n ssl: this.opts.ssl,\n sasl: this.opts.sasl,\n });\n return kafka.producer({\n idempotent: this.opts.idempotent ?? true,\n maxInFlightRequests: this.transactional ? 1 : undefined,\n transactionalId: this.transactional ? this.opts.transactionalId : undefined,\n });\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n async sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (!this.producer) throw new Error(\"KafkaJsDriver not connected\");\n const topicMessages = groupByTopic(messages, this.opts.compression);\n\n if (this.transactional) {\n const txn = await this.producer.transaction();\n try {\n await txn.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });\n await txn.commit();\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n await txn.abort().catch(() => undefined);\n return failedResults(messages, err);\n }\n }\n\n try {\n await this.producer.sendBatch({ topicMessages, acks: this.opts.acks ?? -1 });\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n return failedResults(messages, err);\n }\n }\n}\n\nfunction failedResults(\n messages: PublishableMessage[],\n err: unknown,\n): PublishResult[] {\n const error = err instanceof Error ? err : new Error(String(err));\n const errorKind = classifyKafkajsError(err);\n return messages.map((m) => ({\n recordId: m.recordId,\n ok: false,\n error,\n errorKind,\n }));\n}\n\nfunction groupByTopic(messages: PublishableMessage[], compression?: string) {\n const byTopic = new Map<string, unknown[]>();\n for (const m of messages) {\n const arr = byTopic.get(m.topic) ?? [];\n arr.push({\n key: m.key,\n value: m.value,\n headers: m.headers,\n });\n byTopic.set(m.topic, arr);\n }\n return [...byTopic.entries()].map(([topic, msgs]) => ({\n topic,\n messages: msgs,\n ...(compression && compression !== \"none\" ? { compression } : {}),\n }));\n}\n\nasync function importKafkaJs(): Promise<{ Kafka: new (cfg: unknown) => KjsKafka }> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"kafkajs\")) as any;\n } catch {\n throw new Error(\n 'Driver \"kafkajs\" selected but the \"kafkajs\" package is not installed. Run: npm i kafkajs',\n );\n }\n}\n","import type { PublishErrorKind } from \"@eventferry/core\";\n\n/**\n * Classify a `@confluentinc/kafka-javascript` (librdkafka) producer error\n * into a {@link PublishErrorKind} so the core relay can decide whether to\n * retry, short-circuit to the DLQ, or pause polling.\n *\n * librdkafka exposes errors as numeric `RD_KAFKA_RESP_ERR_*` codes — negative\n * codes are library-internal (transport, queue-full, ssl), non-negative\n * codes are wire-protocol errors that match the Kafka protocol's error-code\n * registry. The confluent driver surfaces these on `err.code` (alongside\n * an `err.name` for the symbolic form).\n *\n * Unknown errors fall back to `\"retriable\"` — the safe bias.\n */\nexport function classifyConfluentError(err: unknown): PublishErrorKind {\n if (!err || typeof err !== \"object\") return \"retriable\";\n const e = err as { code?: number; name?: string };\n\n if (typeof e.code === \"number\") {\n const k = CODE_TO_KIND.get(e.code);\n if (k) return k;\n }\n\n if (typeof e.name === \"string\") {\n const k = NAME_TO_KIND.get(e.name);\n if (k) return k;\n }\n\n return \"retriable\";\n}\n\n/**\n * Authoritative mapping for the most-common librdkafka producer error codes.\n * Sources: `librdkafka/src/rdkafka.h` (`RD_KAFKA_RESP_ERR_*` enum) and the\n * Kafka Protocol error-code registry. Adding a code here is a one-line\n * change — start narrow, broaden as production exposes new codes.\n */\nconst CODE_TO_KIND: ReadonlyMap<number, PublishErrorKind> = new Map([\n // Library-internal (negative codes)\n [-184, \"backpressure\"], // ERR__QUEUE_FULL — our outbound buffer is full\n [-185, \"retriable\"], // ERR__TIMED_OUT\n [-187, \"retriable\"], // ERR__ALL_BROKERS_DOWN\n [-188, \"poison\"], // ERR__UNKNOWN_TOPIC — topic doesn't exist on broker\n [-190, \"poison\"], // ERR__UNKNOWN_PARTITION\n [-192, \"retriable\"], // ERR__MSG_TIMED_OUT\n [-195, \"retriable\"], // ERR__TRANSPORT\n [-198, \"poison\"], // ERR__BAD_COMPRESSION\n [-144, \"fatal\"], // ERR__FENCED — producer fenced by another with same txn id\n [-150, \"fatal\"], // ERR__FATAL — unrecoverable librdkafka error\n [-169, \"fatal\"], // ERR__AUTHENTICATION\n [-181, \"fatal\"], // ERR__SSL\n [-196, \"retriable\"], // ERR__FAIL — catch-all, safe-default to retriable\n\n // Wire-protocol (non-negative codes — Kafka error-code registry)\n [2, \"poison\"], // CORRUPT_MESSAGE\n [3, \"retriable\"], // UNKNOWN_TOPIC_OR_PARTITION\n [5, \"retriable\"], // LEADER_NOT_AVAILABLE\n [6, \"retriable\"], // NOT_LEADER_FOR_PARTITION\n [7, \"retriable\"], // REQUEST_TIMED_OUT\n [9, \"retriable\"], // REPLICA_NOT_AVAILABLE\n [10, \"poison\"], // MESSAGE_TOO_LARGE\n [13, \"retriable\"], // NETWORK_EXCEPTION\n [19, \"retriable\"], // NOT_ENOUGH_REPLICAS\n [29, \"fatal\"], // TOPIC_AUTHORIZATION_FAILED\n [31, \"fatal\"], // CLUSTER_AUTHORIZATION_FAILED\n [47, \"fatal\"], // INVALID_PRODUCER_EPOCH\n [58, \"fatal\"], // SASL_AUTHENTICATION_FAILED\n [74, \"retriable\"], // FENCED_LEADER_EPOCH\n [76, \"poison\"], // UNSUPPORTED_COMPRESSION_TYPE\n [87, \"poison\"], // INVALID_RECORD\n [89, \"quota\"], // THROTTLING_QUOTA_EXCEEDED\n]);\n\n/** Symbolic name fallback for clients that surface `err.name` only. */\nconst NAME_TO_KIND: ReadonlyMap<string, PublishErrorKind> = new Map([\n [\"ERR__QUEUE_FULL\", \"backpressure\"],\n [\"ERR__FENCED\", \"fatal\"],\n [\"ERR__FATAL\", \"fatal\"],\n [\"ERR__AUTHENTICATION\", \"fatal\"],\n [\"ERR__SSL\", \"fatal\"],\n [\"ERR__UNKNOWN_TOPIC\", \"poison\"],\n [\"ERR__UNKNOWN_PARTITION\", \"poison\"],\n [\"ERR__BAD_COMPRESSION\", \"poison\"],\n [\"ERR_TOPIC_AUTHORIZATION_FAILED\", \"fatal\"],\n [\"ERR_CLUSTER_AUTHORIZATION_FAILED\", \"fatal\"],\n [\"ERR_INVALID_PRODUCER_EPOCH\", \"fatal\"],\n [\"ERR_SASL_AUTHENTICATION_FAILED\", \"fatal\"],\n [\"ERR_CORRUPT_MESSAGE\", \"poison\"],\n [\"ERR_MSG_SIZE_TOO_LARGE\", \"poison\"],\n [\"ERR_INVALID_RECORD\", \"poison\"],\n [\"ERR_UNSUPPORTED_COMPRESSION_TYPE\", \"poison\"],\n [\"ERR_THROTTLING_QUOTA_EXCEEDED\", \"quota\"],\n]);\n","import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport { classifyConfluentError } from \"./confluent-classifier.js\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\n\n// Structural typing of the confluent KafkaJS-compatible API surface so this\n// file compiles without the optional native dep installed.\ninterface CkProducer {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n send(args: unknown): Promise<unknown>;\n transaction(): Promise<CkTransaction>;\n}\ninterface CkTransaction {\n send(args: unknown): Promise<unknown>;\n commit(): Promise<void>;\n abort(): Promise<void>;\n}\ninterface CkKafka {\n producer(args?: unknown): CkProducer;\n}\n\nexport interface ConfluentDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {}\n\n/**\n * Driver backed by `@confluentinc/kafka-javascript` (librdkafka wrapper).\n * Higher throughput; uses the KafkaJS-compatible promisified API so the\n * adapter mirrors the kafkajs driver closely.\n */\nexport class ConfluentDriver implements KafkaDriver {\n readonly transactional: boolean;\n private producer: CkProducer | null = null;\n private readonly opts: ConfluentDriverOptions;\n\n constructor(opts: ConfluentDriverOptions) {\n this.opts = opts;\n this.transactional = opts.transactional ?? false;\n if (this.transactional && !opts.transactionalId) {\n throw new Error(\n \"ConfluentDriver: transactionalId is required when transactional=true\",\n );\n }\n }\n\n async connect(): Promise<void> {\n this.producer = await this.createProducer();\n await this.producer.connect();\n }\n\n /**\n * Construct the underlying confluent producer. Overridable as a test seam so\n * the send/transaction logic can be exercised without a real broker.\n */\n protected async createProducer(): Promise<CkProducer> {\n const mod = await importConfluent();\n const kafka: CkKafka = new mod.KafkaJS.Kafka({\n kafkaJS: {\n clientId: this.opts.clientId ?? \"eventferry\",\n brokers: this.opts.brokers,\n ssl: this.opts.ssl,\n sasl: this.opts.sasl,\n },\n });\n return kafka.producer({\n kafkaJS: {\n idempotent: this.opts.idempotent ?? true,\n ...(this.transactional\n ? { transactionalId: this.opts.transactionalId }\n : {}),\n },\n });\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n async sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (!this.producer) throw new Error(\"ConfluentDriver not connected\");\n const topicMessages = groupByTopic(messages);\n const acks = this.opts.acks ?? -1;\n const compression = this.opts.compression;\n\n const doSends = async (target: CkProducer | CkTransaction) => {\n for (const tm of topicMessages) {\n await target.send({\n topic: tm.topic,\n messages: tm.messages,\n acks,\n ...(compression && compression !== \"none\" ? { compression } : {}),\n });\n }\n };\n\n if (this.transactional) {\n const txn = await this.producer.transaction();\n try {\n await doSends(txn);\n await txn.commit();\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n await txn.abort().catch(() => undefined);\n return failedResults(messages, err);\n }\n }\n\n try {\n await doSends(this.producer);\n return messages.map((m) => ({ recordId: m.recordId, ok: true }));\n } catch (err) {\n return failedResults(messages, err);\n }\n }\n}\n\nfunction failedResults(\n messages: PublishableMessage[],\n err: unknown,\n): PublishResult[] {\n const error = err instanceof Error ? err : new Error(String(err));\n const errorKind = classifyConfluentError(err);\n return messages.map((m) => ({\n recordId: m.recordId,\n ok: false,\n error,\n errorKind,\n }));\n}\n\nfunction groupByTopic(messages: PublishableMessage[]) {\n const byTopic = new Map<string, unknown[]>();\n for (const m of messages) {\n const arr = byTopic.get(m.topic) ?? [];\n arr.push({ key: m.key, value: m.value, headers: m.headers });\n byTopic.set(m.topic, arr);\n }\n return [...byTopic.entries()].map(([topic, msgs]) => ({\n topic,\n messages: msgs,\n }));\n}\n\nasync function importConfluent(): Promise<{\n KafkaJS: { Kafka: new (cfg: unknown) => CkKafka };\n}> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"@confluentinc/kafka-javascript\")) as any;\n } catch {\n throw new Error(\n 'Driver \"confluent\" selected but \"@confluentinc/kafka-javascript\" is not installed. Run: npm i @confluentinc/kafka-javascript',\n );\n }\n}\n","import type {\n PublishableMessage,\n Publisher,\n PublishResult,\n} from \"@eventferry/core\";\nimport { ConfluentDriver } from \"./confluent-driver.js\";\nimport type {\n DriverKind,\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\nimport { KafkaJsDriver } from \"./kafkajs-driver.js\";\n\nexport interface KafkaPublisherOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {\n /** Which underlying client to use. Default \"kafkajs\". */\n driver?: DriverKind;\n /**\n * Provide a fully custom driver instance instead of the built-ins.\n * Useful for testing or unsupported clients.\n */\n customDriver?: KafkaDriver;\n}\n\n/**\n * The Publisher the Relay talks to. Wraps a pluggable KafkaDriver and\n * adds dead-letter routing. Works against Kafka and Redpanda identically\n * (Redpanda is Kafka-API compatible).\n */\nexport class KafkaPublisher implements Publisher {\n private readonly driver: KafkaDriver;\n\n constructor(opts: KafkaPublisherOptions) {\n this.driver = opts.customDriver ?? selectDriver(opts);\n }\n\n connect(): Promise<void> {\n return this.driver.connect();\n }\n\n disconnect(): Promise<void> {\n return this.driver.disconnect();\n }\n\n publish(messages: PublishableMessage[]): Promise<PublishResult[]> {\n return this.driver.sendBatch(messages);\n }\n\n /**\n * Send a single dead-lettered message. The message already carries the\n * DLQ topic (the Relay rewrites it), plus the failure reason as a header.\n */\n async publishToDlq(message: PublishableMessage, error: Error): Promise<void> {\n const dlqMessage: PublishableMessage = {\n ...message,\n headers: {\n ...message.headers,\n \"dlq-reason\": error.message,\n \"dlq-original-topic\": message.headers[\"original-topic\"] ?? \"\",\n \"dlq-failed-at\": new Date().toISOString(),\n },\n };\n const [result] = await this.driver.sendBatch([dlqMessage]);\n if (result && !result.ok) {\n throw result.error ?? new Error(\"DLQ publish failed\");\n }\n }\n\n /** Whether the configured driver provides atomic (EOS) batch sends. */\n get transactional(): boolean {\n return this.driver.transactional;\n }\n}\n\nfunction selectDriver(opts: KafkaPublisherOptions): KafkaDriver {\n const kind = opts.driver ?? \"kafkajs\";\n switch (kind) {\n case \"kafkajs\":\n return new KafkaJsDriver(opts);\n case \"confluent\":\n return new ConfluentDriver(opts);\n default:\n throw new Error(`Unknown driver \"${kind}\"`);\n }\n}\n"],"mappings":"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","names":["CODE_TO_KIND","groupByTopic","failedResults"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventferry/kafka",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Kafka/Redpanda publisher for @eventferry (kafkajs + confluent drivers)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -49,7 +49,7 @@
49
49
  "peerDependencies": {
50
50
  "kafkajs": "^2.0.0",
51
51
  "@confluentinc/kafka-javascript": "^1.0.0",
52
- "@eventferry/core": "2.0.0"
52
+ "@eventferry/core": "3.0.0"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "kafkajs": {
@@ -64,7 +64,7 @@
64
64
  "tsup": "^8.3.5",
65
65
  "typescript": "^5.7.2",
66
66
  "vitest": "^2.1.8",
67
- "@eventferry/core": "2.0.0"
67
+ "@eventferry/core": "3.0.0"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "tsup",