@eventferry/kafka 3.3.1 → 3.4.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/CHANGELOG.md +15 -0
- package/README.md +184 -0
- package/dist/consume.cjs +115 -0
- package/dist/consume.cjs.map +1 -0
- package/dist/consume.d.cts +114 -0
- package/dist/consume.d.ts +114 -0
- package/dist/consume.js +88 -0
- package/dist/consume.js.map +1 -0
- package/dist/index.cjs +269 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -1
- package/dist/index.d.ts +218 -1
- package/dist/index.js +269 -7
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/kafkajs-classifier.ts","../src/transactional-id.ts","../src/kafkajs-driver.ts","../src/confluent-classifier.ts","../src/confluent-config.ts","../src/confluent-driver.ts","../src/hooks.ts","../src/tracing.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 \"./confluent-config.js\";\nexport * from \"./hooks.js\";\nexport * from \"./tracing.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","/**\n * Resolve a {@link KafkaConnectionConfig}-style `transactionalId` into the\n * concrete string the underlying driver expects.\n *\n * Accepts:\n * - `string` — used verbatim.\n * - `() => string` — invoked once at connect time.\n * - `() => Promise<string>` — awaited at connect time.\n *\n * Throws when the input is undefined (caller should pre-validate) or when\n * the callable yields an empty string.\n */\nexport async function resolveTransactionalId(\n input: string | (() => string | Promise<string>) | undefined,\n): Promise<string> {\n if (input === undefined) {\n throw new Error(\"transactionalId is required when transactional=true\");\n }\n const raw = typeof input === \"function\" ? await input() : input;\n if (typeof raw !== \"string\" || raw.length === 0) {\n throw new Error(\n \"transactionalId resolver must return a non-empty string\",\n );\n }\n return raw;\n}\n","import type {\n Logger,\n PublishableMessage,\n PublishResult,\n} from \"@eventferry/core\";\nimport { classifyKafkajsError } from \"./kafkajs-classifier.js\";\nimport { resolveTransactionalId } from \"./transactional-id.js\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n KafkaJsPartitionerChoice,\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// kafkajs's `Partitioners` namespace: three factory functions; we pluck them\n// at runtime rather than depending on the kafkajs types.\ninterface KjsPartitionersNamespace {\n DefaultPartitioner: () => unknown;\n LegacyPartitioner: () => unknown;\n JavaCompatiblePartitioner: () => unknown;\n}\n\nexport interface KafkaJsDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {\n /**\n * Optional logger for the driver's own diagnostics (e.g. warnings about\n * unsupported tuning options). When absent the driver falls back to\n * `console.warn` so existing users see the same output.\n */\n logger?: Logger;\n}\n\n/**\n * kafkajs producer-level knobs we expose on the typed API that kafkajs does\n * NOT actually support. On this driver these are warn-and-ignore; users\n * who need them should switch to the confluent driver.\n */\nconst UNSUPPORTED_BY_KAFKAJS = [\n \"lingerMs\",\n \"batchSize\",\n \"deliveryTimeoutMs\",\n \"maxRequestSize\",\n] as const;\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 warnUnsupportedKafkajsOptions(opts);\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 // kafkajs accepts `ssl: tls.ConnectionOptions` directly — Buffer + PEM\n // string both supported. Our TlsConfig is a structural subset of that\n // (`rejectUnauthorized` intentionally omitted; the cluster CA goes via\n // `ca`). No translation needed.\n ssl: this.opts.ssl,\n // SASL: PLAIN / SCRAM-SHA-256 / SCRAM-SHA-512 / OAUTHBEARER. kafkajs's\n // shape matches ours; for OAUTHBEARER kafkajs reads only `value` from\n // the provider's returned token (other fields are ignored).\n sasl: this.opts.sasl,\n });\n const createPartitioner = resolveCreatePartitioner(\n mod.Partitioners,\n this.opts.partitioner,\n this.transactional,\n );\n // Resolve a callable transactionalId — async-safe so runtime context\n // (pod name, AZ index, k8s ordinal) can be derived at connect time.\n const resolvedTxId = this.transactional\n ? await resolveTransactionalId(this.opts.transactionalId)\n : undefined;\n return kafka.producer({\n idempotent: this.opts.idempotent ?? true,\n // Idempotent / transactional producers cap maxInFlight at 5. When the\n // user picks transactional we force 1 to keep strict ordering across\n // retries on classic (non-idempotent) clusters that haven't migrated\n // to the broker-side fence.\n maxInFlightRequests: this.transactional\n ? 1\n : this.opts.maxInFlightRequests,\n transactionalId: resolvedTxId,\n // kafkajs accepts these directly when set; undefined falls through to\n // the kafkajs default.\n requestTimeout: this.opts.requestTimeoutMs,\n transactionTimeout: this.opts.transactionTimeoutMs,\n // Setting any partitioner choice silences kafkajs's\n // KafkaJSPartitionerNotSpecified warning.\n createPartitioner,\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 // Notify the abort hook BEFORE returning failedResults. The hook is\n // best-effort: try/catch around it so a misbehaving hook can't make\n // the abort path itself throw.\n try {\n this.opts.onTransactionAbort?.(error);\n } catch {\n // swallow — already documented as best-effort\n }\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 // Per-message partition override. When set, kafkajs routes the record\n // to this exact partition; when undefined, the configured partitioner\n // chooses. We keep the key here too because compacted topics need it\n // even when partition is pinned.\n ...(m.partition !== undefined ? { partition: m.partition } : {}),\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\n/**\n * Resolve the `createPartitioner` factory kafkajs expects on\n * `producer({...})`. Returns `undefined` to fall through to the kafkajs\n * default when no choice is made AND the producer is non-transactional\n * (transactional producers don't trigger the no-partitioner warning).\n */\nfunction resolveCreatePartitioner(\n partitioners: KjsPartitionersNamespace | undefined,\n choice: KafkaJsPartitionerChoice | undefined,\n transactional: boolean,\n): (() => unknown) | undefined {\n if (!partitioners) return undefined;\n // Default to the java-compatible partitioner when the caller didn't pick.\n // It matches the Java client (murmur2) and silences the noisy warning;\n // for transactional producers we leave the kafkajs default alone since\n // EOS ordering is partitioner-agnostic and the warning doesn't fire there.\n const effective: KafkaJsPartitionerChoice =\n choice ?? (transactional ? \"default\" : \"java-compatible\");\n switch (effective) {\n case \"java-compatible\":\n return partitioners.JavaCompatiblePartitioner;\n case \"legacy\":\n return partitioners.LegacyPartitioner;\n case \"default\":\n return partitioners.DefaultPartitioner;\n }\n}\n\n/** Process-wide dedup so we never warn for the same option twice. */\nconst warnedKafkajsKeys = new Set<string>();\n\nfunction warnUnsupportedKafkajsOptions(opts: KafkaJsDriverOptions): void {\n for (const key of UNSUPPORTED_BY_KAFKAJS) {\n if (opts[key] === undefined) continue;\n if (warnedKafkajsKeys.has(key)) continue;\n warnedKafkajsKeys.add(key);\n const message =\n `'${key}' is not configurable on the kafkajs driver and was ignored. ` +\n `Switch to the confluent driver (driver: \"confluent\") for fine-grained tuning, ` +\n `or remove the option to silence this warning.`;\n // Route through the configured logger when present; otherwise fall back\n // to console.warn so users who never plumbed a logger still see the\n // diagnostic (matches the prior behavior).\n if (opts.logger) {\n opts.logger.warn(`[@eventferry/kafka] ${message}`, { option: key });\n } else {\n console.warn(`[@eventferry/kafka] ${message}`);\n }\n }\n}\n\n/** Internal — used by tests. Resets the dedup so warnings can be observed in isolation. */\nexport function _resetKafkajsWarnDedup(): void {\n warnedKafkajsKeys.clear();\n}\n\nasync function importKafkaJs(): Promise<{\n Kafka: new (cfg: unknown) => KjsKafka;\n Partitioners: KjsPartitionersNamespace;\n}> {\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 {\n KafkaConnectionConfig,\n ProducerBehaviorConfig,\n TlsConfig,\n} from \"./driver.js\";\n\n/**\n * Translate eventferry's normalized `KafkaConnectionConfig` into the shape\n * expected by `@confluentinc/kafka-javascript`'s `KafkaJS.Kafka` constructor.\n *\n * Returns an object with two parts:\n * - `kafkaJS`: the kafkajs-compatible config layer (clientId, brokers, and\n * simple ssl/sasl when no advanced TLS is needed).\n * - top-level keys: librdkafka-style config (e.g. `ssl.ca.pem`,\n * `security.protocol`) used when the user supplies a {@link TlsConfig}.\n *\n * Why a separate translator: the kafkajs-compat layer accepts the simple\n * `ssl: true` boolean but the verified path for mTLS (CA + cert + key) is\n * librdkafka's `ssl.*.pem` keys. The translator picks the right surface\n * based on what the caller supplied. Buffer inputs are coerced to strings —\n * librdkafka accepts PEM strings, NOT Buffers.\n */\nexport interface ConfluentClientConfig {\n kafkaJS: Record<string, unknown>;\n // librdkafka top-level keys; kept as a Record so we can spread them.\n librdkafka: Record<string, unknown>;\n}\n\nexport function buildConfluentClientConfig(\n opts: KafkaConnectionConfig & ProducerBehaviorConfig,\n): ConfluentClientConfig {\n const kafkaJS: Record<string, unknown> = {\n clientId: opts.clientId ?? \"eventferry\",\n brokers: opts.brokers,\n };\n const librdkafka: Record<string, unknown> = {};\n\n // ── Producer tuning passthrough (librdkafka config keys) ─────────────\n if (opts.lingerMs !== undefined) librdkafka[\"linger.ms\"] = opts.lingerMs;\n if (opts.batchSize !== undefined) librdkafka[\"batch.size\"] = opts.batchSize;\n if (opts.maxInFlightRequests !== undefined) {\n librdkafka[\"max.in.flight.requests.per.connection\"] =\n opts.maxInFlightRequests;\n }\n if (opts.requestTimeoutMs !== undefined) {\n librdkafka[\"request.timeout.ms\"] = opts.requestTimeoutMs;\n }\n if (opts.deliveryTimeoutMs !== undefined) {\n librdkafka[\"delivery.timeout.ms\"] = opts.deliveryTimeoutMs;\n }\n if (opts.maxRequestSize !== undefined) {\n librdkafka[\"message.max.bytes\"] = opts.maxRequestSize;\n }\n if (opts.transactionTimeoutMs !== undefined) {\n librdkafka[\"transaction.timeout.ms\"] = opts.transactionTimeoutMs;\n }\n\n const tlsRequested = opts.ssl === true || isTlsConfig(opts.ssl);\n const saslRequested = !!opts.sasl;\n\n if (saslRequested && tlsRequested) {\n librdkafka[\"security.protocol\"] = \"sasl_ssl\";\n } else if (tlsRequested) {\n librdkafka[\"security.protocol\"] = \"ssl\";\n } else if (saslRequested) {\n librdkafka[\"security.protocol\"] = \"sasl_plaintext\";\n } // else: leave as default (plaintext)\n\n if (isTlsConfig(opts.ssl)) {\n // Custom TLS — explicit librdkafka PEM keys. Buffers are coerced to\n // strings (librdkafka does not accept Buffer).\n const tls = opts.ssl;\n if (tls.ca !== undefined) {\n librdkafka[\"ssl.ca.pem\"] = stringifyPem(tls.ca);\n }\n if (tls.cert !== undefined) {\n librdkafka[\"ssl.certificate.pem\"] = stringifyPem(tls.cert);\n }\n if (tls.key !== undefined) {\n librdkafka[\"ssl.key.pem\"] = stringifyPem(tls.key);\n }\n if (tls.passphrase !== undefined) {\n librdkafka[\"ssl.key.password\"] = tls.passphrase;\n }\n // servername (SNI) — librdkafka derives SNI from `ssl.endpoint.identification.algorithm`;\n // explicit SNI override is not documented in the v1.x kafkaJS-compat surface, so we\n // honor it as a no-op for now and document the limitation in the gap analysis.\n } else if (opts.ssl === true) {\n // Simple TLS — kafkajs-compat boolean is sufficient.\n kafkaJS[\"ssl\"] = true;\n }\n\n if (opts.sasl) {\n // SASL — kafkajs-compat shape works for both password mechanisms and\n // OAUTHBEARER (the confluent client implements the provider callback).\n kafkaJS[\"sasl\"] = opts.sasl;\n }\n\n return { kafkaJS, librdkafka };\n}\n\nfunction isTlsConfig(v: unknown): v is TlsConfig {\n return typeof v === \"object\" && v !== null;\n}\n\nfunction stringifyPem(input: string | Buffer | Array<string | Buffer>): string {\n if (Array.isArray(input)) {\n return input.map((x) => (typeof x === \"string\" ? x : x.toString(\"utf8\"))).join(\"\\n\");\n }\n return typeof input === \"string\" ? input : input.toString(\"utf8\");\n}\n","import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport { classifyConfluentError } from \"./confluent-classifier.js\";\nimport { buildConfluentClientConfig } from \"./confluent-config.js\";\nimport { resolveTransactionalId } from \"./transactional-id.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 { kafkaJS, librdkafka } = buildConfluentClientConfig(this.opts);\n const kafka: CkKafka = new mod.KafkaJS.Kafka({\n kafkaJS,\n ...librdkafka,\n });\n // Resolve a callable transactionalId — async-safe so runtime context\n // (pod name, AZ index, k8s ordinal) can be derived at connect time.\n const resolvedTxId = this.transactional\n ? await resolveTransactionalId(this.opts.transactionalId)\n : undefined;\n return kafka.producer({\n kafkaJS: {\n idempotent: this.opts.idempotent ?? true,\n ...(resolvedTxId ? { transactionalId: resolvedTxId } : {}),\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 try {\n this.opts.onTransactionAbort?.(error);\n } catch {\n // swallow — abort hook is best-effort\n }\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({\n key: m.key,\n value: m.value,\n headers: m.headers,\n // Per-message partition override. librdkafka honors an explicit\n // partition value; undefined leaves the default partitioner in charge.\n ...(m.partition !== undefined ? { partition: m.partition } : {}),\n });\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 { PublishableMessage, PublishResult, Logger } from \"@eventferry/core\";\n\n/**\n * Lifecycle hooks fired by `KafkaPublisher`. Every hook is optional. The\n * publisher wraps each invocation in a try/catch and logs (via the\n * configured logger) on failure — a misbehaving hook will NEVER break\n * publishing.\n *\n * Typical wiring:\n * - Custom observability stacks (Datadog APM, New Relic) → `onPublish`,\n * `onError`, `onTransactionAbort`.\n * - Connection-aware readiness probes → `onConnect` / `onDisconnect`.\n * - Audit logs of every published record → `onPublish`.\n */\nexport interface KafkaPublisherHooks {\n /** Fires after the underlying client successfully connects. */\n onConnect?(): void | Promise<void>;\n /** Fires after the underlying client disconnects (clean shutdown). */\n onDisconnect?(): void | Promise<void>;\n /**\n * Fires once per record after a publish attempt — both successes and\n * failures. The `result.ok` flag distinguishes them.\n */\n onPublish?(\n result: PublishResult,\n message: PublishableMessage,\n ): void | Promise<void>;\n /**\n * Fires for any error surfaced from the publish path — driver-thrown\n * errors, transaction abort errors, etc. `message` is set when the error\n * is per-record; absent for batch-level errors (e.g. connect failure).\n */\n onError?(\n error: Error,\n message?: PublishableMessage,\n ): void | Promise<void>;\n /**\n * Fires when a transactional sendBatch's inner abort path is taken.\n * Useful for observability dashboards that track EOS failure rates.\n */\n onTransactionAbort?(error: Error): void | Promise<void>;\n}\n\n/**\n * Invoke a hook safely. Never throws back into the caller — logs the hook's\n * failure via the configured logger (or no-op when logger is absent).\n */\nexport async function safeHook(\n logger: Logger | undefined,\n hookName: keyof KafkaPublisherHooks,\n invoke: () => void | Promise<void> | undefined,\n): Promise<void> {\n try {\n const r = invoke();\n if (r && typeof (r as Promise<void>).then === \"function\") {\n await r;\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n logger?.warn(`[@eventferry/kafka] hook ${hookName} threw; ignored`, {\n error: error.message,\n });\n }\n}\n","/**\n * Tracing surface for the publisher.\n *\n * eventferry deliberately does not depend on `@opentelemetry/api` — instead\n * users wire a thin adapter over their tracing system (OpenTelemetry,\n * Datadog, internal, …). This file defines the minimal contract; an\n * OpenTelemetry adapter is ~10 lines (see the README).\n *\n * The contract follows the **current stable** OpenTelemetry messaging\n * semantic conventions\n * ({@link https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/kafka.md spec}):\n *\n * - Span name: `\"{topic} publish\"`\n * - `SpanKind.PRODUCER`\n * - Required attributes: `messaging.system=kafka`,\n * `messaging.operation.type=publish`, `messaging.destination.name=<topic>`\n * - Recommended: `messaging.batch.message_count`, `messaging.kafka.partition`,\n * `server.address`, `server.port`\n * - One span per batch (NOT per message — per-message spans cause\n * cardinality explosion and the spec actively warns against this)\n */\n\n/** Attribute values the spec allows. */\nexport type SpanAttributeValue = string | number | boolean;\n\n/**\n * Minimal span surface the publisher needs. Implementations wrap a\n * tracing-system-specific span; methods MUST never throw out of the\n * publisher's hot path (wrap your own SDK calls in try/catch).\n */\nexport interface SpanLike {\n setAttribute(key: string, value: SpanAttributeValue): void;\n setAttributes(attrs: Record<string, SpanAttributeValue>): void;\n /** OK on success; ERROR on failure. The `message` is the error message. */\n setStatus(status: { code: \"ok\" | \"error\"; message?: string }): void;\n /** Attach an exception to the span (OpenTelemetry `recordException`). */\n recordException(error: Error): void;\n end(): void;\n}\n\n/**\n * Factory the publisher calls once per `sendBatch` to start a span.\n * Implementations MUST set `SpanKind.PRODUCER` and the messaging semconv\n * attributes on the returned span before returning it.\n */\nexport interface KafkaTracer {\n /**\n * Start a publish span.\n * @param name Recommended format: `\"{topic} publish\"`.\n * @param attributes Initial attributes (the publisher supplies the messaging\n * semconv set: system, destination.name, operation.type,\n * batch.message_count, plus optional kafka.partition and\n * server.address/port).\n */\n startPublishSpan(\n name: string,\n attributes: Record<string, SpanAttributeValue>,\n ): SpanLike;\n}\n\n/**\n * No-op tracer. Used when the user does not configure one. Cheap allocation\n * — never touches I/O.\n */\nexport class NoopKafkaTracer implements KafkaTracer {\n startPublishSpan(): SpanLike {\n return NOOP_SPAN;\n }\n}\n\nconst NOOP_SPAN: SpanLike = {\n setAttribute() {},\n setAttributes() {},\n setStatus() {},\n recordException() {},\n end() {},\n};\n","import type {\n Logger,\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\";\nimport { safeHook } from \"./hooks.js\";\nimport type { KafkaPublisherHooks } from \"./hooks.js\";\nimport { NoopKafkaTracer } from \"./tracing.js\";\nimport type { KafkaTracer, SpanLike } from \"./tracing.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 * Optional structured logger. When set, the publisher routes its own\n * diagnostics (driver warnings, hook failures) through it. When omitted,\n * the publisher is silent — only the underlying drivers may still log.\n */\n logger?: Logger;\n /**\n * Optional lifecycle hooks. Every hook is invoked safely (try/catch +\n * logged via `logger`) and a misbehaving hook will never break publishing.\n */\n hooks?: KafkaPublisherHooks;\n /**\n * Optional tracer. When set, `publish()` wraps each batch in a span that\n * follows the current stable OpenTelemetry messaging semantic conventions.\n * Use a thin adapter over your tracing SDK (see {@link KafkaTracer}).\n */\n tracer?: KafkaTracer;\n}\n\n/**\n * The Publisher the Relay talks to. Wraps a pluggable KafkaDriver and adds\n * dead-letter routing, observability hooks, and OpenTelemetry-shaped publish\n * spans. Works against Kafka and Redpanda identically (Redpanda is Kafka-API\n * compatible).\n */\nexport class KafkaPublisher implements Publisher {\n private readonly driver: KafkaDriver;\n private readonly logger: Logger | undefined;\n private readonly hooks: KafkaPublisherHooks;\n private readonly tracer: KafkaTracer;\n\n constructor(opts: KafkaPublisherOptions) {\n this.logger = opts.logger;\n this.hooks = opts.hooks ?? {};\n this.tracer = opts.tracer ?? new NoopKafkaTracer();\n // Plumb the logger into driver construction so driver-side diagnostics\n // (e.g. kafkajs unsupported-tuning warnings) route through it too.\n // Plumb a safe-wrapped onTransactionAbort callback so the driver-level\n // transaction abort path fans out to the user-supplied hook safely.\n const onTransactionAbort = this.hooks.onTransactionAbort\n ? (error: Error) => {\n void safeHook(this.logger, \"onTransactionAbort\", () =>\n this.hooks.onTransactionAbort?.(error),\n );\n }\n : undefined;\n this.driver =\n opts.customDriver ?? selectDriver({ ...opts, onTransactionAbort });\n }\n\n async connect(): Promise<void> {\n await this.driver.connect();\n await safeHook(this.logger, \"onConnect\", () => this.hooks.onConnect?.());\n }\n\n async disconnect(): Promise<void> {\n await this.driver.disconnect();\n await safeHook(this.logger, \"onDisconnect\", () =>\n this.hooks.onDisconnect?.(),\n );\n }\n\n async publish(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (messages.length === 0) return [];\n\n const span = this.startBatchSpan(messages);\n let results: PublishResult[];\n try {\n results = await this.driver.sendBatch(messages);\n } catch (err) {\n // Driver-level throw — every record is a failure attributed to the\n // batch-level error. Record on the span, fire hook, rethrow.\n const error = err instanceof Error ? err : new Error(String(err));\n span.setStatus({ code: \"error\", message: error.message });\n span.recordException(error);\n span.end();\n await safeHook(this.logger, \"onError\", () => this.hooks.onError?.(error));\n throw err;\n }\n\n // Per-record hooks. Walk by index so the original message is available.\n const byId = new Map(messages.map((m) => [m.recordId, m]));\n let allOk = true;\n for (const r of results) {\n const msg = byId.get(r.recordId);\n if (!msg) continue;\n await safeHook(this.logger, \"onPublish\", () =>\n this.hooks.onPublish?.(r, msg),\n );\n if (!r.ok) {\n allOk = false;\n const err = r.error ?? new Error(\"publish failed\");\n await safeHook(this.logger, \"onError\", () =>\n this.hooks.onError?.(err, msg),\n );\n }\n }\n\n span.setStatus(allOk ? { code: \"ok\" } : { code: \"error\" });\n span.end();\n return results;\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 // Error class name (e.g. \"KafkaJSProtocolError\", \"RecordTooLargeException\"),\n // useful for downstream alert routing without parsing the reason string.\n \"dlq-error-class\": error.name || error.constructor?.name || \"Error\",\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 /**\n * Start a span for the batch following the OTel messaging conventions.\n *\n * Multi-topic batches: per the OTel spec, the span name uses the\n * destination — we pick the FIRST topic in the batch and document the\n * limitation. Callers that publish heterogeneous batches and care about\n * per-topic spans should split their batches upstream.\n */\n private startBatchSpan(messages: PublishableMessage[]): SpanLike {\n const topic = messages[0]?.topic ?? \"unknown\";\n return this.tracer.startPublishSpan(`${topic} publish`, {\n \"messaging.system\": \"kafka\",\n \"messaging.operation.type\": \"publish\",\n \"messaging.destination.name\": topic,\n \"messaging.batch.message_count\": messages.length,\n });\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/kafkajs-classifier.ts","../src/transactional-id.ts","../src/kafkajs-driver.ts","../src/confluent-classifier.ts","../src/confluent-config.ts","../src/confluent-driver.ts","../src/hooks.ts","../src/tracing.ts","../src/publisher.ts"],"sourcesContent":["export * from \"./driver.js\";\nexport * from \"./admin.js\";\nexport * from \"./kafkajs-driver.js\";\nexport * from \"./kafkajs-classifier.js\";\nexport * from \"./confluent-driver.js\";\nexport * from \"./confluent-classifier.js\";\nexport * from \"./confluent-config.js\";\nexport * from \"./hooks.js\";\nexport * from \"./tracing.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","/**\n * Resolve a {@link KafkaConnectionConfig}-style `transactionalId` into the\n * concrete string the underlying driver expects.\n *\n * Accepts:\n * - `string` — used verbatim.\n * - `() => string` — invoked once at connect time.\n * - `() => Promise<string>` — awaited at connect time.\n *\n * Throws when the input is undefined (caller should pre-validate) or when\n * the callable yields an empty string.\n */\nexport async function resolveTransactionalId(\n input: string | (() => string | Promise<string>) | undefined,\n): Promise<string> {\n if (input === undefined) {\n throw new Error(\"transactionalId is required when transactional=true\");\n }\n const raw = typeof input === \"function\" ? await input() : input;\n if (typeof raw !== \"string\" || raw.length === 0) {\n throw new Error(\n \"transactionalId resolver must return a non-empty string\",\n );\n }\n return raw;\n}\n","import type {\n Logger,\n PublishableMessage,\n PublishResult,\n} from \"@eventferry/core\";\nimport { classifyKafkajsError } from \"./kafkajs-classifier.js\";\nimport { resolveTransactionalId } from \"./transactional-id.js\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n KafkaJsPartitionerChoice,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\nimport type {\n KafkaDriverAdmin,\n PartitionGrowSpec,\n TopicCreateSpec,\n TopicMetadata,\n} from \"./admin.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 admin(): KjsAdmin;\n}\n// Minimal structural shape of the kafkajs Admin client — we use only the\n// methods needed for listTopics / describeTopics / createTopics / createPartitions.\n// Errors thrown from these calls bubble up unchanged (admin errors are\n// operator-facing, not relay-facing — no error-kind classification here).\ninterface KjsAdmin {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n listTopics(): Promise<string[]>;\n fetchTopicMetadata(args: {\n topics: string[];\n }): Promise<{\n topics: Array<{\n name: string;\n partitions: Array<{\n partitionId: number;\n leader: number;\n replicas: number[];\n isr: number[];\n }>;\n }>;\n }>;\n createTopics(args: {\n topics: Array<{\n topic: string;\n numPartitions?: number;\n replicationFactor?: number;\n configEntries?: Array<{ name: string; value: string }>;\n }>;\n waitForLeaders?: boolean;\n }): Promise<boolean>;\n createPartitions(args: {\n topicPartitions: Array<{ topic: string; count: number }>;\n }): Promise<void>;\n}\n// kafkajs's `Partitioners` namespace: three factory functions; we pluck them\n// at runtime rather than depending on the kafkajs types.\ninterface KjsPartitionersNamespace {\n DefaultPartitioner: () => unknown;\n LegacyPartitioner: () => unknown;\n JavaCompatiblePartitioner: () => unknown;\n}\n\nexport interface KafkaJsDriverOptions\n extends KafkaConnectionConfig,\n ProducerBehaviorConfig {\n /**\n * Optional logger for the driver's own diagnostics (e.g. warnings about\n * unsupported tuning options). When absent the driver falls back to\n * `console.warn` so existing users see the same output.\n */\n logger?: Logger;\n}\n\n/**\n * kafkajs producer-level knobs we expose on the typed API that kafkajs does\n * NOT actually support. On this driver these are warn-and-ignore; users\n * who need them should switch to the confluent driver.\n */\nconst UNSUPPORTED_BY_KAFKAJS = [\n \"lingerMs\",\n \"batchSize\",\n \"deliveryTimeoutMs\",\n \"maxRequestSize\",\n // Confluent-only escape hatches; ignored on kafkajs.\n \"compressionLevel\",\n \"rawProducerConfig\",\n] as const;\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 warnUnsupportedKafkajsOptions(opts);\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 // kafkajs accepts `ssl: tls.ConnectionOptions` directly — Buffer + PEM\n // string both supported. Our TlsConfig is a structural subset of that\n // (`rejectUnauthorized` intentionally omitted; the cluster CA goes via\n // `ca`). No translation needed.\n ssl: this.opts.ssl,\n // SASL: PLAIN / SCRAM-SHA-256 / SCRAM-SHA-512 / OAUTHBEARER. kafkajs's\n // shape matches ours; for OAUTHBEARER kafkajs reads only `value` from\n // the provider's returned token (other fields are ignored).\n sasl: this.opts.sasl,\n });\n return kafka.producer(await this.buildProducerOptions(mod.Partitioners));\n }\n\n /**\n * Compute the options object passed to `kafka.producer({...})`. Exposed\n * as a test seam so power-user escape hatches (customPartitioner,\n * rawKafkaJsProducerConfig) can be asserted without a live broker.\n */\n protected async buildProducerOptions(\n partitioners: KjsPartitionersNamespace | undefined,\n ): Promise<Record<string, unknown>> {\n // Custom partitioner (escape hatch) wins over the preset `partitioner` choice.\n const createPartitioner =\n this.opts.customPartitioner ??\n resolveCreatePartitioner(\n partitioners,\n this.opts.partitioner,\n this.transactional,\n );\n // Resolve a callable transactionalId — async-safe so runtime context\n // (pod name, AZ index, k8s ordinal) can be derived at connect time.\n const resolvedTxId = this.transactional\n ? await resolveTransactionalId(this.opts.transactionalId)\n : undefined;\n return {\n idempotent: this.opts.idempotent ?? true,\n // Idempotent / transactional producers cap maxInFlight at 5. When the\n // user picks transactional we force 1 to keep strict ordering across\n // retries on classic (non-idempotent) clusters that haven't migrated\n // to the broker-side fence.\n maxInFlightRequests: this.transactional\n ? 1\n : this.opts.maxInFlightRequests,\n transactionalId: resolvedTxId,\n // kafkajs accepts these directly when set; undefined falls through to\n // the kafkajs default.\n requestTimeout: this.opts.requestTimeoutMs,\n transactionTimeout: this.opts.transactionTimeoutMs,\n // Setting any partitioner choice silences kafkajs's\n // KafkaJSPartitionerNotSpecified warning.\n createPartitioner,\n // Power-user escape hatch — merged LAST so raw keys win against the\n // translated ones. That's the contract: anything you put here is\n // final, even if it overrides idempotent/transactionalId/etc.\n ...(this.opts.rawKafkaJsProducerConfig ?? {}),\n };\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n /**\n * Construct a kafkajs admin client wrapped in the eventferry-facing\n * `KafkaDriverAdmin` shape. The publisher calls `.connect()` on the\n * returned object before exposing it via `publisher.admin()`.\n */\n async admin(): Promise<KafkaDriverAdmin> {\n const mod = await importKafkaJs();\n const kafka: KjsKafka = new mod.Kafka({\n clientId: this.opts.clientId ?? \"eventferry-admin\",\n brokers: this.opts.brokers,\n ssl: this.opts.ssl,\n sasl: this.opts.sasl,\n });\n return new KafkaJsAdmin(kafka.admin());\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 // Notify the abort hook BEFORE returning failedResults. The hook is\n // best-effort: try/catch around it so a misbehaving hook can't make\n // the abort path itself throw.\n try {\n this.opts.onTransactionAbort?.(error);\n } catch {\n // swallow — already documented as best-effort\n }\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 // Per-message partition override. When set, kafkajs routes the record\n // to this exact partition; when undefined, the configured partitioner\n // chooses. We keep the key here too because compacted topics need it\n // even when partition is pinned.\n ...(m.partition !== undefined ? { partition: m.partition } : {}),\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\n/**\n * Resolve the `createPartitioner` factory kafkajs expects on\n * `producer({...})`. Returns `undefined` to fall through to the kafkajs\n * default when no choice is made AND the producer is non-transactional\n * (transactional producers don't trigger the no-partitioner warning).\n */\nfunction resolveCreatePartitioner(\n partitioners: KjsPartitionersNamespace | undefined,\n choice: KafkaJsPartitionerChoice | undefined,\n transactional: boolean,\n): (() => unknown) | undefined {\n if (!partitioners) return undefined;\n // Default to the java-compatible partitioner when the caller didn't pick.\n // It matches the Java client (murmur2) and silences the noisy warning;\n // for transactional producers we leave the kafkajs default alone since\n // EOS ordering is partitioner-agnostic and the warning doesn't fire there.\n const effective: KafkaJsPartitionerChoice =\n choice ?? (transactional ? \"default\" : \"java-compatible\");\n switch (effective) {\n case \"java-compatible\":\n return partitioners.JavaCompatiblePartitioner;\n case \"legacy\":\n return partitioners.LegacyPartitioner;\n case \"default\":\n return partitioners.DefaultPartitioner;\n }\n}\n\n/** Process-wide dedup so we never warn for the same option twice. */\nconst warnedKafkajsKeys = new Set<string>();\n\nfunction warnUnsupportedKafkajsOptions(opts: KafkaJsDriverOptions): void {\n for (const key of UNSUPPORTED_BY_KAFKAJS) {\n if (opts[key] === undefined) continue;\n if (warnedKafkajsKeys.has(key)) continue;\n warnedKafkajsKeys.add(key);\n const message =\n `'${key}' is not configurable on the kafkajs driver and was ignored. ` +\n `Switch to the confluent driver (driver: \"confluent\") for fine-grained tuning, ` +\n `or remove the option to silence this warning.`;\n // Route through the configured logger when present; otherwise fall back\n // to console.warn so users who never plumbed a logger still see the\n // diagnostic (matches the prior behavior).\n if (opts.logger) {\n opts.logger.warn(`[@eventferry/kafka] ${message}`, { option: key });\n } else {\n console.warn(`[@eventferry/kafka] ${message}`);\n }\n }\n}\n\n/** Internal — used by tests. Resets the dedup so warnings can be observed in isolation. */\nexport function _resetKafkajsWarnDedup(): void {\n warnedKafkajsKeys.clear();\n}\n\n/**\n * Adapt the kafkajs admin client to the {@link KafkaDriverAdmin} contract.\n * Idempotent semantics for `createTopics` / `createPartitions` are\n * implemented here so the publisher's `ensureTopics` helper stays small.\n */\nclass KafkaJsAdmin implements KafkaDriverAdmin {\n constructor(private readonly client: KjsAdmin) {}\n\n async connect(): Promise<void> {\n await this.client.connect();\n }\n\n async close(): Promise<void> {\n await this.client.disconnect();\n }\n\n async listTopics(): Promise<string[]> {\n return await this.client.listTopics();\n }\n\n async describeTopics(topics: string[]): Promise<TopicMetadata[]> {\n if (topics.length === 0) return [];\n const all = new Set(await this.client.listTopics());\n // Fetch metadata only for the ones that exist; map absent topics to\n // empty-partitions descriptors (matches the documented behavior).\n const existing = topics.filter((t) => all.has(t));\n const missing = topics.filter((t) => !all.has(t));\n const meta = existing.length\n ? await this.client.fetchTopicMetadata({ topics: existing })\n : { topics: [] };\n const byName = new Map(meta.topics.map((t) => [t.name, t]));\n return topics.map((topic) => {\n if (missing.includes(topic)) return { topic, partitions: [] };\n const found = byName.get(topic);\n if (!found) return { topic, partitions: [] };\n return {\n topic,\n partitions: found.partitions.map((p) => ({\n partitionId: p.partitionId,\n leader: p.leader,\n replicas: p.replicas,\n isr: p.isr,\n })),\n };\n });\n }\n\n async createTopics(specs: TopicCreateSpec[]): Promise<void> {\n if (specs.length === 0) return;\n const topics = specs.map((s) => ({\n topic: s.topic,\n numPartitions: s.numPartitions,\n replicationFactor: s.replicationFactor,\n configEntries: s.configEntries\n ? Object.entries(s.configEntries).map(([name, value]) => ({ name, value }))\n : undefined,\n }));\n try {\n await this.client.createTopics({ topics, waitForLeaders: true });\n } catch (err) {\n // kafkajs raises KafkaJSProtocolError with type \"TOPIC_ALREADY_EXISTS\".\n // Idempotent contract: silently swallow that case; surface everything else.\n const e = err as { type?: string; message?: string };\n if (e?.type === \"TOPIC_ALREADY_EXISTS\") return;\n if (/already exists/i.test(e?.message ?? \"\")) return;\n throw err;\n }\n }\n\n async createPartitions(specs: PartitionGrowSpec[]): Promise<void> {\n if (specs.length === 0) return;\n await this.client.createPartitions({\n topicPartitions: specs.map((s) => ({\n topic: s.topic,\n count: s.totalCount,\n })),\n });\n }\n}\n\nasync function importKafkaJs(): Promise<{\n Kafka: new (cfg: unknown) => KjsKafka;\n Partitioners: KjsPartitionersNamespace;\n}> {\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 {\n KafkaConnectionConfig,\n ProducerBehaviorConfig,\n TlsConfig,\n} from \"./driver.js\";\n\n/**\n * Translate eventferry's normalized `KafkaConnectionConfig` into the shape\n * expected by `@confluentinc/kafka-javascript`'s `KafkaJS.Kafka` constructor.\n *\n * Returns an object with two parts:\n * - `kafkaJS`: the kafkajs-compatible config layer (clientId, brokers, and\n * simple ssl/sasl when no advanced TLS is needed).\n * - top-level keys: librdkafka-style config (e.g. `ssl.ca.pem`,\n * `security.protocol`) used when the user supplies a {@link TlsConfig}.\n *\n * Why a separate translator: the kafkajs-compat layer accepts the simple\n * `ssl: true` boolean but the verified path for mTLS (CA + cert + key) is\n * librdkafka's `ssl.*.pem` keys. The translator picks the right surface\n * based on what the caller supplied. Buffer inputs are coerced to strings —\n * librdkafka accepts PEM strings, NOT Buffers.\n */\nexport interface ConfluentClientConfig {\n kafkaJS: Record<string, unknown>;\n // librdkafka top-level keys; kept as a Record so we can spread them.\n librdkafka: Record<string, unknown>;\n}\n\nexport function buildConfluentClientConfig(\n opts: KafkaConnectionConfig & ProducerBehaviorConfig,\n): ConfluentClientConfig {\n const kafkaJS: Record<string, unknown> = {\n clientId: opts.clientId ?? \"eventferry\",\n brokers: opts.brokers,\n };\n const librdkafka: Record<string, unknown> = {};\n\n // ── Producer tuning passthrough (librdkafka config keys) ─────────────\n if (opts.lingerMs !== undefined) librdkafka[\"linger.ms\"] = opts.lingerMs;\n if (opts.batchSize !== undefined) librdkafka[\"batch.size\"] = opts.batchSize;\n if (opts.maxInFlightRequests !== undefined) {\n librdkafka[\"max.in.flight.requests.per.connection\"] =\n opts.maxInFlightRequests;\n }\n if (opts.requestTimeoutMs !== undefined) {\n librdkafka[\"request.timeout.ms\"] = opts.requestTimeoutMs;\n }\n if (opts.deliveryTimeoutMs !== undefined) {\n librdkafka[\"delivery.timeout.ms\"] = opts.deliveryTimeoutMs;\n }\n if (opts.maxRequestSize !== undefined) {\n librdkafka[\"message.max.bytes\"] = opts.maxRequestSize;\n }\n if (opts.transactionTimeoutMs !== undefined) {\n librdkafka[\"transaction.timeout.ms\"] = opts.transactionTimeoutMs;\n }\n if (opts.compressionLevel !== undefined) {\n librdkafka[\"compression.level\"] = opts.compressionLevel;\n }\n\n const tlsRequested = opts.ssl === true || isTlsConfig(opts.ssl);\n const saslRequested = !!opts.sasl;\n\n if (saslRequested && tlsRequested) {\n librdkafka[\"security.protocol\"] = \"sasl_ssl\";\n } else if (tlsRequested) {\n librdkafka[\"security.protocol\"] = \"ssl\";\n } else if (saslRequested) {\n librdkafka[\"security.protocol\"] = \"sasl_plaintext\";\n } // else: leave as default (plaintext)\n\n if (isTlsConfig(opts.ssl)) {\n // Custom TLS — explicit librdkafka PEM keys. Buffers are coerced to\n // strings (librdkafka does not accept Buffer).\n const tls = opts.ssl;\n if (tls.ca !== undefined) {\n librdkafka[\"ssl.ca.pem\"] = stringifyPem(tls.ca);\n }\n if (tls.cert !== undefined) {\n librdkafka[\"ssl.certificate.pem\"] = stringifyPem(tls.cert);\n }\n if (tls.key !== undefined) {\n librdkafka[\"ssl.key.pem\"] = stringifyPem(tls.key);\n }\n if (tls.passphrase !== undefined) {\n librdkafka[\"ssl.key.password\"] = tls.passphrase;\n }\n // servername (SNI) — librdkafka derives SNI from `ssl.endpoint.identification.algorithm`;\n // explicit SNI override is not documented in the v1.x kafkaJS-compat surface, so we\n // honor it as a no-op for now and document the limitation in the gap analysis.\n } else if (opts.ssl === true) {\n // Simple TLS — kafkajs-compat boolean is sufficient.\n kafkaJS[\"ssl\"] = true;\n }\n\n if (opts.sasl) {\n // SASL — kafkajs-compat shape works for both password mechanisms and\n // OAUTHBEARER (the confluent client implements the provider callback).\n kafkaJS[\"sasl\"] = opts.sasl;\n }\n\n // Power-user escape hatch — merged LAST so raw keys win against the\n // translated ones (deliberate; that's the whole point of the hatch).\n if (opts.rawProducerConfig) {\n Object.assign(librdkafka, opts.rawProducerConfig);\n }\n\n return { kafkaJS, librdkafka };\n}\n\nfunction isTlsConfig(v: unknown): v is TlsConfig {\n return typeof v === \"object\" && v !== null;\n}\n\nfunction stringifyPem(input: string | Buffer | Array<string | Buffer>): string {\n if (Array.isArray(input)) {\n return input.map((x) => (typeof x === \"string\" ? x : x.toString(\"utf8\"))).join(\"\\n\");\n }\n return typeof input === \"string\" ? input : input.toString(\"utf8\");\n}\n","import type { PublishableMessage, PublishResult } from \"@eventferry/core\";\nimport { classifyConfluentError } from \"./confluent-classifier.js\";\nimport { buildConfluentClientConfig } from \"./confluent-config.js\";\nimport { resolveTransactionalId } from \"./transactional-id.js\";\nimport type {\n KafkaConnectionConfig,\n KafkaDriver,\n ProducerBehaviorConfig,\n} from \"./driver.js\";\nimport type {\n KafkaDriverAdmin,\n PartitionGrowSpec,\n TopicCreateSpec,\n TopicMetadata,\n} from \"./admin.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 admin(): CkAdmin;\n}\n// Structural shape of the confluent kafkaJS-compat admin client. The\n// surface mirrors kafkajs's so we can reuse the wrapper pattern.\ninterface CkAdmin {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n listTopics(): Promise<string[]>;\n fetchTopicMetadata(args: { topics: string[] }): Promise<{\n topics: Array<{\n name: string;\n partitions: Array<{\n partitionId: number;\n leader: number;\n replicas: number[];\n isr: number[];\n }>;\n }>;\n }>;\n createTopics(args: {\n topics: Array<{\n topic: string;\n numPartitions?: number;\n replicationFactor?: number;\n configEntries?: Array<{ name: string; value: string }>;\n }>;\n waitForLeaders?: boolean;\n }): Promise<boolean>;\n createPartitions(args: {\n topicPartitions: Array<{ topic: string; count: number }>;\n }): Promise<void>;\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 { kafkaJS, librdkafka } = buildConfluentClientConfig(this.opts);\n const kafka: CkKafka = new mod.KafkaJS.Kafka({\n kafkaJS,\n ...librdkafka,\n });\n // Resolve a callable transactionalId — async-safe so runtime context\n // (pod name, AZ index, k8s ordinal) can be derived at connect time.\n const resolvedTxId = this.transactional\n ? await resolveTransactionalId(this.opts.transactionalId)\n : undefined;\n return kafka.producer({\n kafkaJS: {\n idempotent: this.opts.idempotent ?? true,\n ...(resolvedTxId ? { transactionalId: resolvedTxId } : {}),\n },\n });\n }\n\n async disconnect(): Promise<void> {\n await this.producer?.disconnect();\n this.producer = null;\n }\n\n /**\n * Construct a librdkafka-backed admin client wrapped in the eventferry\n * `KafkaDriverAdmin` shape. The publisher's `connect()` is called before\n * the admin reaches the user.\n */\n async admin(): Promise<KafkaDriverAdmin> {\n const mod = await importConfluent();\n const { kafkaJS, librdkafka } = buildConfluentClientConfig(this.opts);\n const kafka: CkKafka = new mod.KafkaJS.Kafka({ kafkaJS, ...librdkafka });\n return new ConfluentAdmin(kafka.admin());\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 try {\n this.opts.onTransactionAbort?.(error);\n } catch {\n // swallow — abort hook is best-effort\n }\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({\n key: m.key,\n value: m.value,\n headers: m.headers,\n // Per-message partition override. librdkafka honors an explicit\n // partition value; undefined leaves the default partitioner in charge.\n ...(m.partition !== undefined ? { partition: m.partition } : {}),\n });\n byTopic.set(m.topic, arr);\n }\n return [...byTopic.entries()].map(([topic, msgs]) => ({\n topic,\n messages: msgs,\n }));\n}\n\n/**\n * Adapt the confluent (librdkafka) admin client to the\n * {@link KafkaDriverAdmin} contract. Idempotent semantics mirror the\n * kafkajs admin wrapper.\n */\nclass ConfluentAdmin implements KafkaDriverAdmin {\n constructor(private readonly client: CkAdmin) {}\n\n async connect(): Promise<void> {\n await this.client.connect();\n }\n\n async close(): Promise<void> {\n await this.client.disconnect();\n }\n\n async listTopics(): Promise<string[]> {\n return await this.client.listTopics();\n }\n\n async describeTopics(topics: string[]): Promise<TopicMetadata[]> {\n if (topics.length === 0) return [];\n const all = new Set(await this.client.listTopics());\n const existing = topics.filter((t) => all.has(t));\n const missing = topics.filter((t) => !all.has(t));\n const meta = existing.length\n ? await this.client.fetchTopicMetadata({ topics: existing })\n : { topics: [] };\n const byName = new Map(meta.topics.map((t) => [t.name, t]));\n return topics.map((topic) => {\n if (missing.includes(topic)) return { topic, partitions: [] };\n const found = byName.get(topic);\n if (!found) return { topic, partitions: [] };\n return {\n topic,\n partitions: found.partitions.map((p) => ({\n partitionId: p.partitionId,\n leader: p.leader,\n replicas: p.replicas,\n isr: p.isr,\n })),\n };\n });\n }\n\n async createTopics(specs: TopicCreateSpec[]): Promise<void> {\n if (specs.length === 0) return;\n const topics = specs.map((s) => ({\n topic: s.topic,\n numPartitions: s.numPartitions,\n replicationFactor: s.replicationFactor,\n configEntries: s.configEntries\n ? Object.entries(s.configEntries).map(([name, value]) => ({ name, value }))\n : undefined,\n }));\n try {\n await this.client.createTopics({ topics, waitForLeaders: true });\n } catch (err) {\n // librdkafka raises a `code` numeric `36` for TOPIC_ALREADY_EXISTS;\n // the kafkaJS-compat layer mirrors the kafkajs error shape too.\n const e = err as { code?: number; name?: string; message?: string };\n if (e?.code === 36 || e?.name === \"TOPIC_ALREADY_EXISTS\") return;\n if (/already exists/i.test(e?.message ?? \"\")) return;\n throw err;\n }\n }\n\n async createPartitions(specs: PartitionGrowSpec[]): Promise<void> {\n if (specs.length === 0) return;\n await this.client.createPartitions({\n topicPartitions: specs.map((s) => ({\n topic: s.topic,\n count: s.totalCount,\n })),\n });\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 { PublishableMessage, PublishResult, Logger } from \"@eventferry/core\";\n\n/**\n * Lifecycle hooks fired by `KafkaPublisher`. Every hook is optional. The\n * publisher wraps each invocation in a try/catch and logs (via the\n * configured logger) on failure — a misbehaving hook will NEVER break\n * publishing.\n *\n * Typical wiring:\n * - Custom observability stacks (Datadog APM, New Relic) → `onPublish`,\n * `onError`, `onTransactionAbort`.\n * - Connection-aware readiness probes → `onConnect` / `onDisconnect`.\n * - Audit logs of every published record → `onPublish`.\n */\nexport interface KafkaPublisherHooks {\n /** Fires after the underlying client successfully connects. */\n onConnect?(): void | Promise<void>;\n /** Fires after the underlying client disconnects (clean shutdown). */\n onDisconnect?(): void | Promise<void>;\n /**\n * Fires once per record after a publish attempt — both successes and\n * failures. The `result.ok` flag distinguishes them.\n */\n onPublish?(\n result: PublishResult,\n message: PublishableMessage,\n ): void | Promise<void>;\n /**\n * Fires for any error surfaced from the publish path — driver-thrown\n * errors, transaction abort errors, etc. `message` is set when the error\n * is per-record; absent for batch-level errors (e.g. connect failure).\n */\n onError?(\n error: Error,\n message?: PublishableMessage,\n ): void | Promise<void>;\n /**\n * Fires when a transactional sendBatch's inner abort path is taken.\n * Useful for observability dashboards that track EOS failure rates.\n */\n onTransactionAbort?(error: Error): void | Promise<void>;\n}\n\n/**\n * Invoke a hook safely. Never throws back into the caller — logs the hook's\n * failure via the configured logger (or no-op when logger is absent).\n */\nexport async function safeHook(\n logger: Logger | undefined,\n hookName: keyof KafkaPublisherHooks,\n invoke: () => void | Promise<void> | undefined,\n): Promise<void> {\n try {\n const r = invoke();\n if (r && typeof (r as Promise<void>).then === \"function\") {\n await r;\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n logger?.warn(`[@eventferry/kafka] hook ${hookName} threw; ignored`, {\n error: error.message,\n });\n }\n}\n","/**\n * Tracing surface for the publisher.\n *\n * eventferry deliberately does not depend on `@opentelemetry/api` — instead\n * users wire a thin adapter over their tracing system (OpenTelemetry,\n * Datadog, internal, …). This file defines the minimal contract; an\n * OpenTelemetry adapter is ~10 lines (see the README).\n *\n * The contract follows the **current stable** OpenTelemetry messaging\n * semantic conventions\n * ({@link https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/kafka.md spec}):\n *\n * - Span name: `\"{topic} publish\"`\n * - `SpanKind.PRODUCER`\n * - Required attributes: `messaging.system=kafka`,\n * `messaging.operation.type=publish`, `messaging.destination.name=<topic>`\n * - Recommended: `messaging.batch.message_count`, `messaging.kafka.partition`,\n * `server.address`, `server.port`\n * - One span per batch (NOT per message — per-message spans cause\n * cardinality explosion and the spec actively warns against this)\n */\n\n/** Attribute values the spec allows. */\nexport type SpanAttributeValue = string | number | boolean;\n\n/**\n * Minimal span surface the publisher needs. Implementations wrap a\n * tracing-system-specific span; methods MUST never throw out of the\n * publisher's hot path (wrap your own SDK calls in try/catch).\n */\nexport interface SpanLike {\n setAttribute(key: string, value: SpanAttributeValue): void;\n setAttributes(attrs: Record<string, SpanAttributeValue>): void;\n /** OK on success; ERROR on failure. The `message` is the error message. */\n setStatus(status: { code: \"ok\" | \"error\"; message?: string }): void;\n /** Attach an exception to the span (OpenTelemetry `recordException`). */\n recordException(error: Error): void;\n end(): void;\n}\n\n/**\n * Factory the publisher calls once per `sendBatch` to start a span.\n * Implementations MUST set `SpanKind.PRODUCER` and the messaging semconv\n * attributes on the returned span before returning it.\n */\nexport interface KafkaTracer {\n /**\n * Start a publish span.\n * @param name Recommended format: `\"{topic} publish\"`.\n * @param attributes Initial attributes (the publisher supplies the messaging\n * semconv set: system, destination.name, operation.type,\n * batch.message_count, plus optional kafka.partition and\n * server.address/port).\n */\n startPublishSpan(\n name: string,\n attributes: Record<string, SpanAttributeValue>,\n ): SpanLike;\n\n /**\n * OPTIONAL: inject the active trace context (W3C `traceparent` +\n * `tracestate`) into a per-message header map. Called by the publisher\n * AFTER the batch span is created and BEFORE the records hit the wire.\n *\n * Implementations typically wrap OpenTelemetry's `propagation.inject(...)`\n * or your tracing SDK's equivalent. Mutate the `headers` object in\n * place — the publisher allocates a fresh copy per message so this is\n * safe and matches the propagation API of every major SDK.\n *\n * Tracers without distributed-context propagation (or that only care\n * about local spans) may leave this off — consumers can still derive\n * trace headers themselves by other means.\n */\n inject?(span: SpanLike, headers: Record<string, string>): void;\n}\n\n/**\n * No-op tracer. Used when the user does not configure one. Cheap allocation\n * — never touches I/O.\n */\nexport class NoopKafkaTracer implements KafkaTracer {\n startPublishSpan(): SpanLike {\n return NOOP_SPAN;\n }\n}\n\nconst NOOP_SPAN: SpanLike = {\n setAttribute() {},\n setAttributes() {},\n setStatus() {},\n recordException() {},\n end() {},\n};\n","import type {\n Logger,\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\";\nimport { safeHook } from \"./hooks.js\";\nimport type { KafkaPublisherHooks } from \"./hooks.js\";\nimport { NoopKafkaTracer } from \"./tracing.js\";\nimport type { KafkaTracer, SpanLike } from \"./tracing.js\";\nimport type {\n KafkaAdmin,\n KafkaDriverAdmin,\n PartitionGrowSpec,\n TopicCreateSpec,\n} from \"./admin.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 * Optional structured logger. When set, the publisher routes its own\n * diagnostics (driver warnings, hook failures) through it. When omitted,\n * the publisher is silent — only the underlying drivers may still log.\n */\n logger?: Logger;\n /**\n * Optional lifecycle hooks. Every hook is invoked safely (try/catch +\n * logged via `logger`) and a misbehaving hook will never break publishing.\n */\n hooks?: KafkaPublisherHooks;\n /**\n * Optional tracer. When set, `publish()` wraps each batch in a span that\n * follows the current stable OpenTelemetry messaging semantic conventions.\n * Use a thin adapter over your tracing SDK (see {@link KafkaTracer}).\n */\n tracer?: KafkaTracer;\n /**\n * If set, `connect()` checks that every topic in this list exists on the\n * cluster and throws a descriptive error if any are missing. Use this to\n * fail-fast at startup instead of letting the first send-time error\n * surprise you.\n *\n * Validation runs AFTER the producer connects but BEFORE `onConnect` hooks\n * fire. Driver must implement `admin()` (the built-ins do).\n */\n validateTopicsOnConnect?: string[];\n}\n\n/**\n * The Publisher the Relay talks to. Wraps a pluggable KafkaDriver and adds\n * dead-letter routing, observability hooks, and OpenTelemetry-shaped publish\n * spans. Works against Kafka and Redpanda identically (Redpanda is Kafka-API\n * compatible).\n */\nexport class KafkaPublisher implements Publisher {\n private readonly driver: KafkaDriver;\n private readonly logger: Logger | undefined;\n private readonly hooks: KafkaPublisherHooks;\n private readonly tracer: KafkaTracer;\n private readonly validateTopicsOnConnect: readonly string[] | undefined;\n\n constructor(opts: KafkaPublisherOptions) {\n this.logger = opts.logger;\n this.hooks = opts.hooks ?? {};\n this.tracer = opts.tracer ?? new NoopKafkaTracer();\n this.validateTopicsOnConnect = opts.validateTopicsOnConnect\n ? Object.freeze([...opts.validateTopicsOnConnect])\n : undefined;\n // Plumb the logger into driver construction so driver-side diagnostics\n // (e.g. kafkajs unsupported-tuning warnings) route through it too.\n // Plumb a safe-wrapped onTransactionAbort callback so the driver-level\n // transaction abort path fans out to the user-supplied hook safely.\n const onTransactionAbort = this.hooks.onTransactionAbort\n ? (error: Error) => {\n void safeHook(this.logger, \"onTransactionAbort\", () =>\n this.hooks.onTransactionAbort?.(error),\n );\n }\n : undefined;\n this.driver =\n opts.customDriver ?? selectDriver({ ...opts, onTransactionAbort });\n }\n\n async connect(): Promise<void> {\n await this.driver.connect();\n if (this.validateTopicsOnConnect && this.validateTopicsOnConnect.length) {\n await this.assertTopicsExist(this.validateTopicsOnConnect);\n }\n await safeHook(this.logger, \"onConnect\", () => this.hooks.onConnect?.());\n }\n\n /**\n * Borrow a new admin client from the driver. The returned admin is\n * connected and ready to use; the CALLER must `close()` it. Throws if the\n * driver does not implement admin (custom driver lacking the capability).\n */\n async admin(): Promise<KafkaAdmin> {\n const driverAdmin = await this.openDriverAdmin();\n return driverAdmin;\n }\n\n /**\n * Idempotently provision topics. Each spec creates the topic if absent;\n * existing topics are skipped without error. If `growPartitions: true`\n * (default false), topics whose current partition count is below the\n * requested `numPartitions` are grown via `createPartitions`.\n *\n * Replication factor and config entries on EXISTING topics are NOT\n * reconciled — Kafka does not provide a safe in-place alter for those\n * (changing replication requires reassignment; configs use alterConfigs).\n * Reach for the raw admin if you need that.\n */\n async ensureTopics(\n specs: TopicCreateSpec[],\n opts: { growPartitions?: boolean } = {},\n ): Promise<void> {\n if (specs.length === 0) return;\n const admin = await this.openDriverAdmin();\n try {\n const topicNames = specs.map((s) => s.topic);\n const existing = await admin.describeTopics(topicNames);\n const existingByName = new Map(existing.map((t) => [t.topic, t]));\n\n const toCreate = specs.filter(\n (s) => (existingByName.get(s.topic)?.partitions.length ?? 0) === 0,\n );\n if (toCreate.length) await admin.createTopics(toCreate);\n\n if (opts.growPartitions) {\n const grow: PartitionGrowSpec[] = [];\n for (const s of specs) {\n if (s.numPartitions === undefined) continue;\n const current = existingByName.get(s.topic);\n const currentCount = current?.partitions.length ?? 0;\n if (currentCount > 0 && currentCount < s.numPartitions) {\n grow.push({ topic: s.topic, totalCount: s.numPartitions });\n }\n }\n if (grow.length) await admin.createPartitions(grow);\n }\n } finally {\n await admin.close();\n }\n }\n\n /**\n * Borrow a fresh admin from the driver and connect it. Throws when the\n * driver does not implement admin (custom drivers without that capability).\n */\n private async openDriverAdmin(): Promise<KafkaDriverAdmin> {\n if (!this.driver.admin) {\n throw new Error(\n \"KafkaPublisher: configured driver does not implement admin(). \" +\n \"Use the built-in kafkajs or confluent driver, or extend your custom driver.\",\n );\n }\n const admin = await this.driver.admin();\n await admin.connect();\n return admin;\n }\n\n /**\n * Open an admin, list topics, throw if any required topic is missing.\n * Always closes the admin (success or failure).\n */\n private async assertTopicsExist(required: readonly string[]): Promise<void> {\n const admin = await this.openDriverAdmin();\n try {\n const all = new Set(await admin.listTopics());\n const missing = required.filter((t) => !all.has(t));\n if (missing.length) {\n throw new Error(\n `KafkaPublisher: validateTopicsOnConnect failed — topics missing on cluster: ${missing.join(\", \")}`,\n );\n }\n } finally {\n await admin.close();\n }\n }\n\n async disconnect(): Promise<void> {\n await this.driver.disconnect();\n await safeHook(this.logger, \"onDisconnect\", () =>\n this.hooks.onDisconnect?.(),\n );\n }\n\n async publish(messages: PublishableMessage[]): Promise<PublishResult[]> {\n if (messages.length === 0) return [];\n\n const span = this.startBatchSpan(messages);\n // If the tracer can inject trace context (W3C `traceparent`/`tracestate`\n // is the common case), clone each message and let the tracer enrich its\n // headers. We MUST NOT mutate the caller's PublishableMessage objects —\n // the relay reuses the same record reference across retries and any\n // mutation would corrupt later attempts.\n const outgoing: PublishableMessage[] = this.tracer.inject\n ? messages.map((m) => {\n const headers = { ...m.headers };\n this.tracer.inject!(span, headers);\n return { ...m, headers };\n })\n : messages;\n let results: PublishResult[];\n try {\n results = await this.driver.sendBatch(outgoing);\n } catch (err) {\n // Driver-level throw — every record is a failure attributed to the\n // batch-level error. Record on the span, fire hook, rethrow.\n const error = err instanceof Error ? err : new Error(String(err));\n span.setStatus({ code: \"error\", message: error.message });\n span.recordException(error);\n span.end();\n await safeHook(this.logger, \"onError\", () => this.hooks.onError?.(error));\n throw err;\n }\n\n // Per-record hooks. Walk by index so the original message is available.\n const byId = new Map(messages.map((m) => [m.recordId, m]));\n let allOk = true;\n for (const r of results) {\n const msg = byId.get(r.recordId);\n if (!msg) continue;\n await safeHook(this.logger, \"onPublish\", () =>\n this.hooks.onPublish?.(r, msg),\n );\n if (!r.ok) {\n allOk = false;\n const err = r.error ?? new Error(\"publish failed\");\n await safeHook(this.logger, \"onError\", () =>\n this.hooks.onError?.(err, msg),\n );\n }\n }\n\n span.setStatus(allOk ? { code: \"ok\" } : { code: \"error\" });\n span.end();\n return results;\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 // Error class name (e.g. \"KafkaJSProtocolError\", \"RecordTooLargeException\"),\n // useful for downstream alert routing without parsing the reason string.\n \"dlq-error-class\": error.name || error.constructor?.name || \"Error\",\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 /**\n * Start a span for the batch following the OTel messaging conventions.\n *\n * Multi-topic batches: per the OTel spec, the span name uses the\n * destination — we pick the FIRST topic in the batch and document the\n * limitation. Callers that publish heterogeneous batches and care about\n * per-topic spans should split their batches upstream.\n */\n private startBatchSpan(messages: PublishableMessage[]): SpanLike {\n const topic = messages[0]?.topic ?? \"unknown\";\n return this.tracer.startPublishSpan(`${topic} publish`, {\n \"messaging.system\": \"kafka\",\n \"messaging.operation.type\": \"publish\",\n \"messaging.destination.name\": topic,\n \"messaging.batch.message_count\": messages.length,\n });\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,5 +1,97 @@
|
|
|
1
1
|
import { PublishableMessage, PublishResult, Logger, PublishErrorKind, Publisher } from '@eventferry/core';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Admin surface for `KafkaPublisher`. A typed wrapper over each driver's
|
|
5
|
+
* underlying admin client — implementations live in `kafkajs-driver.ts` and
|
|
6
|
+
* `confluent-driver.ts`. The publisher exposes this via `publisher.admin()`
|
|
7
|
+
* (returns a connected admin) and `publisher.ensureTopics()` (idempotent
|
|
8
|
+
* topic provisioning built on top).
|
|
9
|
+
*
|
|
10
|
+
* Scope is deliberately the most-used subset of the Kafka AdminClient
|
|
11
|
+
* protocol — listing, describing, creating topics and partitions. ACL
|
|
12
|
+
* management, quota inspection, and consumer-group operations are left to
|
|
13
|
+
* the underlying client (reach for kafkajs's `Admin` directly if needed).
|
|
14
|
+
*/
|
|
15
|
+
/** Specification for creating one topic. */
|
|
16
|
+
interface TopicCreateSpec {
|
|
17
|
+
topic: string;
|
|
18
|
+
/** Default: cluster's `num.partitions` broker setting. */
|
|
19
|
+
numPartitions?: number;
|
|
20
|
+
/** Default: cluster's `default.replication.factor` broker setting. */
|
|
21
|
+
replicationFactor?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Per-topic config entries (e.g. `{ "retention.ms": "604800000" }`).
|
|
24
|
+
* See Kafka broker docs for the full set.
|
|
25
|
+
*/
|
|
26
|
+
configEntries?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
/** Topic + partition descriptor returned by describeTopics. */
|
|
29
|
+
interface TopicMetadata {
|
|
30
|
+
topic: string;
|
|
31
|
+
/** Empty when the topic doesn't exist (so callers can detect absence cheaply). */
|
|
32
|
+
partitions: PartitionMetadata[];
|
|
33
|
+
}
|
|
34
|
+
interface PartitionMetadata {
|
|
35
|
+
partitionId: number;
|
|
36
|
+
/** Broker id of the partition leader; -1 if no leader is known. */
|
|
37
|
+
leader: number;
|
|
38
|
+
/** Replica broker ids. */
|
|
39
|
+
replicas: number[];
|
|
40
|
+
/** In-sync replica broker ids. */
|
|
41
|
+
isr: number[];
|
|
42
|
+
}
|
|
43
|
+
/** Specification for growing a topic's partition count (never shrink). */
|
|
44
|
+
interface PartitionGrowSpec {
|
|
45
|
+
topic: string;
|
|
46
|
+
/** Total partition count after the change. MUST be ≥ the current count. */
|
|
47
|
+
totalCount: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Typed admin surface exposed by {@link KafkaPublisher.admin}.
|
|
51
|
+
*
|
|
52
|
+
* The returned object is already connected — call `.close()` (or let the
|
|
53
|
+
* publisher's `disconnect()` cascade close it) when done.
|
|
54
|
+
*
|
|
55
|
+
* All methods may throw native errors from the underlying client. The
|
|
56
|
+
* publisher does not classify admin errors via the `errorKind` machinery
|
|
57
|
+
* because admin failures are operator-facing and don't flow through the
|
|
58
|
+
* relay's retry path.
|
|
59
|
+
*/
|
|
60
|
+
interface KafkaAdmin {
|
|
61
|
+
/** All topic names visible to this principal, including internal topics. */
|
|
62
|
+
listTopics(): Promise<string[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Metadata for the given topics. Topics that don't exist on the cluster
|
|
65
|
+
* are returned with an empty `partitions` array — callers detect absence
|
|
66
|
+
* cheaply without a try/catch.
|
|
67
|
+
*/
|
|
68
|
+
describeTopics(topics: string[]): Promise<TopicMetadata[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Create topics. Idempotent at this layer: topics that already exist are
|
|
71
|
+
* silently skipped (the underlying client may throw `TopicExistsError`;
|
|
72
|
+
* we swallow it). Use `ensureTopics` on the publisher for partition-count
|
|
73
|
+
* + replication-factor coherence checks.
|
|
74
|
+
*/
|
|
75
|
+
createTopics(specs: TopicCreateSpec[]): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Grow each topic's partition count. Never shrinks (Kafka does not
|
|
78
|
+
* support shrinking partitions). Specs whose `totalCount` equals the
|
|
79
|
+
* current partition count are silently skipped.
|
|
80
|
+
*/
|
|
81
|
+
createPartitions(specs: PartitionGrowSpec[]): Promise<void>;
|
|
82
|
+
/** Disconnect the admin client. */
|
|
83
|
+
close(): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Optional driver-level hook returned by {@link KafkaDriver.admin}. Drivers
|
|
87
|
+
* implement this thin contract; the publisher composes the higher-level
|
|
88
|
+
* `KafkaAdmin` and `ensureTopics` on top.
|
|
89
|
+
*/
|
|
90
|
+
interface KafkaDriverAdmin extends KafkaAdmin {
|
|
91
|
+
/** Called by the publisher before any method is invoked. */
|
|
92
|
+
connect(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
|
|
3
95
|
/**
|
|
4
96
|
* Low-level driver contract. Each concrete driver (kafkajs, confluent)
|
|
5
97
|
* adapts its native client to this minimal surface. The KafkaPublisher
|
|
@@ -19,6 +111,14 @@ interface KafkaDriver {
|
|
|
19
111
|
* uses this to decide whether `sendBatch` is atomic.
|
|
20
112
|
*/
|
|
21
113
|
readonly transactional: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Construct a NEW admin client. The returned admin is not yet connected —
|
|
116
|
+
* the publisher calls `.connect()` before handing it to the user.
|
|
117
|
+
*
|
|
118
|
+
* Optional: drivers without an admin surface may omit this; the publisher
|
|
119
|
+
* throws a clear error when `publisher.admin()` is called on such a driver.
|
|
120
|
+
*/
|
|
121
|
+
admin?(): Promise<KafkaDriverAdmin>;
|
|
22
122
|
}
|
|
23
123
|
/**
|
|
24
124
|
* TLS configuration for client connections. Pass a full {@link TlsConfig}
|
|
@@ -149,6 +249,14 @@ interface ProducerBehaviorConfig {
|
|
|
149
249
|
acks?: number;
|
|
150
250
|
/** Compression codec. Driver maps to its native enum. */
|
|
151
251
|
compression?: "none" | "gzip" | "snappy" | "lz4" | "zstd";
|
|
252
|
+
/**
|
|
253
|
+
* (confluent only) Compression level for the chosen codec. Defaults vary
|
|
254
|
+
* per codec — librdkafka picks the broker-friendly default when unset.
|
|
255
|
+
* Common ranges: gzip 1–9, lz4 0–12, zstd 1–22 (higher = smaller + slower).
|
|
256
|
+
*
|
|
257
|
+
* No-op on the kafkajs driver (kafkajs does not expose codec levels).
|
|
258
|
+
*/
|
|
259
|
+
compressionLevel?: number;
|
|
152
260
|
/**
|
|
153
261
|
* (confluent only) How long the producer waits to accumulate records before
|
|
154
262
|
* flushing a partition batch. Default 0 (ship-immediately). Increase to
|
|
@@ -199,6 +307,36 @@ interface ProducerBehaviorConfig {
|
|
|
199
307
|
* whether this callback throws.
|
|
200
308
|
*/
|
|
201
309
|
onTransactionAbort?: (error: Error) => void;
|
|
310
|
+
/**
|
|
311
|
+
* (confluent only) Raw librdkafka producer-config keys merged on top of
|
|
312
|
+
* eventferry's translated config. Use for tuning surface area we don't
|
|
313
|
+
* expose typed (e.g. `queue.buffering.max.messages`, `socket.keepalive.enable`,
|
|
314
|
+
* `statistics.interval.ms`). Native keys win against the translated ones,
|
|
315
|
+
* so this can also be used to override defaults.
|
|
316
|
+
*
|
|
317
|
+
* Ignored by the kafkajs driver — log a one-time warning instead of
|
|
318
|
+
* silently dropping. Use `rawKafkaJsProducerConfig` for kafkajs-side tuning.
|
|
319
|
+
*/
|
|
320
|
+
rawProducerConfig?: Record<string, unknown>;
|
|
321
|
+
/**
|
|
322
|
+
* (kafkajs only) Raw producer-config keys merged into kafkajs's
|
|
323
|
+
* `kafka.producer({...})` call. Native keys win against the translated
|
|
324
|
+
* ones. Use for kafkajs-internal knobs like `retry`, `metadataMaxAge`,
|
|
325
|
+
* `idempotent` overrides, etc.
|
|
326
|
+
*
|
|
327
|
+
* No-op on the confluent driver — use `rawProducerConfig` there.
|
|
328
|
+
*/
|
|
329
|
+
rawKafkaJsProducerConfig?: Record<string, unknown>;
|
|
330
|
+
/**
|
|
331
|
+
* (kafkajs only) Custom partitioner factory passed straight to
|
|
332
|
+
* `kafka.producer({ createPartitioner })`. Overrides {@link partitioner}
|
|
333
|
+
* preset entirely. See kafkajs docs for the factory signature:
|
|
334
|
+
* `() => (args: { topic, partitionMetadata, message }) => number`.
|
|
335
|
+
*
|
|
336
|
+
* Ignored by the confluent driver — librdkafka's partitioner is a C
|
|
337
|
+
* extension point, not a JS callback.
|
|
338
|
+
*/
|
|
339
|
+
customPartitioner?: () => (args: unknown) => number;
|
|
202
340
|
}
|
|
203
341
|
type DriverKind = "kafkajs" | "confluent";
|
|
204
342
|
|
|
@@ -213,6 +351,11 @@ interface KjsTransaction {
|
|
|
213
351
|
commit(): Promise<void>;
|
|
214
352
|
abort(): Promise<void>;
|
|
215
353
|
}
|
|
354
|
+
interface KjsPartitionersNamespace {
|
|
355
|
+
DefaultPartitioner: () => unknown;
|
|
356
|
+
LegacyPartitioner: () => unknown;
|
|
357
|
+
JavaCompatiblePartitioner: () => unknown;
|
|
358
|
+
}
|
|
216
359
|
interface KafkaJsDriverOptions extends KafkaConnectionConfig, ProducerBehaviorConfig {
|
|
217
360
|
/**
|
|
218
361
|
* Optional logger for the driver's own diagnostics (e.g. warnings about
|
|
@@ -235,7 +378,19 @@ declare class KafkaJsDriver implements KafkaDriver {
|
|
|
235
378
|
* the send/transaction logic can be exercised without a real broker.
|
|
236
379
|
*/
|
|
237
380
|
protected createProducer(): Promise<KjsProducer>;
|
|
381
|
+
/**
|
|
382
|
+
* Compute the options object passed to `kafka.producer({...})`. Exposed
|
|
383
|
+
* as a test seam so power-user escape hatches (customPartitioner,
|
|
384
|
+
* rawKafkaJsProducerConfig) can be asserted without a live broker.
|
|
385
|
+
*/
|
|
386
|
+
protected buildProducerOptions(partitioners: KjsPartitionersNamespace | undefined): Promise<Record<string, unknown>>;
|
|
238
387
|
disconnect(): Promise<void>;
|
|
388
|
+
/**
|
|
389
|
+
* Construct a kafkajs admin client wrapped in the eventferry-facing
|
|
390
|
+
* `KafkaDriverAdmin` shape. The publisher calls `.connect()` on the
|
|
391
|
+
* returned object before exposing it via `publisher.admin()`.
|
|
392
|
+
*/
|
|
393
|
+
admin(): Promise<KafkaDriverAdmin>;
|
|
239
394
|
sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
|
|
240
395
|
}
|
|
241
396
|
/** Internal — used by tests. Resets the dedup so warnings can be observed in isolation. */
|
|
@@ -288,6 +443,12 @@ declare class ConfluentDriver implements KafkaDriver {
|
|
|
288
443
|
*/
|
|
289
444
|
protected createProducer(): Promise<CkProducer>;
|
|
290
445
|
disconnect(): Promise<void>;
|
|
446
|
+
/**
|
|
447
|
+
* Construct a librdkafka-backed admin client wrapped in the eventferry
|
|
448
|
+
* `KafkaDriverAdmin` shape. The publisher's `connect()` is called before
|
|
449
|
+
* the admin reaches the user.
|
|
450
|
+
*/
|
|
451
|
+
admin(): Promise<KafkaDriverAdmin>;
|
|
291
452
|
sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
|
|
292
453
|
}
|
|
293
454
|
|
|
@@ -423,6 +584,21 @@ interface KafkaTracer {
|
|
|
423
584
|
* server.address/port).
|
|
424
585
|
*/
|
|
425
586
|
startPublishSpan(name: string, attributes: Record<string, SpanAttributeValue>): SpanLike;
|
|
587
|
+
/**
|
|
588
|
+
* OPTIONAL: inject the active trace context (W3C `traceparent` +
|
|
589
|
+
* `tracestate`) into a per-message header map. Called by the publisher
|
|
590
|
+
* AFTER the batch span is created and BEFORE the records hit the wire.
|
|
591
|
+
*
|
|
592
|
+
* Implementations typically wrap OpenTelemetry's `propagation.inject(...)`
|
|
593
|
+
* or your tracing SDK's equivalent. Mutate the `headers` object in
|
|
594
|
+
* place — the publisher allocates a fresh copy per message so this is
|
|
595
|
+
* safe and matches the propagation API of every major SDK.
|
|
596
|
+
*
|
|
597
|
+
* Tracers without distributed-context propagation (or that only care
|
|
598
|
+
* about local spans) may leave this off — consumers can still derive
|
|
599
|
+
* trace headers themselves by other means.
|
|
600
|
+
*/
|
|
601
|
+
inject?(span: SpanLike, headers: Record<string, string>): void;
|
|
426
602
|
}
|
|
427
603
|
/**
|
|
428
604
|
* No-op tracer. Used when the user does not configure one. Cheap allocation
|
|
@@ -457,6 +633,16 @@ interface KafkaPublisherOptions extends KafkaConnectionConfig, ProducerBehaviorC
|
|
|
457
633
|
* Use a thin adapter over your tracing SDK (see {@link KafkaTracer}).
|
|
458
634
|
*/
|
|
459
635
|
tracer?: KafkaTracer;
|
|
636
|
+
/**
|
|
637
|
+
* If set, `connect()` checks that every topic in this list exists on the
|
|
638
|
+
* cluster and throws a descriptive error if any are missing. Use this to
|
|
639
|
+
* fail-fast at startup instead of letting the first send-time error
|
|
640
|
+
* surprise you.
|
|
641
|
+
*
|
|
642
|
+
* Validation runs AFTER the producer connects but BEFORE `onConnect` hooks
|
|
643
|
+
* fire. Driver must implement `admin()` (the built-ins do).
|
|
644
|
+
*/
|
|
645
|
+
validateTopicsOnConnect?: string[];
|
|
460
646
|
}
|
|
461
647
|
/**
|
|
462
648
|
* The Publisher the Relay talks to. Wraps a pluggable KafkaDriver and adds
|
|
@@ -469,8 +655,39 @@ declare class KafkaPublisher implements Publisher {
|
|
|
469
655
|
private readonly logger;
|
|
470
656
|
private readonly hooks;
|
|
471
657
|
private readonly tracer;
|
|
658
|
+
private readonly validateTopicsOnConnect;
|
|
472
659
|
constructor(opts: KafkaPublisherOptions);
|
|
473
660
|
connect(): Promise<void>;
|
|
661
|
+
/**
|
|
662
|
+
* Borrow a new admin client from the driver. The returned admin is
|
|
663
|
+
* connected and ready to use; the CALLER must `close()` it. Throws if the
|
|
664
|
+
* driver does not implement admin (custom driver lacking the capability).
|
|
665
|
+
*/
|
|
666
|
+
admin(): Promise<KafkaAdmin>;
|
|
667
|
+
/**
|
|
668
|
+
* Idempotently provision topics. Each spec creates the topic if absent;
|
|
669
|
+
* existing topics are skipped without error. If `growPartitions: true`
|
|
670
|
+
* (default false), topics whose current partition count is below the
|
|
671
|
+
* requested `numPartitions` are grown via `createPartitions`.
|
|
672
|
+
*
|
|
673
|
+
* Replication factor and config entries on EXISTING topics are NOT
|
|
674
|
+
* reconciled — Kafka does not provide a safe in-place alter for those
|
|
675
|
+
* (changing replication requires reassignment; configs use alterConfigs).
|
|
676
|
+
* Reach for the raw admin if you need that.
|
|
677
|
+
*/
|
|
678
|
+
ensureTopics(specs: TopicCreateSpec[], opts?: {
|
|
679
|
+
growPartitions?: boolean;
|
|
680
|
+
}): Promise<void>;
|
|
681
|
+
/**
|
|
682
|
+
* Borrow a fresh admin from the driver and connect it. Throws when the
|
|
683
|
+
* driver does not implement admin (custom drivers without that capability).
|
|
684
|
+
*/
|
|
685
|
+
private openDriverAdmin;
|
|
686
|
+
/**
|
|
687
|
+
* Open an admin, list topics, throw if any required topic is missing.
|
|
688
|
+
* Always closes the admin (success or failure).
|
|
689
|
+
*/
|
|
690
|
+
private assertTopicsExist;
|
|
474
691
|
disconnect(): Promise<void>;
|
|
475
692
|
publish(messages: PublishableMessage[]): Promise<PublishResult[]>;
|
|
476
693
|
/**
|
|
@@ -491,4 +708,4 @@ declare class KafkaPublisher implements Publisher {
|
|
|
491
708
|
private startBatchSpan;
|
|
492
709
|
}
|
|
493
710
|
|
|
494
|
-
export { type ConfluentClientConfig, ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, type KafkaJsPartitionerChoice, KafkaPublisher, type KafkaPublisherHooks, type KafkaPublisherOptions, type KafkaTracer, NoopKafkaTracer, type OauthBearerToken, type ProducerBehaviorConfig, type SaslConfig, type SaslOauthbearerConfig, type SaslPasswordConfig, type SpanAttributeValue, type SpanLike, type TlsConfig, _resetKafkajsWarnDedup, buildConfluentClientConfig, classifyConfluentError, classifyKafkajsError, safeHook };
|
|
711
|
+
export { type ConfluentClientConfig, ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaAdmin, type KafkaConnectionConfig, type KafkaDriver, type KafkaDriverAdmin, KafkaJsDriver, type KafkaJsDriverOptions, type KafkaJsPartitionerChoice, KafkaPublisher, type KafkaPublisherHooks, type KafkaPublisherOptions, type KafkaTracer, NoopKafkaTracer, type OauthBearerToken, type PartitionGrowSpec, type PartitionMetadata, type ProducerBehaviorConfig, type SaslConfig, type SaslOauthbearerConfig, type SaslPasswordConfig, type SpanAttributeValue, type SpanLike, type TlsConfig, type TopicCreateSpec, type TopicMetadata, _resetKafkajsWarnDedup, buildConfluentClientConfig, classifyConfluentError, classifyKafkajsError, safeHook };
|