@eventferry/kafka 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -0
- package/dist/index.cjs +144 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +162 -7
- package/dist/index.d.ts +162 -7
- package/dist/index.js +142 -10
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -20,17 +20,108 @@ interface KafkaDriver {
|
|
|
20
20
|
*/
|
|
21
21
|
readonly transactional: boolean;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* TLS configuration for client connections. Pass a full {@link TlsConfig}
|
|
25
|
+
* when the cluster requires CA pinning, mutual TLS (client cert + key), or a
|
|
26
|
+
* specific SNI host. Plain `ssl: true` keeps the previous behavior (one-way
|
|
27
|
+
* TLS using the driver's default trust store).
|
|
28
|
+
*
|
|
29
|
+
* `rejectUnauthorized` is intentionally NOT a knob here — TLS verification is
|
|
30
|
+
* non-negotiable. Dev clusters with self-signed certs pass their CA via `ca`.
|
|
31
|
+
*/
|
|
32
|
+
interface TlsConfig {
|
|
33
|
+
/** PEM-encoded CA bundle. Buffers and strings both accepted. */
|
|
34
|
+
ca?: string | Buffer | Array<string | Buffer>;
|
|
35
|
+
/** PEM-encoded client certificate (required for mTLS). */
|
|
36
|
+
cert?: string | Buffer;
|
|
37
|
+
/** PEM-encoded private key for the client certificate (required for mTLS). */
|
|
38
|
+
key?: string | Buffer;
|
|
39
|
+
/** Passphrase for an encrypted private key. */
|
|
40
|
+
passphrase?: string;
|
|
41
|
+
/** SNI host. Useful when broker address doesn't match the cert SAN. */
|
|
42
|
+
servername?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Username + password SASL: PLAIN and SCRAM-SHA-256/512. The conventional
|
|
46
|
+
* "API key + secret" shape used by Confluent Cloud, Aiven, on-prem SCRAM.
|
|
47
|
+
*/
|
|
48
|
+
interface SaslPasswordConfig {
|
|
49
|
+
mechanism: "plain" | "scram-sha-256" | "scram-sha-512";
|
|
50
|
+
username: string;
|
|
51
|
+
password: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Token returned by an OAUTHBEARER provider.
|
|
55
|
+
*
|
|
56
|
+
* Driver asymmetry (verified against `kafkajs/types/index.d.ts` and
|
|
57
|
+
* `@confluentinc/kafka-javascript/types/kafkajs.d.ts`):
|
|
58
|
+
*
|
|
59
|
+
* - `kafkajs` reads only `value`. Other fields are silently ignored.
|
|
60
|
+
* - `@confluentinc/kafka-javascript` REQUIRES `value` + `principal` + `lifetime`
|
|
61
|
+
* and accepts an optional `extensions` map. Passing only `{ value }` throws.
|
|
62
|
+
*
|
|
63
|
+
* Cross-driver portable providers MUST populate all four. eventferry treats
|
|
64
|
+
* `principal` / `lifetime` / `extensions` as optional in the type to support
|
|
65
|
+
* kafkajs-only setups; supplying them is a no-op there.
|
|
66
|
+
*/
|
|
67
|
+
interface OauthBearerToken {
|
|
68
|
+
/** The bearer token string (JWT, opaque, …). */
|
|
69
|
+
value: string;
|
|
70
|
+
/** Principal name. REQUIRED on the confluent driver. */
|
|
71
|
+
principal?: string;
|
|
72
|
+
/** Lifetime in MILLISECONDS. REQUIRED on the confluent driver. */
|
|
73
|
+
lifetime?: number;
|
|
74
|
+
/** SASL extensions to send alongside the token (e.g. for OIDC scopes). */
|
|
75
|
+
extensions?: Record<string, string>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* SASL/OAUTHBEARER: bring-your-own token provider. The function is invoked
|
|
79
|
+
* by the underlying client on demand (NOT on a fixed timer); cache the
|
|
80
|
+
* token in your provider if you want to amortise issuance cost.
|
|
81
|
+
*
|
|
82
|
+
* Required for Azure Event Hubs, Confluent Cloud with OAuth/SSO, and any
|
|
83
|
+
* OIDC-fronted Kafka. For AWS MSK IAM, wrap the AWS SigV4 signer in this
|
|
84
|
+
* callback.
|
|
85
|
+
*/
|
|
86
|
+
interface SaslOauthbearerConfig {
|
|
87
|
+
mechanism: "oauthbearer";
|
|
88
|
+
oauthBearerProvider: () => Promise<OauthBearerToken>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Discriminated union over the SASL mechanisms eventferry supports today.
|
|
92
|
+
* Add new mechanisms by extending this union and mapping them in each driver.
|
|
93
|
+
*/
|
|
94
|
+
type SaslConfig = SaslPasswordConfig | SaslOauthbearerConfig;
|
|
23
95
|
/** Shared connection config accepted by both drivers. */
|
|
24
96
|
interface KafkaConnectionConfig {
|
|
25
97
|
brokers: string[];
|
|
26
98
|
clientId?: string;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
99
|
+
/**
|
|
100
|
+
* TLS configuration. `true` enables one-way TLS using the driver's default
|
|
101
|
+
* trust store; a {@link TlsConfig} object lets you supply a custom CA,
|
|
102
|
+
* client cert (for mTLS), and SNI host.
|
|
103
|
+
*/
|
|
104
|
+
ssl?: boolean | TlsConfig;
|
|
105
|
+
sasl?: SaslConfig;
|
|
33
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Choice of partitioner. Only honored by the kafkajs driver — the confluent
|
|
109
|
+
* driver uses librdkafka's `consistent_random` (key-aware sticky) and
|
|
110
|
+
* partitioner override is out of scope for this release.
|
|
111
|
+
*
|
|
112
|
+
* - `"java-compatible"` (recommended for greenfield): kafkajs's
|
|
113
|
+
* `Partitioners.JavaCompatiblePartitioner`. Matches the Java client's
|
|
114
|
+
* murmur2-based hash so producers across language boundaries land on the
|
|
115
|
+
* same partition for the same key.
|
|
116
|
+
* - `"legacy"`: kafkajs's pre-v2 partitioner. Use when migrating an existing
|
|
117
|
+
* topic where hash continuity matters.
|
|
118
|
+
* - `"default"`: kafkajs's current default. Equivalent to legacy in v2 but
|
|
119
|
+
* may change with major kafkajs releases.
|
|
120
|
+
*
|
|
121
|
+
* Setting this also silences the noisy `KafkaJSPartitionerNotSpecified`
|
|
122
|
+
* warning kafkajs emits when no partitioner choice is made explicitly.
|
|
123
|
+
*/
|
|
124
|
+
type KafkaJsPartitionerChoice = "default" | "legacy" | "java-compatible";
|
|
34
125
|
interface ProducerBehaviorConfig {
|
|
35
126
|
/** Enable idempotent producer (dedup + ordering). Default true. */
|
|
36
127
|
idempotent?: boolean;
|
|
@@ -45,6 +136,46 @@ interface ProducerBehaviorConfig {
|
|
|
45
136
|
acks?: number;
|
|
46
137
|
/** Compression codec. Driver maps to its native enum. */
|
|
47
138
|
compression?: "none" | "gzip" | "snappy" | "lz4" | "zstd";
|
|
139
|
+
/**
|
|
140
|
+
* (confluent only) How long the producer waits to accumulate records before
|
|
141
|
+
* flushing a partition batch. Default 0 (ship-immediately). Increase to
|
|
142
|
+
* 10–50ms for higher throughput at the cost of latency.
|
|
143
|
+
*/
|
|
144
|
+
lingerMs?: number;
|
|
145
|
+
/** (confluent only) Maximum bytes per partition batch before forced flush. */
|
|
146
|
+
batchSize?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Max concurrent unacknowledged producer requests. MUST be ≤5 when
|
|
149
|
+
* `idempotent: true`. Higher = throughput; lower = stricter ordering on
|
|
150
|
+
* non-idempotent producers (no other path preserves order on retry).
|
|
151
|
+
*/
|
|
152
|
+
maxInFlightRequests?: number;
|
|
153
|
+
/** Per-request broker-ack timeout. Default 30 s. */
|
|
154
|
+
requestTimeoutMs?: number;
|
|
155
|
+
/**
|
|
156
|
+
* (confluent only) End-to-end timeout for a record from produce() call to
|
|
157
|
+
* terminal success / failure (includes retries). Defaults to 120 s.
|
|
158
|
+
* If this exceeds the relay's `claimTimeoutMs`, the reaper may double-
|
|
159
|
+
* publish a slow record — set both coherently.
|
|
160
|
+
*/
|
|
161
|
+
deliveryTimeoutMs?: number;
|
|
162
|
+
/**
|
|
163
|
+
* (confluent only) Max bytes of a single record (after compression).
|
|
164
|
+
* MUST be ≤ broker's `message.max.bytes`. Defaults to 1 MB.
|
|
165
|
+
*/
|
|
166
|
+
maxRequestSize?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Broker-side ceiling on how long a transaction can stay open before
|
|
169
|
+
* auto-abort. Maps to `transaction.timeout.ms`. Default 60 s; capped by
|
|
170
|
+
* the broker's `transaction.max.timeout.ms`.
|
|
171
|
+
*/
|
|
172
|
+
transactionTimeoutMs?: number;
|
|
173
|
+
/**
|
|
174
|
+
* (kafkajs only) Choice of partitioner. See
|
|
175
|
+
* {@link KafkaJsPartitionerChoice} for the options. Setting any value
|
|
176
|
+
* silences kafkajs's `KafkaJSPartitionerNotSpecified` warning.
|
|
177
|
+
*/
|
|
178
|
+
partitioner?: KafkaJsPartitionerChoice;
|
|
48
179
|
}
|
|
49
180
|
type DriverKind = "kafkajs" | "confluent";
|
|
50
181
|
|
|
@@ -78,6 +209,8 @@ declare class KafkaJsDriver implements KafkaDriver {
|
|
|
78
209
|
disconnect(): Promise<void>;
|
|
79
210
|
sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
|
|
80
211
|
}
|
|
212
|
+
/** Internal — used by tests. Resets the dedup so warnings can be observed in isolation. */
|
|
213
|
+
declare function _resetKafkajsWarnDedup(): void;
|
|
81
214
|
|
|
82
215
|
/**
|
|
83
216
|
* Classify a kafkajs producer error into a {@link PublishErrorKind} so the
|
|
@@ -144,6 +277,28 @@ declare class ConfluentDriver implements KafkaDriver {
|
|
|
144
277
|
*/
|
|
145
278
|
declare function classifyConfluentError(err: unknown): PublishErrorKind;
|
|
146
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Translate eventferry's normalized `KafkaConnectionConfig` into the shape
|
|
282
|
+
* expected by `@confluentinc/kafka-javascript`'s `KafkaJS.Kafka` constructor.
|
|
283
|
+
*
|
|
284
|
+
* Returns an object with two parts:
|
|
285
|
+
* - `kafkaJS`: the kafkajs-compatible config layer (clientId, brokers, and
|
|
286
|
+
* simple ssl/sasl when no advanced TLS is needed).
|
|
287
|
+
* - top-level keys: librdkafka-style config (e.g. `ssl.ca.pem`,
|
|
288
|
+
* `security.protocol`) used when the user supplies a {@link TlsConfig}.
|
|
289
|
+
*
|
|
290
|
+
* Why a separate translator: the kafkajs-compat layer accepts the simple
|
|
291
|
+
* `ssl: true` boolean but the verified path for mTLS (CA + cert + key) is
|
|
292
|
+
* librdkafka's `ssl.*.pem` keys. The translator picks the right surface
|
|
293
|
+
* based on what the caller supplied. Buffer inputs are coerced to strings —
|
|
294
|
+
* librdkafka accepts PEM strings, NOT Buffers.
|
|
295
|
+
*/
|
|
296
|
+
interface ConfluentClientConfig {
|
|
297
|
+
kafkaJS: Record<string, unknown>;
|
|
298
|
+
librdkafka: Record<string, unknown>;
|
|
299
|
+
}
|
|
300
|
+
declare function buildConfluentClientConfig(opts: KafkaConnectionConfig & ProducerBehaviorConfig): ConfluentClientConfig;
|
|
301
|
+
|
|
147
302
|
interface KafkaPublisherOptions extends KafkaConnectionConfig, ProducerBehaviorConfig {
|
|
148
303
|
/** Which underlying client to use. Default "kafkajs". */
|
|
149
304
|
driver?: DriverKind;
|
|
@@ -173,4 +328,4 @@ declare class KafkaPublisher implements Publisher {
|
|
|
173
328
|
get transactional(): boolean;
|
|
174
329
|
}
|
|
175
330
|
|
|
176
|
-
export { ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, KafkaPublisher, type KafkaPublisherOptions, type ProducerBehaviorConfig, classifyConfluentError, classifyKafkajsError };
|
|
331
|
+
export { type ConfluentClientConfig, ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, type KafkaJsPartitionerChoice, KafkaPublisher, type KafkaPublisherOptions, type OauthBearerToken, type ProducerBehaviorConfig, type SaslConfig, type SaslOauthbearerConfig, type SaslPasswordConfig, type TlsConfig, _resetKafkajsWarnDedup, buildConfluentClientConfig, classifyConfluentError, classifyKafkajsError };
|
package/dist/index.d.ts
CHANGED
|
@@ -20,17 +20,108 @@ interface KafkaDriver {
|
|
|
20
20
|
*/
|
|
21
21
|
readonly transactional: boolean;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* TLS configuration for client connections. Pass a full {@link TlsConfig}
|
|
25
|
+
* when the cluster requires CA pinning, mutual TLS (client cert + key), or a
|
|
26
|
+
* specific SNI host. Plain `ssl: true` keeps the previous behavior (one-way
|
|
27
|
+
* TLS using the driver's default trust store).
|
|
28
|
+
*
|
|
29
|
+
* `rejectUnauthorized` is intentionally NOT a knob here — TLS verification is
|
|
30
|
+
* non-negotiable. Dev clusters with self-signed certs pass their CA via `ca`.
|
|
31
|
+
*/
|
|
32
|
+
interface TlsConfig {
|
|
33
|
+
/** PEM-encoded CA bundle. Buffers and strings both accepted. */
|
|
34
|
+
ca?: string | Buffer | Array<string | Buffer>;
|
|
35
|
+
/** PEM-encoded client certificate (required for mTLS). */
|
|
36
|
+
cert?: string | Buffer;
|
|
37
|
+
/** PEM-encoded private key for the client certificate (required for mTLS). */
|
|
38
|
+
key?: string | Buffer;
|
|
39
|
+
/** Passphrase for an encrypted private key. */
|
|
40
|
+
passphrase?: string;
|
|
41
|
+
/** SNI host. Useful when broker address doesn't match the cert SAN. */
|
|
42
|
+
servername?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Username + password SASL: PLAIN and SCRAM-SHA-256/512. The conventional
|
|
46
|
+
* "API key + secret" shape used by Confluent Cloud, Aiven, on-prem SCRAM.
|
|
47
|
+
*/
|
|
48
|
+
interface SaslPasswordConfig {
|
|
49
|
+
mechanism: "plain" | "scram-sha-256" | "scram-sha-512";
|
|
50
|
+
username: string;
|
|
51
|
+
password: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Token returned by an OAUTHBEARER provider.
|
|
55
|
+
*
|
|
56
|
+
* Driver asymmetry (verified against `kafkajs/types/index.d.ts` and
|
|
57
|
+
* `@confluentinc/kafka-javascript/types/kafkajs.d.ts`):
|
|
58
|
+
*
|
|
59
|
+
* - `kafkajs` reads only `value`. Other fields are silently ignored.
|
|
60
|
+
* - `@confluentinc/kafka-javascript` REQUIRES `value` + `principal` + `lifetime`
|
|
61
|
+
* and accepts an optional `extensions` map. Passing only `{ value }` throws.
|
|
62
|
+
*
|
|
63
|
+
* Cross-driver portable providers MUST populate all four. eventferry treats
|
|
64
|
+
* `principal` / `lifetime` / `extensions` as optional in the type to support
|
|
65
|
+
* kafkajs-only setups; supplying them is a no-op there.
|
|
66
|
+
*/
|
|
67
|
+
interface OauthBearerToken {
|
|
68
|
+
/** The bearer token string (JWT, opaque, …). */
|
|
69
|
+
value: string;
|
|
70
|
+
/** Principal name. REQUIRED on the confluent driver. */
|
|
71
|
+
principal?: string;
|
|
72
|
+
/** Lifetime in MILLISECONDS. REQUIRED on the confluent driver. */
|
|
73
|
+
lifetime?: number;
|
|
74
|
+
/** SASL extensions to send alongside the token (e.g. for OIDC scopes). */
|
|
75
|
+
extensions?: Record<string, string>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* SASL/OAUTHBEARER: bring-your-own token provider. The function is invoked
|
|
79
|
+
* by the underlying client on demand (NOT on a fixed timer); cache the
|
|
80
|
+
* token in your provider if you want to amortise issuance cost.
|
|
81
|
+
*
|
|
82
|
+
* Required for Azure Event Hubs, Confluent Cloud with OAuth/SSO, and any
|
|
83
|
+
* OIDC-fronted Kafka. For AWS MSK IAM, wrap the AWS SigV4 signer in this
|
|
84
|
+
* callback.
|
|
85
|
+
*/
|
|
86
|
+
interface SaslOauthbearerConfig {
|
|
87
|
+
mechanism: "oauthbearer";
|
|
88
|
+
oauthBearerProvider: () => Promise<OauthBearerToken>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Discriminated union over the SASL mechanisms eventferry supports today.
|
|
92
|
+
* Add new mechanisms by extending this union and mapping them in each driver.
|
|
93
|
+
*/
|
|
94
|
+
type SaslConfig = SaslPasswordConfig | SaslOauthbearerConfig;
|
|
23
95
|
/** Shared connection config accepted by both drivers. */
|
|
24
96
|
interface KafkaConnectionConfig {
|
|
25
97
|
brokers: string[];
|
|
26
98
|
clientId?: string;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
99
|
+
/**
|
|
100
|
+
* TLS configuration. `true` enables one-way TLS using the driver's default
|
|
101
|
+
* trust store; a {@link TlsConfig} object lets you supply a custom CA,
|
|
102
|
+
* client cert (for mTLS), and SNI host.
|
|
103
|
+
*/
|
|
104
|
+
ssl?: boolean | TlsConfig;
|
|
105
|
+
sasl?: SaslConfig;
|
|
33
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Choice of partitioner. Only honored by the kafkajs driver — the confluent
|
|
109
|
+
* driver uses librdkafka's `consistent_random` (key-aware sticky) and
|
|
110
|
+
* partitioner override is out of scope for this release.
|
|
111
|
+
*
|
|
112
|
+
* - `"java-compatible"` (recommended for greenfield): kafkajs's
|
|
113
|
+
* `Partitioners.JavaCompatiblePartitioner`. Matches the Java client's
|
|
114
|
+
* murmur2-based hash so producers across language boundaries land on the
|
|
115
|
+
* same partition for the same key.
|
|
116
|
+
* - `"legacy"`: kafkajs's pre-v2 partitioner. Use when migrating an existing
|
|
117
|
+
* topic where hash continuity matters.
|
|
118
|
+
* - `"default"`: kafkajs's current default. Equivalent to legacy in v2 but
|
|
119
|
+
* may change with major kafkajs releases.
|
|
120
|
+
*
|
|
121
|
+
* Setting this also silences the noisy `KafkaJSPartitionerNotSpecified`
|
|
122
|
+
* warning kafkajs emits when no partitioner choice is made explicitly.
|
|
123
|
+
*/
|
|
124
|
+
type KafkaJsPartitionerChoice = "default" | "legacy" | "java-compatible";
|
|
34
125
|
interface ProducerBehaviorConfig {
|
|
35
126
|
/** Enable idempotent producer (dedup + ordering). Default true. */
|
|
36
127
|
idempotent?: boolean;
|
|
@@ -45,6 +136,46 @@ interface ProducerBehaviorConfig {
|
|
|
45
136
|
acks?: number;
|
|
46
137
|
/** Compression codec. Driver maps to its native enum. */
|
|
47
138
|
compression?: "none" | "gzip" | "snappy" | "lz4" | "zstd";
|
|
139
|
+
/**
|
|
140
|
+
* (confluent only) How long the producer waits to accumulate records before
|
|
141
|
+
* flushing a partition batch. Default 0 (ship-immediately). Increase to
|
|
142
|
+
* 10–50ms for higher throughput at the cost of latency.
|
|
143
|
+
*/
|
|
144
|
+
lingerMs?: number;
|
|
145
|
+
/** (confluent only) Maximum bytes per partition batch before forced flush. */
|
|
146
|
+
batchSize?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Max concurrent unacknowledged producer requests. MUST be ≤5 when
|
|
149
|
+
* `idempotent: true`. Higher = throughput; lower = stricter ordering on
|
|
150
|
+
* non-idempotent producers (no other path preserves order on retry).
|
|
151
|
+
*/
|
|
152
|
+
maxInFlightRequests?: number;
|
|
153
|
+
/** Per-request broker-ack timeout. Default 30 s. */
|
|
154
|
+
requestTimeoutMs?: number;
|
|
155
|
+
/**
|
|
156
|
+
* (confluent only) End-to-end timeout for a record from produce() call to
|
|
157
|
+
* terminal success / failure (includes retries). Defaults to 120 s.
|
|
158
|
+
* If this exceeds the relay's `claimTimeoutMs`, the reaper may double-
|
|
159
|
+
* publish a slow record — set both coherently.
|
|
160
|
+
*/
|
|
161
|
+
deliveryTimeoutMs?: number;
|
|
162
|
+
/**
|
|
163
|
+
* (confluent only) Max bytes of a single record (after compression).
|
|
164
|
+
* MUST be ≤ broker's `message.max.bytes`. Defaults to 1 MB.
|
|
165
|
+
*/
|
|
166
|
+
maxRequestSize?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Broker-side ceiling on how long a transaction can stay open before
|
|
169
|
+
* auto-abort. Maps to `transaction.timeout.ms`. Default 60 s; capped by
|
|
170
|
+
* the broker's `transaction.max.timeout.ms`.
|
|
171
|
+
*/
|
|
172
|
+
transactionTimeoutMs?: number;
|
|
173
|
+
/**
|
|
174
|
+
* (kafkajs only) Choice of partitioner. See
|
|
175
|
+
* {@link KafkaJsPartitionerChoice} for the options. Setting any value
|
|
176
|
+
* silences kafkajs's `KafkaJSPartitionerNotSpecified` warning.
|
|
177
|
+
*/
|
|
178
|
+
partitioner?: KafkaJsPartitionerChoice;
|
|
48
179
|
}
|
|
49
180
|
type DriverKind = "kafkajs" | "confluent";
|
|
50
181
|
|
|
@@ -78,6 +209,8 @@ declare class KafkaJsDriver implements KafkaDriver {
|
|
|
78
209
|
disconnect(): Promise<void>;
|
|
79
210
|
sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
|
|
80
211
|
}
|
|
212
|
+
/** Internal — used by tests. Resets the dedup so warnings can be observed in isolation. */
|
|
213
|
+
declare function _resetKafkajsWarnDedup(): void;
|
|
81
214
|
|
|
82
215
|
/**
|
|
83
216
|
* Classify a kafkajs producer error into a {@link PublishErrorKind} so the
|
|
@@ -144,6 +277,28 @@ declare class ConfluentDriver implements KafkaDriver {
|
|
|
144
277
|
*/
|
|
145
278
|
declare function classifyConfluentError(err: unknown): PublishErrorKind;
|
|
146
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Translate eventferry's normalized `KafkaConnectionConfig` into the shape
|
|
282
|
+
* expected by `@confluentinc/kafka-javascript`'s `KafkaJS.Kafka` constructor.
|
|
283
|
+
*
|
|
284
|
+
* Returns an object with two parts:
|
|
285
|
+
* - `kafkaJS`: the kafkajs-compatible config layer (clientId, brokers, and
|
|
286
|
+
* simple ssl/sasl when no advanced TLS is needed).
|
|
287
|
+
* - top-level keys: librdkafka-style config (e.g. `ssl.ca.pem`,
|
|
288
|
+
* `security.protocol`) used when the user supplies a {@link TlsConfig}.
|
|
289
|
+
*
|
|
290
|
+
* Why a separate translator: the kafkajs-compat layer accepts the simple
|
|
291
|
+
* `ssl: true` boolean but the verified path for mTLS (CA + cert + key) is
|
|
292
|
+
* librdkafka's `ssl.*.pem` keys. The translator picks the right surface
|
|
293
|
+
* based on what the caller supplied. Buffer inputs are coerced to strings —
|
|
294
|
+
* librdkafka accepts PEM strings, NOT Buffers.
|
|
295
|
+
*/
|
|
296
|
+
interface ConfluentClientConfig {
|
|
297
|
+
kafkaJS: Record<string, unknown>;
|
|
298
|
+
librdkafka: Record<string, unknown>;
|
|
299
|
+
}
|
|
300
|
+
declare function buildConfluentClientConfig(opts: KafkaConnectionConfig & ProducerBehaviorConfig): ConfluentClientConfig;
|
|
301
|
+
|
|
147
302
|
interface KafkaPublisherOptions extends KafkaConnectionConfig, ProducerBehaviorConfig {
|
|
148
303
|
/** Which underlying client to use. Default "kafkajs". */
|
|
149
304
|
driver?: DriverKind;
|
|
@@ -173,4 +328,4 @@ declare class KafkaPublisher implements Publisher {
|
|
|
173
328
|
get transactional(): boolean;
|
|
174
329
|
}
|
|
175
330
|
|
|
176
|
-
export { ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, KafkaPublisher, type KafkaPublisherOptions, type ProducerBehaviorConfig, classifyConfluentError, classifyKafkajsError };
|
|
331
|
+
export { type ConfluentClientConfig, ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, type KafkaJsPartitionerChoice, KafkaPublisher, type KafkaPublisherOptions, type OauthBearerToken, type ProducerBehaviorConfig, type SaslConfig, type SaslOauthbearerConfig, type SaslPasswordConfig, type TlsConfig, _resetKafkajsWarnDedup, buildConfluentClientConfig, classifyConfluentError, classifyKafkajsError };
|
package/dist/index.js
CHANGED
|
@@ -86,6 +86,12 @@ var CODE_TO_KIND = /* @__PURE__ */ new Map([
|
|
|
86
86
|
]);
|
|
87
87
|
|
|
88
88
|
// src/kafkajs-driver.ts
|
|
89
|
+
var UNSUPPORTED_BY_KAFKAJS = [
|
|
90
|
+
"lingerMs",
|
|
91
|
+
"batchSize",
|
|
92
|
+
"deliveryTimeoutMs",
|
|
93
|
+
"maxRequestSize"
|
|
94
|
+
];
|
|
89
95
|
var KafkaJsDriver = class {
|
|
90
96
|
transactional;
|
|
91
97
|
producer = null;
|
|
@@ -98,6 +104,7 @@ var KafkaJsDriver = class {
|
|
|
98
104
|
"KafkaJsDriver: transactionalId is required when transactional=true"
|
|
99
105
|
);
|
|
100
106
|
}
|
|
107
|
+
warnUnsupportedKafkajsOptions(opts);
|
|
101
108
|
}
|
|
102
109
|
async connect() {
|
|
103
110
|
this.producer = await this.createProducer();
|
|
@@ -112,13 +119,36 @@ var KafkaJsDriver = class {
|
|
|
112
119
|
const kafka = new mod.Kafka({
|
|
113
120
|
clientId: this.opts.clientId ?? "eventferry",
|
|
114
121
|
brokers: this.opts.brokers,
|
|
122
|
+
// kafkajs accepts `ssl: tls.ConnectionOptions` directly — Buffer + PEM
|
|
123
|
+
// string both supported. Our TlsConfig is a structural subset of that
|
|
124
|
+
// (`rejectUnauthorized` intentionally omitted; the cluster CA goes via
|
|
125
|
+
// `ca`). No translation needed.
|
|
115
126
|
ssl: this.opts.ssl,
|
|
127
|
+
// SASL: PLAIN / SCRAM-SHA-256 / SCRAM-SHA-512 / OAUTHBEARER. kafkajs's
|
|
128
|
+
// shape matches ours; for OAUTHBEARER kafkajs reads only `value` from
|
|
129
|
+
// the provider's returned token (other fields are ignored).
|
|
116
130
|
sasl: this.opts.sasl
|
|
117
131
|
});
|
|
132
|
+
const createPartitioner = resolveCreatePartitioner(
|
|
133
|
+
mod.Partitioners,
|
|
134
|
+
this.opts.partitioner,
|
|
135
|
+
this.transactional
|
|
136
|
+
);
|
|
118
137
|
return kafka.producer({
|
|
119
138
|
idempotent: this.opts.idempotent ?? true,
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
// Idempotent / transactional producers cap maxInFlight at 5. When the
|
|
140
|
+
// user picks transactional we force 1 to keep strict ordering across
|
|
141
|
+
// retries on classic (non-idempotent) clusters that haven't migrated
|
|
142
|
+
// to the broker-side fence.
|
|
143
|
+
maxInFlightRequests: this.transactional ? 1 : this.opts.maxInFlightRequests,
|
|
144
|
+
transactionalId: this.transactional ? this.opts.transactionalId : void 0,
|
|
145
|
+
// kafkajs accepts these directly when set; undefined falls through to
|
|
146
|
+
// the kafkajs default.
|
|
147
|
+
requestTimeout: this.opts.requestTimeoutMs,
|
|
148
|
+
transactionTimeout: this.opts.transactionTimeoutMs,
|
|
149
|
+
// Setting any partitioner choice silences kafkajs's
|
|
150
|
+
// KafkaJSPartitionerNotSpecified warning.
|
|
151
|
+
createPartitioner
|
|
122
152
|
});
|
|
123
153
|
}
|
|
124
154
|
async disconnect() {
|
|
@@ -164,7 +194,12 @@ function groupByTopic(messages, compression) {
|
|
|
164
194
|
arr.push({
|
|
165
195
|
key: m.key,
|
|
166
196
|
value: m.value,
|
|
167
|
-
headers: m.headers
|
|
197
|
+
headers: m.headers,
|
|
198
|
+
// Per-message partition override. When set, kafkajs routes the record
|
|
199
|
+
// to this exact partition; when undefined, the configured partitioner
|
|
200
|
+
// chooses. We keep the key here too because compacted topics need it
|
|
201
|
+
// even when partition is pinned.
|
|
202
|
+
...m.partition !== void 0 ? { partition: m.partition } : {}
|
|
168
203
|
});
|
|
169
204
|
byTopic.set(m.topic, arr);
|
|
170
205
|
}
|
|
@@ -174,6 +209,32 @@ function groupByTopic(messages, compression) {
|
|
|
174
209
|
...compression && compression !== "none" ? { compression } : {}
|
|
175
210
|
}));
|
|
176
211
|
}
|
|
212
|
+
function resolveCreatePartitioner(partitioners, choice, transactional) {
|
|
213
|
+
if (!partitioners) return void 0;
|
|
214
|
+
const effective = choice ?? (transactional ? "default" : "java-compatible");
|
|
215
|
+
switch (effective) {
|
|
216
|
+
case "java-compatible":
|
|
217
|
+
return partitioners.JavaCompatiblePartitioner;
|
|
218
|
+
case "legacy":
|
|
219
|
+
return partitioners.LegacyPartitioner;
|
|
220
|
+
case "default":
|
|
221
|
+
return partitioners.DefaultPartitioner;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
var warnedKafkajsKeys = /* @__PURE__ */ new Set();
|
|
225
|
+
function warnUnsupportedKafkajsOptions(opts) {
|
|
226
|
+
for (const key of UNSUPPORTED_BY_KAFKAJS) {
|
|
227
|
+
if (opts[key] === void 0) continue;
|
|
228
|
+
if (warnedKafkajsKeys.has(key)) continue;
|
|
229
|
+
warnedKafkajsKeys.add(key);
|
|
230
|
+
console.warn(
|
|
231
|
+
`[@eventferry/kafka] '${key}' is not configurable on the kafkajs driver and was ignored. Switch to the confluent driver (driver: "confluent") for fine-grained tuning, or remove the option to silence this warning.`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function _resetKafkajsWarnDedup() {
|
|
236
|
+
warnedKafkajsKeys.clear();
|
|
237
|
+
}
|
|
177
238
|
async function importKafkaJs() {
|
|
178
239
|
try {
|
|
179
240
|
return await import("kafkajs");
|
|
@@ -282,6 +343,71 @@ var NAME_TO_KIND = /* @__PURE__ */ new Map([
|
|
|
282
343
|
["ERR_THROTTLING_QUOTA_EXCEEDED", "quota"]
|
|
283
344
|
]);
|
|
284
345
|
|
|
346
|
+
// src/confluent-config.ts
|
|
347
|
+
function buildConfluentClientConfig(opts) {
|
|
348
|
+
const kafkaJS = {
|
|
349
|
+
clientId: opts.clientId ?? "eventferry",
|
|
350
|
+
brokers: opts.brokers
|
|
351
|
+
};
|
|
352
|
+
const librdkafka = {};
|
|
353
|
+
if (opts.lingerMs !== void 0) librdkafka["linger.ms"] = opts.lingerMs;
|
|
354
|
+
if (opts.batchSize !== void 0) librdkafka["batch.size"] = opts.batchSize;
|
|
355
|
+
if (opts.maxInFlightRequests !== void 0) {
|
|
356
|
+
librdkafka["max.in.flight.requests.per.connection"] = opts.maxInFlightRequests;
|
|
357
|
+
}
|
|
358
|
+
if (opts.requestTimeoutMs !== void 0) {
|
|
359
|
+
librdkafka["request.timeout.ms"] = opts.requestTimeoutMs;
|
|
360
|
+
}
|
|
361
|
+
if (opts.deliveryTimeoutMs !== void 0) {
|
|
362
|
+
librdkafka["delivery.timeout.ms"] = opts.deliveryTimeoutMs;
|
|
363
|
+
}
|
|
364
|
+
if (opts.maxRequestSize !== void 0) {
|
|
365
|
+
librdkafka["message.max.bytes"] = opts.maxRequestSize;
|
|
366
|
+
}
|
|
367
|
+
if (opts.transactionTimeoutMs !== void 0) {
|
|
368
|
+
librdkafka["transaction.timeout.ms"] = opts.transactionTimeoutMs;
|
|
369
|
+
}
|
|
370
|
+
const tlsRequested = opts.ssl === true || isTlsConfig(opts.ssl);
|
|
371
|
+
const saslRequested = !!opts.sasl;
|
|
372
|
+
if (saslRequested && tlsRequested) {
|
|
373
|
+
librdkafka["security.protocol"] = "sasl_ssl";
|
|
374
|
+
} else if (tlsRequested) {
|
|
375
|
+
librdkafka["security.protocol"] = "ssl";
|
|
376
|
+
} else if (saslRequested) {
|
|
377
|
+
librdkafka["security.protocol"] = "sasl_plaintext";
|
|
378
|
+
}
|
|
379
|
+
if (isTlsConfig(opts.ssl)) {
|
|
380
|
+
const tls = opts.ssl;
|
|
381
|
+
if (tls.ca !== void 0) {
|
|
382
|
+
librdkafka["ssl.ca.pem"] = stringifyPem(tls.ca);
|
|
383
|
+
}
|
|
384
|
+
if (tls.cert !== void 0) {
|
|
385
|
+
librdkafka["ssl.certificate.pem"] = stringifyPem(tls.cert);
|
|
386
|
+
}
|
|
387
|
+
if (tls.key !== void 0) {
|
|
388
|
+
librdkafka["ssl.key.pem"] = stringifyPem(tls.key);
|
|
389
|
+
}
|
|
390
|
+
if (tls.passphrase !== void 0) {
|
|
391
|
+
librdkafka["ssl.key.password"] = tls.passphrase;
|
|
392
|
+
}
|
|
393
|
+
} else if (opts.ssl === true) {
|
|
394
|
+
kafkaJS["ssl"] = true;
|
|
395
|
+
}
|
|
396
|
+
if (opts.sasl) {
|
|
397
|
+
kafkaJS["sasl"] = opts.sasl;
|
|
398
|
+
}
|
|
399
|
+
return { kafkaJS, librdkafka };
|
|
400
|
+
}
|
|
401
|
+
function isTlsConfig(v) {
|
|
402
|
+
return typeof v === "object" && v !== null;
|
|
403
|
+
}
|
|
404
|
+
function stringifyPem(input) {
|
|
405
|
+
if (Array.isArray(input)) {
|
|
406
|
+
return input.map((x) => typeof x === "string" ? x : x.toString("utf8")).join("\n");
|
|
407
|
+
}
|
|
408
|
+
return typeof input === "string" ? input : input.toString("utf8");
|
|
409
|
+
}
|
|
410
|
+
|
|
285
411
|
// src/confluent-driver.ts
|
|
286
412
|
var ConfluentDriver = class {
|
|
287
413
|
transactional;
|
|
@@ -306,13 +432,10 @@ var ConfluentDriver = class {
|
|
|
306
432
|
*/
|
|
307
433
|
async createProducer() {
|
|
308
434
|
const mod = await importConfluent();
|
|
435
|
+
const { kafkaJS, librdkafka } = buildConfluentClientConfig(this.opts);
|
|
309
436
|
const kafka = new mod.KafkaJS.Kafka({
|
|
310
|
-
kafkaJS
|
|
311
|
-
|
|
312
|
-
brokers: this.opts.brokers,
|
|
313
|
-
ssl: this.opts.ssl,
|
|
314
|
-
sasl: this.opts.sasl
|
|
315
|
-
}
|
|
437
|
+
kafkaJS,
|
|
438
|
+
...librdkafka
|
|
316
439
|
});
|
|
317
440
|
return kafka.producer({
|
|
318
441
|
kafkaJS: {
|
|
@@ -373,7 +496,14 @@ function groupByTopic2(messages) {
|
|
|
373
496
|
const byTopic = /* @__PURE__ */ new Map();
|
|
374
497
|
for (const m of messages) {
|
|
375
498
|
const arr = byTopic.get(m.topic) ?? [];
|
|
376
|
-
arr.push({
|
|
499
|
+
arr.push({
|
|
500
|
+
key: m.key,
|
|
501
|
+
value: m.value,
|
|
502
|
+
headers: m.headers,
|
|
503
|
+
// Per-message partition override. librdkafka honors an explicit
|
|
504
|
+
// partition value; undefined leaves the default partitioner in charge.
|
|
505
|
+
...m.partition !== void 0 ? { partition: m.partition } : {}
|
|
506
|
+
});
|
|
377
507
|
byTopic.set(m.topic, arr);
|
|
378
508
|
}
|
|
379
509
|
return [...byTopic.entries()].map(([topic, msgs]) => ({
|
|
@@ -445,6 +575,8 @@ export {
|
|
|
445
575
|
ConfluentDriver,
|
|
446
576
|
KafkaJsDriver,
|
|
447
577
|
KafkaPublisher,
|
|
578
|
+
_resetKafkajsWarnDedup,
|
|
579
|
+
buildConfluentClientConfig,
|
|
448
580
|
classifyConfluentError,
|
|
449
581
|
classifyKafkajsError
|
|
450
582
|
};
|