@drarzter/kafka-client 0.9.4 → 0.11.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 +693 -8
- package/dist/chunk-OR7TPAAE.mjs +4760 -0
- package/dist/chunk-OR7TPAAE.mjs.map +1 -0
- package/dist/chunk-PQVBRDNV.mjs +149 -0
- package/dist/chunk-PQVBRDNV.mjs.map +1 -0
- package/dist/cli/dlq.d.ts +119 -0
- package/dist/cli/dlq.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/{chunk-SM4FZKAZ.mjs → cli/index.js} +1073 -309
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +356 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/config/from-env.d.ts +188 -0
- package/dist/client/config/from-env.d.ts.map +1 -0
- package/dist/client/config/index.d.ts +2 -0
- package/dist/client/config/index.d.ts.map +1 -0
- package/dist/client/errors.d.ts +67 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/kafka.client/admin/ops.d.ts +114 -0
- package/dist/client/kafka.client/admin/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/delayed.d.ts +24 -0
- package/dist/client/kafka.client/consumer/features/delayed.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts +52 -0
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/routed.d.ts +4 -0
- package/dist/client/kafka.client/consumer/features/routed.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/snapshot.d.ts +10 -0
- package/dist/client/kafka.client/consumer/features/snapshot.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/window.d.ts +5 -0
- package/dist/client/kafka.client/consumer/features/window.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/handler.d.ts +163 -0
- package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/ops.d.ts +64 -0
- package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/pipeline.d.ts +168 -0
- package/dist/client/kafka.client/consumer/pipeline.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/queue.d.ts +37 -0
- package/dist/client/kafka.client/consumer/queue.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/retry-topic.d.ts +68 -0
- package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/setup.d.ts +66 -0
- package/dist/client/kafka.client/consumer/setup.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/start.d.ts +7 -0
- package/dist/client/kafka.client/consumer/start.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/stop.d.ts +19 -0
- package/dist/client/kafka.client/consumer/stop.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/subscribe-retry.d.ts +4 -0
- package/dist/client/kafka.client/consumer/subscribe-retry.d.ts.map +1 -0
- package/dist/client/kafka.client/context.d.ts +75 -0
- package/dist/client/kafka.client/context.d.ts.map +1 -0
- package/dist/client/kafka.client/index.d.ts +155 -0
- package/dist/client/kafka.client/index.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts +61 -0
- package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/dedup.store.d.ts +28 -0
- package/dist/client/kafka.client/infra/dedup.store.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/inflight.tracker.d.ts +22 -0
- package/dist/client/kafka.client/infra/inflight.tracker.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/metrics.manager.d.ts +67 -0
- package/dist/client/kafka.client/infra/metrics.manager.d.ts.map +1 -0
- package/dist/client/kafka.client/producer/lifecycle.d.ts +41 -0
- package/dist/client/kafka.client/producer/lifecycle.d.ts.map +1 -0
- package/dist/client/kafka.client/producer/ops.d.ts +79 -0
- package/dist/client/kafka.client/producer/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/producer/send.d.ts +21 -0
- package/dist/client/kafka.client/producer/send.d.ts.map +1 -0
- package/dist/client/kafka.client/validate-options.d.ts +11 -0
- package/dist/client/kafka.client/validate-options.d.ts.map +1 -0
- package/dist/client/message/envelope.d.ts +105 -0
- package/dist/client/message/envelope.d.ts.map +1 -0
- package/dist/client/message/schema-registry.d.ts +124 -0
- package/dist/client/message/schema-registry.d.ts.map +1 -0
- package/dist/client/message/serde.d.ts +68 -0
- package/dist/client/message/serde.d.ts.map +1 -0
- package/dist/client/message/topic.d.ts +159 -0
- package/dist/client/message/topic.d.ts.map +1 -0
- package/dist/client/message/versioned-schema.d.ts +53 -0
- package/dist/client/message/versioned-schema.d.ts.map +1 -0
- package/dist/client/outbox/index.d.ts +4 -0
- package/dist/client/outbox/index.d.ts.map +1 -0
- package/dist/client/outbox/outbox.relay.d.ts +90 -0
- package/dist/client/outbox/outbox.relay.d.ts.map +1 -0
- package/dist/client/outbox/outbox.store.d.ts +42 -0
- package/dist/client/outbox/outbox.store.d.ts.map +1 -0
- package/dist/client/outbox/outbox.types.d.ts +144 -0
- package/dist/client/outbox/outbox.types.d.ts.map +1 -0
- package/dist/client/security/acl.d.ts +108 -0
- package/dist/client/security/acl.d.ts.map +1 -0
- package/dist/client/security/index.d.ts +5 -0
- package/dist/client/security/index.d.ts.map +1 -0
- package/dist/client/security/providers.d.ts +88 -0
- package/dist/client/security/providers.d.ts.map +1 -0
- package/dist/client/security/resolve-security.d.ts +19 -0
- package/dist/client/security/resolve-security.d.ts.map +1 -0
- package/dist/client/security/security.types.d.ts +76 -0
- package/dist/client/security/security.types.d.ts.map +1 -0
- package/dist/client/transport/confluent.transport.d.ts +32 -0
- package/dist/client/transport/confluent.transport.d.ts.map +1 -0
- package/dist/client/transport/transport.interface.d.ts +221 -0
- package/dist/client/transport/transport.interface.d.ts.map +1 -0
- package/dist/client/types/admin.interface.d.ts +174 -0
- package/dist/client/types/admin.interface.d.ts.map +1 -0
- package/dist/client/types/admin.types.d.ts +140 -0
- package/dist/client/types/admin.types.d.ts.map +1 -0
- package/dist/client/types/client.d.ts +21 -0
- package/dist/client/types/client.d.ts.map +1 -0
- package/dist/client/types/common.d.ts +84 -0
- package/dist/client/types/common.d.ts.map +1 -0
- package/dist/client/types/config.types.d.ts +167 -0
- package/dist/client/types/config.types.d.ts.map +1 -0
- package/dist/client/types/consumer.interface.d.ts +115 -0
- package/dist/client/types/consumer.interface.d.ts.map +1 -0
- package/dist/{consumer.types-fFCag3VJ.d.mts → client/types/consumer.types.d.ts} +62 -383
- package/dist/client/types/consumer.types.d.ts.map +1 -0
- package/dist/client/types/dedup.types.d.ts +50 -0
- package/dist/client/types/dedup.types.d.ts.map +1 -0
- package/dist/client/types/lifecycle.interface.d.ts +72 -0
- package/dist/client/types/lifecycle.interface.d.ts.map +1 -0
- package/dist/client/types/producer.interface.d.ts +52 -0
- package/dist/client/types/producer.interface.d.ts.map +1 -0
- package/dist/client/types/producer.types.d.ts +90 -0
- package/dist/client/types/producer.types.d.ts.map +1 -0
- package/dist/client/types.d.ts +8 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/core.d.ts +13 -314
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1466 -123
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +45 -3
- package/dist/index.d.ts +7 -128
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1483 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -3
- package/dist/index.mjs.map +1 -1
- package/dist/nest/kafka.constants.d.ts +5 -0
- package/dist/nest/kafka.constants.d.ts.map +1 -0
- package/dist/nest/kafka.decorator.d.ts +49 -0
- package/dist/nest/kafka.decorator.d.ts.map +1 -0
- package/dist/nest/kafka.explorer.d.ts +17 -0
- package/dist/nest/kafka.explorer.d.ts.map +1 -0
- package/dist/nest/kafka.health.d.ts +7 -0
- package/dist/nest/kafka.health.d.ts.map +1 -0
- package/dist/nest/kafka.module.d.ts +61 -0
- package/dist/nest/kafka.module.d.ts.map +1 -0
- package/dist/otel.d.ts +83 -5
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +100 -6
- package/dist/otel.js.map +1 -1
- package/dist/otel.mjs +98 -5
- package/dist/otel.mjs.map +1 -1
- package/dist/serde.d.ts +157 -0
- package/dist/serde.d.ts.map +1 -0
- package/dist/serde.js +308 -0
- package/dist/serde.js.map +1 -0
- package/dist/serde.mjs +158 -0
- package/dist/serde.mjs.map +1 -0
- package/dist/testing/client.mock.d.ts +47 -0
- package/dist/testing/client.mock.d.ts.map +1 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/test.container.d.ts +63 -0
- package/dist/testing/test.container.d.ts.map +1 -0
- package/dist/{testing.d.mts → testing/transport.fake.d.ts} +7 -111
- package/dist/testing/transport.fake.d.ts.map +1 -0
- package/dist/testing.d.ts +2 -318
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +26 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +26 -0
- package/dist/testing.mjs.map +1 -1
- package/package.json +40 -8
- package/dist/chunk-SM4FZKAZ.mjs.map +0 -1
- package/dist/client-1irhGEu0.d.mts +0 -751
- package/dist/client-BpFjkHhr.d.ts +0 -751
- package/dist/consumer.types-fFCag3VJ.d.ts +0 -958
- package/dist/core.d.mts +0 -314
- package/dist/index.d.mts +0 -128
- package/dist/otel.d.mts +0 -27
package/dist/core.js
CHANGED
|
@@ -20,32 +20,53 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/core.ts
|
|
21
21
|
var core_exports = {};
|
|
22
22
|
__export(core_exports, {
|
|
23
|
+
ConfluentTransport: () => ConfluentTransport,
|
|
23
24
|
HEADER_CORRELATION_ID: () => HEADER_CORRELATION_ID,
|
|
25
|
+
HEADER_DELAYED_TARGET: () => HEADER_DELAYED_TARGET,
|
|
26
|
+
HEADER_DELAYED_UNTIL: () => HEADER_DELAYED_UNTIL,
|
|
24
27
|
HEADER_EVENT_ID: () => HEADER_EVENT_ID,
|
|
25
28
|
HEADER_LAMPORT_CLOCK: () => HEADER_LAMPORT_CLOCK,
|
|
26
29
|
HEADER_SCHEMA_VERSION: () => HEADER_SCHEMA_VERSION,
|
|
27
30
|
HEADER_TIMESTAMP: () => HEADER_TIMESTAMP,
|
|
28
31
|
HEADER_TRACEPARENT: () => HEADER_TRACEPARENT,
|
|
32
|
+
InMemoryDedupStore: () => InMemoryDedupStore,
|
|
33
|
+
InMemoryOutboxStore: () => InMemoryOutboxStore,
|
|
34
|
+
JsonSerde: () => JsonSerde,
|
|
29
35
|
KafkaClient: () => KafkaClient,
|
|
30
36
|
KafkaProcessingError: () => KafkaProcessingError,
|
|
31
37
|
KafkaRetryExhaustedError: () => KafkaRetryExhaustedError,
|
|
32
38
|
KafkaValidationError: () => KafkaValidationError,
|
|
39
|
+
SchemaRegistryClient: () => SchemaRegistryClient,
|
|
40
|
+
awsMskIamProvider: () => awsMskIamProvider,
|
|
33
41
|
buildEnvelopeHeaders: () => buildEnvelopeHeaders,
|
|
42
|
+
consumerOptionsFromEnv: () => consumerOptionsFromEnv,
|
|
34
43
|
decodeHeaders: () => decodeHeaders,
|
|
44
|
+
describeRequiredAcls: () => describeRequiredAcls,
|
|
35
45
|
extractEnvelope: () => extractEnvelope,
|
|
46
|
+
gcpAccessTokenProvider: () => gcpAccessTokenProvider,
|
|
36
47
|
getEnvelopeContext: () => getEnvelopeContext,
|
|
48
|
+
kafkaClientConfigFromEnv: () => kafkaClientConfigFromEnv,
|
|
49
|
+
mergeConsumerOptions: () => mergeConsumerOptions,
|
|
50
|
+
registrySchema: () => registrySchema,
|
|
51
|
+
resolveSecurityOptions: () => resolveSecurityOptions,
|
|
37
52
|
runWithEnvelopeContext: () => runWithEnvelopeContext,
|
|
38
|
-
|
|
53
|
+
startOutboxRelay: () => startOutboxRelay,
|
|
54
|
+
toError: () => toError,
|
|
55
|
+
toKafkaAclCommands: () => toKafkaAclCommands,
|
|
56
|
+
toMskIamPolicy: () => toMskIamPolicy,
|
|
57
|
+
topic: () => topic,
|
|
58
|
+
versionedSchema: () => versionedSchema
|
|
39
59
|
});
|
|
40
60
|
module.exports = __toCommonJS(core_exports);
|
|
41
61
|
|
|
42
|
-
// src/client/
|
|
62
|
+
// src/client/transport/confluent.transport.ts
|
|
43
63
|
var import_kafka_javascript = require("@confluentinc/kafka-javascript");
|
|
44
64
|
var { Kafka: KafkaClass, logLevel: KafkaLogLevel, PartitionAssigners } = import_kafka_javascript.KafkaJS;
|
|
45
65
|
var ConfluentTransaction = class {
|
|
46
66
|
constructor(tx) {
|
|
47
67
|
this.tx = tx;
|
|
48
68
|
}
|
|
69
|
+
tx;
|
|
49
70
|
async send(record) {
|
|
50
71
|
await this.tx.send(record);
|
|
51
72
|
}
|
|
@@ -67,10 +88,17 @@ var ConfluentProducer = class {
|
|
|
67
88
|
constructor(producer) {
|
|
68
89
|
this.producer = producer;
|
|
69
90
|
}
|
|
91
|
+
producer;
|
|
92
|
+
connectPromise;
|
|
70
93
|
async connect() {
|
|
71
|
-
|
|
94
|
+
this.connectPromise ??= this.producer.connect().catch((err) => {
|
|
95
|
+
this.connectPromise = void 0;
|
|
96
|
+
throw err;
|
|
97
|
+
});
|
|
98
|
+
return this.connectPromise;
|
|
72
99
|
}
|
|
73
100
|
async disconnect() {
|
|
101
|
+
this.connectPromise = void 0;
|
|
74
102
|
await this.producer.disconnect();
|
|
75
103
|
}
|
|
76
104
|
async send(record) {
|
|
@@ -85,6 +113,7 @@ var ConfluentConsumer = class {
|
|
|
85
113
|
constructor(consumer) {
|
|
86
114
|
this.consumer = consumer;
|
|
87
115
|
}
|
|
116
|
+
consumer;
|
|
88
117
|
/** Returns the underlying KafkaJS.Consumer — used by ConfluentTransaction.sendOffsets. */
|
|
89
118
|
getNative() {
|
|
90
119
|
return this.consumer;
|
|
@@ -124,6 +153,7 @@ var ConfluentAdmin = class {
|
|
|
124
153
|
constructor(admin) {
|
|
125
154
|
this.admin = admin;
|
|
126
155
|
}
|
|
156
|
+
admin;
|
|
127
157
|
async connect() {
|
|
128
158
|
await this.admin.connect();
|
|
129
159
|
}
|
|
@@ -137,7 +167,7 @@ var ConfluentAdmin = class {
|
|
|
137
167
|
return this.admin.fetchTopicOffsets(topic2);
|
|
138
168
|
}
|
|
139
169
|
async fetchTopicOffsetsByTimestamp(topic2, timestamp) {
|
|
140
|
-
return this.admin.
|
|
170
|
+
return this.admin.fetchTopicOffsetsByTimestamp(topic2, timestamp);
|
|
141
171
|
}
|
|
142
172
|
async fetchOffsets(options) {
|
|
143
173
|
return this.admin.fetchOffsets(options);
|
|
@@ -163,10 +193,29 @@ var ConfluentAdmin = class {
|
|
|
163
193
|
};
|
|
164
194
|
var ConfluentTransport = class {
|
|
165
195
|
kafka;
|
|
166
|
-
constructor(clientId, brokers) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
196
|
+
constructor(clientId, brokers, security) {
|
|
197
|
+
const kafkaJS = { clientId, brokers, logLevel: KafkaLogLevel.ERROR };
|
|
198
|
+
if (security?.ssl !== void 0) kafkaJS.ssl = security.ssl;
|
|
199
|
+
if (security?.sasl) {
|
|
200
|
+
if (security.sasl.mechanism === "oauthbearer") {
|
|
201
|
+
const provider = security.sasl.oauthBearerProvider;
|
|
202
|
+
kafkaJS.sasl = {
|
|
203
|
+
mechanism: "oauthbearer",
|
|
204
|
+
oauthBearerProvider: async () => {
|
|
205
|
+
const token = await provider();
|
|
206
|
+
return {
|
|
207
|
+
value: token.value,
|
|
208
|
+
principal: token.principal ?? "kafka-client",
|
|
209
|
+
lifetime: token.lifetimeMs ?? Date.now() + 15 * 6e4,
|
|
210
|
+
...token.extensions && { extensions: token.extensions }
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
} else {
|
|
215
|
+
kafkaJS.sasl = security.sasl;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
this.kafka = new KafkaClass({ kafkaJS });
|
|
170
219
|
}
|
|
171
220
|
producer(options) {
|
|
172
221
|
const native = this.kafka.producer({
|
|
@@ -193,6 +242,9 @@ var ConfluentTransport = class {
|
|
|
193
242
|
partitionAssigners: [assigner]
|
|
194
243
|
}
|
|
195
244
|
};
|
|
245
|
+
if (options.groupInstanceId) {
|
|
246
|
+
config["group.instance.id"] = options.groupInstanceId;
|
|
247
|
+
}
|
|
196
248
|
if (options.onRebalance) {
|
|
197
249
|
const cb = options.onRebalance;
|
|
198
250
|
config.rebalance_cb = (err, assignment) => {
|
|
@@ -210,6 +262,37 @@ var ConfluentTransport = class {
|
|
|
210
262
|
}
|
|
211
263
|
};
|
|
212
264
|
|
|
265
|
+
// src/client/message/serde.ts
|
|
266
|
+
var JsonSerde = class {
|
|
267
|
+
/** JSON-stringify the validated payload. Returns a UTF-8 string. */
|
|
268
|
+
serialize(value) {
|
|
269
|
+
return JSON.stringify(value);
|
|
270
|
+
}
|
|
271
|
+
/** JSON-parse UTF-8 wire bytes into an object. */
|
|
272
|
+
deserialize(data) {
|
|
273
|
+
return JSON.parse(data.toString("utf8"));
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/client/kafka.client/infra/dedup.store.ts
|
|
278
|
+
var InMemoryDedupStore = class {
|
|
279
|
+
constructor(states) {
|
|
280
|
+
this.states = states;
|
|
281
|
+
}
|
|
282
|
+
states;
|
|
283
|
+
getLastClock(groupId, topicPartition) {
|
|
284
|
+
return this.states.get(groupId)?.get(topicPartition);
|
|
285
|
+
}
|
|
286
|
+
setLastClock(groupId, topicPartition, clock) {
|
|
287
|
+
let group = this.states.get(groupId);
|
|
288
|
+
if (!group) {
|
|
289
|
+
group = /* @__PURE__ */ new Map();
|
|
290
|
+
this.states.set(groupId, group);
|
|
291
|
+
}
|
|
292
|
+
group.set(topicPartition, clock);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
213
296
|
// src/client/message/envelope.ts
|
|
214
297
|
var import_node_async_hooks = require("async_hooks");
|
|
215
298
|
var import_node_crypto = require("crypto");
|
|
@@ -219,6 +302,8 @@ var HEADER_TIMESTAMP = "x-timestamp";
|
|
|
219
302
|
var HEADER_SCHEMA_VERSION = "x-schema-version";
|
|
220
303
|
var HEADER_TRACEPARENT = "traceparent";
|
|
221
304
|
var HEADER_LAMPORT_CLOCK = "x-lamport-clock";
|
|
305
|
+
var HEADER_DELAYED_UNTIL = "x-delayed-until";
|
|
306
|
+
var HEADER_DELAYED_TARGET = "x-delayed-target";
|
|
222
307
|
var envelopeStorage = new import_node_async_hooks.AsyncLocalStorage();
|
|
223
308
|
function getEnvelopeContext() {
|
|
224
309
|
return envelopeStorage.getStore();
|
|
@@ -273,6 +358,9 @@ function extractEnvelope(payload, headers, topic2, partition, offset) {
|
|
|
273
358
|
}
|
|
274
359
|
|
|
275
360
|
// src/client/errors.ts
|
|
361
|
+
function toError(error) {
|
|
362
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
363
|
+
}
|
|
276
364
|
var KafkaProcessingError = class extends Error {
|
|
277
365
|
constructor(message, topic2, originalMessage, options) {
|
|
278
366
|
super(message, options);
|
|
@@ -281,6 +369,8 @@ var KafkaProcessingError = class extends Error {
|
|
|
281
369
|
this.name = "KafkaProcessingError";
|
|
282
370
|
if (options?.cause) this.cause = options.cause;
|
|
283
371
|
}
|
|
372
|
+
topic;
|
|
373
|
+
originalMessage;
|
|
284
374
|
};
|
|
285
375
|
var KafkaValidationError = class extends Error {
|
|
286
376
|
constructor(topic2, originalMessage, options) {
|
|
@@ -290,6 +380,8 @@ var KafkaValidationError = class extends Error {
|
|
|
290
380
|
this.name = "KafkaValidationError";
|
|
291
381
|
if (options?.cause) this.cause = options.cause;
|
|
292
382
|
}
|
|
383
|
+
topic;
|
|
384
|
+
originalMessage;
|
|
293
385
|
};
|
|
294
386
|
var KafkaRetryExhaustedError = class extends KafkaProcessingError {
|
|
295
387
|
constructor(topic2, originalMessage, attempts, options) {
|
|
@@ -302,9 +394,13 @@ var KafkaRetryExhaustedError = class extends KafkaProcessingError {
|
|
|
302
394
|
this.attempts = attempts;
|
|
303
395
|
this.name = "KafkaRetryExhaustedError";
|
|
304
396
|
}
|
|
397
|
+
attempts;
|
|
305
398
|
};
|
|
306
399
|
|
|
307
400
|
// src/client/kafka.client/producer/ops.ts
|
|
401
|
+
function resolveSerde(topicOrDesc, clientSerde) {
|
|
402
|
+
return topicOrDesc?.__serde ?? clientSerde;
|
|
403
|
+
}
|
|
308
404
|
function resolveTopicName(topicOrDescriptor) {
|
|
309
405
|
if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
|
|
310
406
|
if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
|
|
@@ -351,6 +447,7 @@ async function validateMessage(topicOrDesc, message, deps, ctx) {
|
|
|
351
447
|
}
|
|
352
448
|
async function buildSendPayload(topicOrDesc, messages, deps, compression) {
|
|
353
449
|
const topic2 = resolveTopicName(topicOrDesc);
|
|
450
|
+
const serde = resolveSerde(topicOrDesc, deps.serde);
|
|
354
451
|
const builtMessages = await Promise.all(
|
|
355
452
|
messages.map(async (m) => {
|
|
356
453
|
const envelopeHeaders = buildEnvelopeHeaders({
|
|
@@ -370,11 +467,16 @@ async function buildSendPayload(topicOrDesc, messages, deps, compression) {
|
|
|
370
467
|
headers: envelopeHeaders,
|
|
371
468
|
version: m.schemaVersion ?? 1
|
|
372
469
|
};
|
|
470
|
+
const validated = await validateMessage(topicOrDesc, m.value, deps, sendCtx);
|
|
373
471
|
return {
|
|
374
|
-
value:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
472
|
+
value: await serde.serialize(validated, {
|
|
473
|
+
topic: topic2,
|
|
474
|
+
headers: envelopeHeaders,
|
|
475
|
+
isKey: false
|
|
476
|
+
}),
|
|
477
|
+
// Explicit key wins; otherwise fall back to the descriptor's .key()
|
|
478
|
+
// extractor (runs on the original, pre-validation payload).
|
|
479
|
+
key: m.key ?? topicOrDesc?.__key?.(m.value) ?? null,
|
|
378
480
|
headers: envelopeHeaders
|
|
379
481
|
};
|
|
380
482
|
})
|
|
@@ -383,7 +485,7 @@ async function buildSendPayload(topicOrDesc, messages, deps, compression) {
|
|
|
383
485
|
}
|
|
384
486
|
|
|
385
487
|
// src/client/kafka.client/consumer/ops.ts
|
|
386
|
-
function getOrCreateConsumer(groupId, fromBeginning, autoCommit, deps, partitionAssigner, onFirstAssignment) {
|
|
488
|
+
function getOrCreateConsumer(groupId, fromBeginning, autoCommit, deps, partitionAssigner, onFirstAssignment, groupInstanceId) {
|
|
387
489
|
const { consumers, consumerCreationOptions, transport, onRebalance, logger } = deps;
|
|
388
490
|
if (consumers.has(groupId)) {
|
|
389
491
|
const prev = consumerCreationOptions.get(groupId);
|
|
@@ -416,6 +518,7 @@ function getOrCreateConsumer(groupId, fromBeginning, autoCommit, deps, partition
|
|
|
416
518
|
fromBeginning,
|
|
417
519
|
autoCommit,
|
|
418
520
|
partitionAssigner: partitionAssigner ?? "cooperative-sticky",
|
|
521
|
+
groupInstanceId,
|
|
419
522
|
onRebalance: (type, assignments) => {
|
|
420
523
|
if (type === "assign") fireOnAssignment();
|
|
421
524
|
else if (type === "revoke") scheduleSettle();
|
|
@@ -455,12 +558,22 @@ function buildSchemaMap(topics, schemaRegistry, optionSchemas, logger) {
|
|
|
455
558
|
}
|
|
456
559
|
return schemaMap;
|
|
457
560
|
}
|
|
561
|
+
function buildSerdeMap(topics) {
|
|
562
|
+
let serdeMap;
|
|
563
|
+
for (const t of topics) {
|
|
564
|
+
if (t?.__serde) {
|
|
565
|
+
(serdeMap ??= /* @__PURE__ */ new Map()).set(resolveTopicName(t), t.__serde);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return serdeMap;
|
|
569
|
+
}
|
|
458
570
|
|
|
459
571
|
// src/client/kafka.client/admin/ops.ts
|
|
460
572
|
var AdminOps = class {
|
|
461
573
|
constructor(deps) {
|
|
462
574
|
this.deps = deps;
|
|
463
575
|
}
|
|
576
|
+
deps;
|
|
464
577
|
isConnected = false;
|
|
465
578
|
/** Underlying admin client — used by index.ts for topic validation. */
|
|
466
579
|
get admin() {
|
|
@@ -567,7 +680,10 @@ var AdminOps = class {
|
|
|
567
680
|
const found = results.find(
|
|
568
681
|
(r) => r.partition === partition
|
|
569
682
|
);
|
|
570
|
-
return { partition, offset: found
|
|
683
|
+
if (found) return { partition, offset: found.offset };
|
|
684
|
+
const topicOffsets = await this.deps.admin.fetchTopicOffsets(topic2);
|
|
685
|
+
const po = topicOffsets.find((o) => o.partition === partition);
|
|
686
|
+
return { partition, offset: po?.high ?? "0" };
|
|
571
687
|
})
|
|
572
688
|
);
|
|
573
689
|
await this.deps.admin.setOffsets({ groupId: gid, topic: topic2, partitions: offsets });
|
|
@@ -648,7 +764,8 @@ var AdminOps = class {
|
|
|
648
764
|
name: t.name,
|
|
649
765
|
partitions: t.partitions.map((p) => ({
|
|
650
766
|
partition: p.partitionId ?? p.partition ?? 0,
|
|
651
|
-
|
|
767
|
+
// -1 is Kafka's own "no leader" sentinel; 0 is a valid broker id
|
|
768
|
+
leader: p.leader ?? -1,
|
|
652
769
|
replicas: (p.replicas ?? []).map(
|
|
653
770
|
(r) => typeof r === "number" ? r : r.nodeId
|
|
654
771
|
),
|
|
@@ -732,23 +849,9 @@ var AdminOps = class {
|
|
|
732
849
|
};
|
|
733
850
|
|
|
734
851
|
// src/client/kafka.client/consumer/pipeline.ts
|
|
735
|
-
function toError(error) {
|
|
736
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
737
|
-
}
|
|
738
852
|
function sleep(ms) {
|
|
739
853
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
740
854
|
}
|
|
741
|
-
function parseJsonMessage(raw, topic2, logger) {
|
|
742
|
-
try {
|
|
743
|
-
return JSON.parse(raw);
|
|
744
|
-
} catch (error) {
|
|
745
|
-
logger.error(
|
|
746
|
-
`Failed to parse message from topic ${topic2}:`,
|
|
747
|
-
toError(error).stack
|
|
748
|
-
);
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
855
|
async function validateWithSchema(message, raw, topic2, schemaMap, interceptors, dlq, deps) {
|
|
753
856
|
const schema = schemaMap.get(topic2);
|
|
754
857
|
if (!schema) return message;
|
|
@@ -1018,6 +1121,7 @@ async function executeWithRetry(fn, ctx, deps) {
|
|
|
1018
1121
|
for (const env of envelopes) deps.onMessage?.(env);
|
|
1019
1122
|
return;
|
|
1020
1123
|
}
|
|
1124
|
+
deps.onFailure?.(envelopes[0]);
|
|
1021
1125
|
const isLastAttempt = attempt === maxAttempts;
|
|
1022
1126
|
const reportedError = isLastAttempt && maxAttempts > 1 ? new KafkaRetryExhaustedError(
|
|
1023
1127
|
topic2,
|
|
@@ -1102,8 +1206,13 @@ async function subscribeWithRetry(consumer, topics, logger, retryOpts) {
|
|
|
1102
1206
|
}
|
|
1103
1207
|
}
|
|
1104
1208
|
|
|
1105
|
-
// src/client/kafka.client/consumer/dlq-replay.ts
|
|
1209
|
+
// src/client/kafka.client/consumer/features/dlq-replay.ts
|
|
1106
1210
|
async function replayDlqTopic(topic2, deps, options = {}) {
|
|
1211
|
+
if (topic2.endsWith(".dlq")) {
|
|
1212
|
+
throw new Error(
|
|
1213
|
+
`replayDlq: pass the ORIGINAL topic name \u2014 "${topic2}" already ends in ".dlq" (the ".dlq" suffix is appended internally, so this would read "${topic2}.dlq")`
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1107
1216
|
const dlqTopic = `${topic2}.dlq`;
|
|
1108
1217
|
const partitionOffsets = await deps.fetchTopicOffsets(dlqTopic);
|
|
1109
1218
|
const activePartitions = partitionOffsets.filter(
|
|
@@ -1135,15 +1244,15 @@ async function replayDlqTopic(topic2, deps, options = {}) {
|
|
|
1135
1244
|
const originalHeaders = Object.fromEntries(
|
|
1136
1245
|
Object.entries(headers).filter(([k]) => !deps.dlqHeaderKeys.has(k))
|
|
1137
1246
|
);
|
|
1138
|
-
const
|
|
1139
|
-
const shouldProcess = !options.filter || options.filter(headers,
|
|
1247
|
+
const bytes = message.value;
|
|
1248
|
+
const shouldProcess = !options.filter || options.filter(headers, bytes.toString("utf8"));
|
|
1140
1249
|
if (!targetTopic || !shouldProcess) {
|
|
1141
1250
|
skipped++;
|
|
1142
1251
|
} else if (options.dryRun) {
|
|
1143
1252
|
deps.logger.log(`[DLQ replay dry-run] Would replay to "${targetTopic}"`);
|
|
1144
1253
|
replayed++;
|
|
1145
1254
|
} else {
|
|
1146
|
-
await deps.send(targetTopic, [{ value, headers: originalHeaders }]);
|
|
1255
|
+
await deps.send(targetTopic, [{ value: bytes, headers: originalHeaders }]);
|
|
1147
1256
|
replayed++;
|
|
1148
1257
|
}
|
|
1149
1258
|
const allDone = Array.from(highWatermarks.entries()).every(
|
|
@@ -1169,6 +1278,7 @@ var MetricsManager = class {
|
|
|
1169
1278
|
constructor(deps) {
|
|
1170
1279
|
this.deps = deps;
|
|
1171
1280
|
}
|
|
1281
|
+
deps;
|
|
1172
1282
|
topicMetrics = /* @__PURE__ */ new Map();
|
|
1173
1283
|
metricsFor(topic2) {
|
|
1174
1284
|
let m = this.topicMetrics.get(topic2);
|
|
@@ -1194,16 +1304,25 @@ var MetricsManager = class {
|
|
|
1194
1304
|
for (const inst of this.deps.instrumentation) inst.onRetry?.(envelope, attempt, maxRetries);
|
|
1195
1305
|
}
|
|
1196
1306
|
/**
|
|
1197
|
-
* Increment the DLQ counter for the envelope's topic
|
|
1198
|
-
*
|
|
1307
|
+
* Increment the DLQ counter for the envelope's topic and fire all `onDlq` instrumentation hooks.
|
|
1308
|
+
* Circuit breaker failures are recorded separately via `notifyFailure` at the
|
|
1309
|
+
* handler-error boundary — dead-lettering itself is not a circuit event.
|
|
1199
1310
|
* @param envelope The message envelope being sent to the DLQ.
|
|
1200
1311
|
* @param reason The reason the message is being dead-lettered.
|
|
1201
|
-
* @param gid Consumer group ID — used to drive circuit breaker state.
|
|
1202
1312
|
*/
|
|
1203
|
-
notifyDlq(envelope, reason
|
|
1313
|
+
notifyDlq(envelope, reason) {
|
|
1204
1314
|
this.metricsFor(envelope.topic).dlqCount++;
|
|
1205
1315
|
for (const inst of this.deps.instrumentation) inst.onDlq?.(envelope, reason);
|
|
1206
|
-
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Notify the circuit breaker of a handler failure. Fired on every failed
|
|
1319
|
+
* handler attempt (in-process retries and retry-topic levels included),
|
|
1320
|
+
* independent of whether the message is ultimately dead-lettered.
|
|
1321
|
+
* @param envelope The message envelope whose handler failed.
|
|
1322
|
+
* @param gid Consumer group ID — used to drive circuit breaker state.
|
|
1323
|
+
*/
|
|
1324
|
+
notifyFailure(envelope, gid) {
|
|
1325
|
+
this.deps.onCircuitFailure(envelope, gid);
|
|
1207
1326
|
}
|
|
1208
1327
|
/**
|
|
1209
1328
|
* Increment the deduplication counter for the envelope's topic and fire all `onDuplicate` hooks.
|
|
@@ -1262,6 +1381,7 @@ var InFlightTracker = class {
|
|
|
1262
1381
|
constructor(warn) {
|
|
1263
1382
|
this.warn = warn;
|
|
1264
1383
|
}
|
|
1384
|
+
warn;
|
|
1265
1385
|
inFlightTotal = 0;
|
|
1266
1386
|
drainResolvers = [];
|
|
1267
1387
|
/**
|
|
@@ -1272,10 +1392,16 @@ var InFlightTracker = class {
|
|
|
1272
1392
|
*/
|
|
1273
1393
|
track(fn) {
|
|
1274
1394
|
this.inFlightTotal++;
|
|
1275
|
-
|
|
1395
|
+
const done = () => {
|
|
1276
1396
|
this.inFlightTotal--;
|
|
1277
1397
|
if (this.inFlightTotal === 0) this.drainResolvers.splice(0).forEach((r) => r());
|
|
1278
|
-
}
|
|
1398
|
+
};
|
|
1399
|
+
try {
|
|
1400
|
+
return fn().finally(done);
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
done();
|
|
1403
|
+
throw err;
|
|
1404
|
+
}
|
|
1279
1405
|
}
|
|
1280
1406
|
/**
|
|
1281
1407
|
* Resolve when all tracked handlers have completed, or after `timeoutMs` elapses.
|
|
@@ -1309,6 +1435,7 @@ var CircuitBreakerManager = class {
|
|
|
1309
1435
|
constructor(deps) {
|
|
1310
1436
|
this.deps = deps;
|
|
1311
1437
|
}
|
|
1438
|
+
deps;
|
|
1312
1439
|
states = /* @__PURE__ */ new Map();
|
|
1313
1440
|
configs = /* @__PURE__ */ new Map();
|
|
1314
1441
|
/**
|
|
@@ -1453,6 +1580,9 @@ var AsyncQueue = class {
|
|
|
1453
1580
|
this.onFull = onFull;
|
|
1454
1581
|
this.onDrained = onDrained;
|
|
1455
1582
|
}
|
|
1583
|
+
highWaterMark;
|
|
1584
|
+
onFull;
|
|
1585
|
+
onDrained;
|
|
1456
1586
|
items = [];
|
|
1457
1587
|
waiting = [];
|
|
1458
1588
|
closed = false;
|
|
@@ -1464,6 +1594,7 @@ var AsyncQueue = class {
|
|
|
1464
1594
|
* @param item The value to enqueue.
|
|
1465
1595
|
*/
|
|
1466
1596
|
push(item) {
|
|
1597
|
+
if (this.closed) return;
|
|
1467
1598
|
if (this.waiting.length > 0) {
|
|
1468
1599
|
this.waiting.shift().resolve({ value: item, done: false });
|
|
1469
1600
|
} else {
|
|
@@ -1514,6 +1645,101 @@ var AsyncQueue = class {
|
|
|
1514
1645
|
}
|
|
1515
1646
|
};
|
|
1516
1647
|
|
|
1648
|
+
// src/client/kafka.client/validate-options.ts
|
|
1649
|
+
function validateClientOptions(clientId, groupId, brokers, options) {
|
|
1650
|
+
const problems = [];
|
|
1651
|
+
if (typeof clientId !== "string" || clientId.trim() === "") {
|
|
1652
|
+
problems.push("clientId must be a non-empty string");
|
|
1653
|
+
}
|
|
1654
|
+
if (typeof groupId !== "string" || groupId.trim() === "") {
|
|
1655
|
+
problems.push("groupId must be a non-empty string");
|
|
1656
|
+
}
|
|
1657
|
+
if (!Array.isArray(brokers) || brokers.length === 0 && !options?.transport) {
|
|
1658
|
+
problems.push("brokers must be a non-empty array of broker addresses");
|
|
1659
|
+
} else if (brokers.some((b) => typeof b !== "string" || b.trim() === "")) {
|
|
1660
|
+
problems.push("brokers must not contain empty entries");
|
|
1661
|
+
}
|
|
1662
|
+
if (options) {
|
|
1663
|
+
const {
|
|
1664
|
+
numPartitions,
|
|
1665
|
+
transactionalId,
|
|
1666
|
+
clockRecovery,
|
|
1667
|
+
lagThrottle
|
|
1668
|
+
} = options;
|
|
1669
|
+
if (numPartitions !== void 0 && (!Number.isInteger(numPartitions) || numPartitions < 1)) {
|
|
1670
|
+
problems.push(
|
|
1671
|
+
`numPartitions must be a positive integer (got ${numPartitions})`
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
if (transactionalId !== void 0 && transactionalId.trim() === "") {
|
|
1675
|
+
problems.push("transactionalId must be a non-empty string when set");
|
|
1676
|
+
}
|
|
1677
|
+
if (clockRecovery) {
|
|
1678
|
+
if (!Array.isArray(clockRecovery.topics)) {
|
|
1679
|
+
problems.push("clockRecovery.topics must be an array of topic names");
|
|
1680
|
+
}
|
|
1681
|
+
if (clockRecovery.timeoutMs !== void 0 && !(clockRecovery.timeoutMs > 0)) {
|
|
1682
|
+
problems.push(
|
|
1683
|
+
`clockRecovery.timeoutMs must be > 0 (got ${clockRecovery.timeoutMs})`
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
if (lagThrottle) {
|
|
1688
|
+
if (!(lagThrottle.maxLag >= 0)) {
|
|
1689
|
+
problems.push(`lagThrottle.maxLag must be >= 0 (got ${lagThrottle.maxLag})`);
|
|
1690
|
+
}
|
|
1691
|
+
if (lagThrottle.pollIntervalMs !== void 0 && !(lagThrottle.pollIntervalMs > 0)) {
|
|
1692
|
+
problems.push(
|
|
1693
|
+
`lagThrottle.pollIntervalMs must be > 0 (got ${lagThrottle.pollIntervalMs})`
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
if (lagThrottle.maxWaitMs !== void 0 && !(lagThrottle.maxWaitMs >= 0)) {
|
|
1697
|
+
problems.push(
|
|
1698
|
+
`lagThrottle.maxWaitMs must be >= 0 (got ${lagThrottle.maxWaitMs})`
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (problems.length > 0) {
|
|
1704
|
+
throw new Error(
|
|
1705
|
+
`KafkaClient: invalid configuration:
|
|
1706
|
+
- ${problems.join("\n- ")}`
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// src/client/security/resolve-security.ts
|
|
1712
|
+
var LOCAL_HOST_PATTERNS = [
|
|
1713
|
+
/^localhost(:\d+)?$/i,
|
|
1714
|
+
/^127\.\d+\.\d+\.\d+(:\d+)?$/,
|
|
1715
|
+
/^\[?::1\]?(:\d+)?$/,
|
|
1716
|
+
/^0\.0\.0\.0(:\d+)?$/,
|
|
1717
|
+
/^host\.docker\.internal(:\d+)?$/i
|
|
1718
|
+
];
|
|
1719
|
+
function isLocalBroker(broker) {
|
|
1720
|
+
return LOCAL_HOST_PATTERNS.some((re) => re.test(broker.trim()));
|
|
1721
|
+
}
|
|
1722
|
+
function resolveSecurityOptions(security, brokers, logger) {
|
|
1723
|
+
const hasRemoteBroker = brokers.some((b) => !isLocalBroker(b));
|
|
1724
|
+
if (!security?.sasl && security?.ssl !== true) {
|
|
1725
|
+
if (hasRemoteBroker && !security?.allowInsecure) {
|
|
1726
|
+
logger.warn(
|
|
1727
|
+
"Connecting to non-local brokers without TLS or SASL \u2014 traffic and payloads travel in plaintext. Configure `security: { ssl, sasl }` for production clusters, or set `security: { allowInsecure: true }` to acknowledge an intentionally insecure setup."
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
return security;
|
|
1731
|
+
}
|
|
1732
|
+
if (security.sasl && security.ssl === void 0) {
|
|
1733
|
+
return { ...security, ssl: true };
|
|
1734
|
+
}
|
|
1735
|
+
if (security.sasl && security.ssl === false) {
|
|
1736
|
+
logger.warn(
|
|
1737
|
+
"SASL credentials are configured with `ssl: false` \u2014 credentials will be sent over plaintext. This is only safe on fully trusted networks."
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
return security;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1517
1743
|
// src/client/kafka.client/producer/lifecycle.ts
|
|
1518
1744
|
var _activeTransactionalIds = /* @__PURE__ */ new Set();
|
|
1519
1745
|
async function ensureTopic(ctx, topic2) {
|
|
@@ -1647,6 +1873,7 @@ async function recoverLamportClockImpl(ctx, topics) {
|
|
|
1647
1873
|
const remaining = new Set(
|
|
1648
1874
|
partitionsToRead.map((p) => `${p.topic}:${p.partition}`)
|
|
1649
1875
|
);
|
|
1876
|
+
let settled = false;
|
|
1650
1877
|
const cleanup = () => {
|
|
1651
1878
|
consumer.disconnect().catch(() => {
|
|
1652
1879
|
}).finally(() => {
|
|
@@ -1654,6 +1881,16 @@ async function recoverLamportClockImpl(ctx, topics) {
|
|
|
1654
1881
|
});
|
|
1655
1882
|
});
|
|
1656
1883
|
};
|
|
1884
|
+
const timeoutTimer = setTimeout(() => {
|
|
1885
|
+
if (settled) return;
|
|
1886
|
+
settled = true;
|
|
1887
|
+
ctx.logger.warn(
|
|
1888
|
+
`Clock recovery: timed out after ${ctx.clockRecoveryTimeoutMs} ms with ${remaining.size} partition(s) unread \u2014 proceeding with partial result`
|
|
1889
|
+
);
|
|
1890
|
+
cleanup();
|
|
1891
|
+
resolve();
|
|
1892
|
+
}, ctx.clockRecoveryTimeoutMs);
|
|
1893
|
+
timeoutTimer.unref?.();
|
|
1657
1894
|
consumer.connect().then(async () => {
|
|
1658
1895
|
const uniqueTopics = [
|
|
1659
1896
|
...new Set(partitionsToRead.map((p) => p.topic))
|
|
@@ -1674,13 +1911,18 @@ async function recoverLamportClockImpl(ctx, topics) {
|
|
|
1674
1911
|
const clock = Number(raw);
|
|
1675
1912
|
if (!Number.isNaN(clock) && clock > maxClock) maxClock = clock;
|
|
1676
1913
|
}
|
|
1677
|
-
if (remaining.size === 0) {
|
|
1914
|
+
if (remaining.size === 0 && !settled) {
|
|
1915
|
+
settled = true;
|
|
1916
|
+
clearTimeout(timeoutTimer);
|
|
1678
1917
|
cleanup();
|
|
1679
1918
|
resolve();
|
|
1680
1919
|
}
|
|
1681
1920
|
}
|
|
1682
1921
|
})
|
|
1683
1922
|
).catch((err) => {
|
|
1923
|
+
if (settled) return;
|
|
1924
|
+
settled = true;
|
|
1925
|
+
clearTimeout(timeoutTimer);
|
|
1684
1926
|
cleanup();
|
|
1685
1927
|
reject(err);
|
|
1686
1928
|
});
|
|
@@ -1721,6 +1963,15 @@ async function preparePayload(ctx, topicOrDesc, messages, compression) {
|
|
|
1721
1963
|
await ensureTopic(ctx, payload.topic);
|
|
1722
1964
|
return payload;
|
|
1723
1965
|
}
|
|
1966
|
+
async function redirectToDelayed(ctx, payload, deliverAfterMs) {
|
|
1967
|
+
const until = String(Date.now() + deliverAfterMs);
|
|
1968
|
+
for (const m of payload.messages) {
|
|
1969
|
+
m.headers[HEADER_DELAYED_UNTIL] = until;
|
|
1970
|
+
m.headers[HEADER_DELAYED_TARGET] = payload.topic;
|
|
1971
|
+
}
|
|
1972
|
+
payload.topic = `${payload.topic}.delayed`;
|
|
1973
|
+
await ensureTopic(ctx, payload.topic);
|
|
1974
|
+
}
|
|
1724
1975
|
async function sendMessageImpl(ctx, topicOrDesc, message, options = {}) {
|
|
1725
1976
|
await waitIfThrottled(ctx);
|
|
1726
1977
|
const payload = await preparePayload(
|
|
@@ -1738,6 +1989,9 @@ async function sendMessageImpl(ctx, topicOrDesc, message, options = {}) {
|
|
|
1738
1989
|
],
|
|
1739
1990
|
options.compression
|
|
1740
1991
|
);
|
|
1992
|
+
if (options.deliverAfterMs && options.deliverAfterMs > 0) {
|
|
1993
|
+
await redirectToDelayed(ctx, payload, options.deliverAfterMs);
|
|
1994
|
+
}
|
|
1741
1995
|
await ctx.producer.send(payload);
|
|
1742
1996
|
ctx.metrics.notifyAfterSend(payload.topic, payload.messages.length);
|
|
1743
1997
|
}
|
|
@@ -1749,6 +2003,9 @@ async function sendBatchImpl(ctx, topicOrDesc, messages, options) {
|
|
|
1749
2003
|
messages,
|
|
1750
2004
|
options?.compression
|
|
1751
2005
|
);
|
|
2006
|
+
if (options?.deliverAfterMs && options.deliverAfterMs > 0) {
|
|
2007
|
+
await redirectToDelayed(ctx, payload, options.deliverAfterMs);
|
|
2008
|
+
}
|
|
1752
2009
|
await ctx.producer.send(payload);
|
|
1753
2010
|
ctx.metrics.notifyAfterSend(payload.topic, payload.messages.length);
|
|
1754
2011
|
}
|
|
@@ -1785,6 +2042,17 @@ async function transactionImpl(ctx, fn) {
|
|
|
1785
2042
|
});
|
|
1786
2043
|
}
|
|
1787
2044
|
ctx.txProducer = await ctx.txProducerInitPromise;
|
|
2045
|
+
const prev = ctx._txChain;
|
|
2046
|
+
let release;
|
|
2047
|
+
ctx._txChain = new Promise((r) => release = r);
|
|
2048
|
+
await prev;
|
|
2049
|
+
try {
|
|
2050
|
+
await runTransaction(ctx, fn);
|
|
2051
|
+
} finally {
|
|
2052
|
+
release();
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
async function runTransaction(ctx, fn) {
|
|
1788
2056
|
const tx = await ctx.txProducer.transaction();
|
|
1789
2057
|
try {
|
|
1790
2058
|
const txCtx = {
|
|
@@ -1858,7 +2126,7 @@ async function waitForPartitionAssignment(consumer, topics, logger, timeoutMs =
|
|
|
1858
2126
|
`Retry consumer did not receive partition assignments for [${topics.join(", ")}] within ${timeoutMs}ms`
|
|
1859
2127
|
);
|
|
1860
2128
|
}
|
|
1861
|
-
async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopics, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs) {
|
|
2129
|
+
async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopics, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
|
|
1862
2130
|
const {
|
|
1863
2131
|
logger,
|
|
1864
2132
|
producer,
|
|
@@ -1904,20 +2172,35 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
1904
2172
|
await sleep(remaining);
|
|
1905
2173
|
consumer.resume([{ topic: levelTopic, partitions: [partition] }]);
|
|
1906
2174
|
}
|
|
1907
|
-
const
|
|
1908
|
-
const parsed = parseJsonMessage(raw, levelTopic, logger);
|
|
1909
|
-
if (parsed === null) {
|
|
1910
|
-
await consumer.commitOffsets([nextOffset]);
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
2175
|
+
const rawBytes = message.value;
|
|
1913
2176
|
const currentMaxRetries = parseInt(
|
|
1914
2177
|
headers[RETRY_HEADER_MAX_RETRIES] ?? String(retry.maxRetries),
|
|
1915
2178
|
10
|
|
1916
2179
|
);
|
|
1917
2180
|
const originalTopic = headers[RETRY_HEADER_ORIGINAL_TOPIC] ?? levelTopic.replace(/\.retry\.\d+$/, "");
|
|
2181
|
+
const serde = serdeMap?.get(originalTopic) ?? deps.serde;
|
|
2182
|
+
let parsed;
|
|
2183
|
+
try {
|
|
2184
|
+
parsed = await serde.deserialize(rawBytes, {
|
|
2185
|
+
topic: originalTopic,
|
|
2186
|
+
headers,
|
|
2187
|
+
isKey: false
|
|
2188
|
+
});
|
|
2189
|
+
} catch (err) {
|
|
2190
|
+
logger.error(
|
|
2191
|
+
`Failed to deserialize retry message from topic ${levelTopic}:`,
|
|
2192
|
+
toError(err).stack
|
|
2193
|
+
);
|
|
2194
|
+
await consumer.commitOffsets([nextOffset]);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (parsed === null) {
|
|
2198
|
+
await consumer.commitOffsets([nextOffset]);
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
1918
2201
|
const validated = await validateWithSchema(
|
|
1919
2202
|
parsed,
|
|
1920
|
-
|
|
2203
|
+
rawBytes,
|
|
1921
2204
|
originalTopic,
|
|
1922
2205
|
schemaMap,
|
|
1923
2206
|
interceptors,
|
|
@@ -1952,6 +2235,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
1952
2235
|
await consumer.commitOffsets([nextOffset]);
|
|
1953
2236
|
return;
|
|
1954
2237
|
}
|
|
2238
|
+
deps.onFailure?.(envelope);
|
|
1955
2239
|
const exhausted = level >= currentMaxRetries;
|
|
1956
2240
|
const reportedError = exhausted && currentMaxRetries > 1 ? new KafkaRetryExhaustedError(
|
|
1957
2241
|
originalTopic,
|
|
@@ -1970,7 +2254,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
1970
2254
|
const delay = Math.floor(Math.random() * cap);
|
|
1971
2255
|
const { topic: rtTopic, messages: rtMsgs } = buildRetryTopicPayload(
|
|
1972
2256
|
originalTopic,
|
|
1973
|
-
[
|
|
2257
|
+
[rawBytes],
|
|
1974
2258
|
nextLevel,
|
|
1975
2259
|
currentMaxRetries,
|
|
1976
2260
|
delay,
|
|
@@ -2012,7 +2296,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
2012
2296
|
} else if (dlq) {
|
|
2013
2297
|
const { topic: dTopic, messages: dMsgs } = buildDlqPayload(
|
|
2014
2298
|
originalTopic,
|
|
2015
|
-
|
|
2299
|
+
rawBytes,
|
|
2016
2300
|
{
|
|
2017
2301
|
error,
|
|
2018
2302
|
// +1 to account for the main consumer's initial attempt before routing.
|
|
@@ -2074,7 +2358,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
2074
2358
|
`Retry level ${level}/${retry.maxRetries} consumer started for: ${originalTopics.join(", ")} (group: ${levelGroupId})`
|
|
2075
2359
|
);
|
|
2076
2360
|
}
|
|
2077
|
-
async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs) {
|
|
2361
|
+
async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
|
|
2078
2362
|
const levelGroupIds = new Array(retry.maxRetries);
|
|
2079
2363
|
await Promise.all(
|
|
2080
2364
|
Array.from({ length: retry.maxRetries }, async (_, i) => {
|
|
@@ -2092,7 +2376,8 @@ async function startRetryTopicConsumers(originalTopics, originalGroupId, handleM
|
|
|
2092
2376
|
interceptors,
|
|
2093
2377
|
schemaMap,
|
|
2094
2378
|
deps,
|
|
2095
|
-
assignmentTimeoutMs
|
|
2379
|
+
assignmentTimeoutMs,
|
|
2380
|
+
serdeMap
|
|
2096
2381
|
);
|
|
2097
2382
|
levelGroupIds[i] = levelGroupId;
|
|
2098
2383
|
})
|
|
@@ -2167,7 +2452,8 @@ async function setupConsumer(ctx, topics, mode, options) {
|
|
|
2167
2452
|
options.autoCommit ?? true,
|
|
2168
2453
|
ctx.consumerOpsDeps,
|
|
2169
2454
|
options.partitionAssigner,
|
|
2170
|
-
resolveReady
|
|
2455
|
+
resolveReady,
|
|
2456
|
+
options.groupInstanceId
|
|
2171
2457
|
);
|
|
2172
2458
|
const schemaMap = buildSchemaMap(
|
|
2173
2459
|
stringTopics,
|
|
@@ -2175,10 +2461,14 @@ async function setupConsumer(ctx, topics, mode, options) {
|
|
|
2175
2461
|
optionSchemas,
|
|
2176
2462
|
ctx.logger
|
|
2177
2463
|
);
|
|
2464
|
+
const serdeMap = buildSerdeMap(stringTopics);
|
|
2178
2465
|
const topicNames = stringTopics.map((t) => resolveTopicName(t));
|
|
2179
2466
|
const subscribeTopics = [...topicNames, ...regexTopics];
|
|
2180
2467
|
await ensureConsumerTopics(ctx, topicNames, dlq, options.deduplication);
|
|
2181
2468
|
await consumer.connect();
|
|
2469
|
+
if (dlq || options.retryTopics || options.deduplication) {
|
|
2470
|
+
await ctx.producer.connect();
|
|
2471
|
+
}
|
|
2182
2472
|
await subscribeWithRetry(
|
|
2183
2473
|
consumer,
|
|
2184
2474
|
subscribeTopics,
|
|
@@ -2189,19 +2479,19 @@ async function setupConsumer(ctx, topics, mode, options) {
|
|
|
2189
2479
|
ctx.logger.log(
|
|
2190
2480
|
`${mode === "eachBatch" ? "Batch consumer" : "Consumer"} subscribed to topics: ${displayTopics}`
|
|
2191
2481
|
);
|
|
2192
|
-
return { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, hasRegex, readyPromise };
|
|
2482
|
+
return { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, hasRegex, readyPromise };
|
|
2193
2483
|
}
|
|
2194
2484
|
function resolveDeduplicationContext(ctx, groupId, options) {
|
|
2195
2485
|
if (!options) return void 0;
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
return { options, state: ctx.dedupStates.get(groupId) };
|
|
2486
|
+
const store = options.store ?? new InMemoryDedupStore(ctx.dedupStates);
|
|
2487
|
+
return { options, store, groupId };
|
|
2199
2488
|
}
|
|
2200
2489
|
function messageDepsFor(ctx, gid, options) {
|
|
2201
2490
|
const notifyRetry = ctx.metrics.notifyRetry.bind(ctx.metrics);
|
|
2202
2491
|
return {
|
|
2203
2492
|
logger: ctx.logger,
|
|
2204
2493
|
producer: ctx.producer,
|
|
2494
|
+
serde: ctx.serde,
|
|
2205
2495
|
instrumentation: ctx.instrumentation,
|
|
2206
2496
|
onMessageLost: options?.onMessageLost ?? ctx.onMessageLost,
|
|
2207
2497
|
onTtlExpired: ctx.onTtlExpired,
|
|
@@ -2209,15 +2499,17 @@ function messageDepsFor(ctx, gid, options) {
|
|
|
2209
2499
|
notifyRetry(envelope, attempt, max);
|
|
2210
2500
|
return options.onRetry(envelope, attempt, max);
|
|
2211
2501
|
} : notifyRetry,
|
|
2212
|
-
onDlq: (envelope, reason) => ctx.metrics.notifyDlq(envelope, reason
|
|
2502
|
+
onDlq: (envelope, reason) => ctx.metrics.notifyDlq(envelope, reason),
|
|
2213
2503
|
onDuplicate: ctx.metrics.notifyDuplicate.bind(ctx.metrics),
|
|
2214
|
-
onMessage: (envelope) => ctx.metrics.notifyMessage(envelope, gid)
|
|
2504
|
+
onMessage: (envelope) => ctx.metrics.notifyMessage(envelope, gid),
|
|
2505
|
+
onFailure: (envelope) => ctx.metrics.notifyFailure(envelope, gid)
|
|
2215
2506
|
};
|
|
2216
2507
|
}
|
|
2217
2508
|
function buildRetryTopicDeps(ctx) {
|
|
2218
2509
|
return {
|
|
2219
2510
|
logger: ctx.logger,
|
|
2220
2511
|
producer: ctx.producer,
|
|
2512
|
+
serde: ctx.serde,
|
|
2221
2513
|
instrumentation: ctx.instrumentation,
|
|
2222
2514
|
onMessageLost: ctx.onMessageLost,
|
|
2223
2515
|
onRetry: ctx.metrics.notifyRetry.bind(ctx.metrics),
|
|
@@ -2235,7 +2527,7 @@ async function makeEosMainContext(ctx, gid, consumer, options) {
|
|
|
2235
2527
|
return { txProducer, consumer };
|
|
2236
2528
|
}
|
|
2237
2529
|
async function launchRetryChain(ctx, gid, topicNames, handleMessage, opts) {
|
|
2238
|
-
const { retry, dlq, interceptors, schemaMap, assignmentTimeoutMs } = opts;
|
|
2530
|
+
const { retry, dlq, interceptors, schemaMap, serdeMap, assignmentTimeoutMs } = opts;
|
|
2239
2531
|
if (!ctx.autoCreateTopicsEnabled) {
|
|
2240
2532
|
await ctx.adminOps.validateRetryTopicsExist(topicNames, retry.maxRetries);
|
|
2241
2533
|
}
|
|
@@ -2250,11 +2542,17 @@ async function launchRetryChain(ctx, gid, topicNames, handleMessage, opts) {
|
|
|
2250
2542
|
schemaMap,
|
|
2251
2543
|
{
|
|
2252
2544
|
...ctx.retryTopicDeps,
|
|
2545
|
+
// Bind circuit breaker events to the MAIN consumer group so failures and
|
|
2546
|
+
// successes inside the retry chain drive the same breaker as the main
|
|
2547
|
+
// consumer (the retry chain has no breaker config of its own).
|
|
2548
|
+
onFailure: (envelope) => ctx.metrics.notifyFailure(envelope, gid),
|
|
2549
|
+
onMessage: (envelope) => ctx.metrics.notifyMessage(envelope, gid),
|
|
2253
2550
|
onLevelStarted: (levelGroupId) => {
|
|
2254
2551
|
ctx.companionGroupIds.get(gid).push(levelGroupId);
|
|
2255
2552
|
}
|
|
2256
2553
|
},
|
|
2257
|
-
assignmentTimeoutMs
|
|
2554
|
+
assignmentTimeoutMs,
|
|
2555
|
+
serdeMap
|
|
2258
2556
|
);
|
|
2259
2557
|
}
|
|
2260
2558
|
|
|
@@ -2265,7 +2563,15 @@ async function applyDeduplication(envelope, raw, dedup, dlq, deps) {
|
|
|
2265
2563
|
const incomingClock = Number(clockRaw);
|
|
2266
2564
|
if (Number.isNaN(incomingClock)) return false;
|
|
2267
2565
|
const stateKey = `${envelope.topic}:${envelope.partition}`;
|
|
2268
|
-
|
|
2566
|
+
let lastProcessedClock;
|
|
2567
|
+
try {
|
|
2568
|
+
lastProcessedClock = await dedup.store.getLastClock(dedup.groupId, stateKey) ?? -1;
|
|
2569
|
+
} catch (err) {
|
|
2570
|
+
deps.logger.error(
|
|
2571
|
+
`Dedup store getLastClock failed on ${envelope.topic}[${envelope.partition}] \u2014 treating message as not a duplicate (fail-open): ${err.message}`
|
|
2572
|
+
);
|
|
2573
|
+
return false;
|
|
2574
|
+
}
|
|
2269
2575
|
if (incomingClock <= lastProcessedClock) {
|
|
2270
2576
|
const meta = {
|
|
2271
2577
|
incomingClock,
|
|
@@ -2295,21 +2601,38 @@ async function applyDeduplication(envelope, raw, dedup, dlq, deps) {
|
|
|
2295
2601
|
}
|
|
2296
2602
|
return true;
|
|
2297
2603
|
}
|
|
2298
|
-
|
|
2604
|
+
try {
|
|
2605
|
+
await dedup.store.setLastClock(dedup.groupId, stateKey, incomingClock);
|
|
2606
|
+
} catch (err) {
|
|
2607
|
+
deps.logger.error(
|
|
2608
|
+
`Dedup store setLastClock failed on ${envelope.topic}[${envelope.partition}] \u2014 processing message anyway (fail-open): ${err.message}`
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2299
2611
|
return false;
|
|
2300
2612
|
}
|
|
2301
|
-
async function parseSingleMessage(message, topic2, partition, schemaMap, interceptors, dlq, deps) {
|
|
2613
|
+
async function parseSingleMessage(message, topic2, partition, schemaMap, interceptors, dlq, deps, serdeMap) {
|
|
2302
2614
|
if (!message.value) {
|
|
2303
2615
|
deps.logger.warn(`Received empty message from topic ${topic2}`);
|
|
2304
2616
|
return null;
|
|
2305
2617
|
}
|
|
2306
|
-
const
|
|
2307
|
-
const parsed = parseJsonMessage(raw, topic2, deps.logger);
|
|
2308
|
-
if (parsed === null) return null;
|
|
2618
|
+
const bytes = message.value;
|
|
2309
2619
|
const headers = decodeHeaders(message.headers);
|
|
2620
|
+
const serde = serdeMap?.get(topic2) ?? deps.serde;
|
|
2621
|
+
let parsed;
|
|
2622
|
+
try {
|
|
2623
|
+
parsed = await serde.deserialize(bytes, { topic: topic2, headers, isKey: false });
|
|
2624
|
+
} catch (error) {
|
|
2625
|
+
deps.logger.error(
|
|
2626
|
+
`Failed to deserialize message from topic ${topic2}:`,
|
|
2627
|
+
toError(error).stack
|
|
2628
|
+
);
|
|
2629
|
+
return null;
|
|
2630
|
+
}
|
|
2631
|
+
if (parsed === null) return null;
|
|
2310
2632
|
const validated = await validateWithSchema(
|
|
2311
2633
|
parsed,
|
|
2312
|
-
|
|
2634
|
+
// Forward the ORIGINAL bytes to DLQ on validation failure (binary-safe).
|
|
2635
|
+
bytes,
|
|
2313
2636
|
topic2,
|
|
2314
2637
|
schemaMap,
|
|
2315
2638
|
interceptors,
|
|
@@ -2323,6 +2646,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2323
2646
|
const { topic: topic2, partition, message } = payload;
|
|
2324
2647
|
const {
|
|
2325
2648
|
schemaMap,
|
|
2649
|
+
serdeMap,
|
|
2326
2650
|
handleMessage,
|
|
2327
2651
|
interceptors,
|
|
2328
2652
|
dlq,
|
|
@@ -2331,6 +2655,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2331
2655
|
timeoutMs,
|
|
2332
2656
|
wrapWithTimeout
|
|
2333
2657
|
} = opts;
|
|
2658
|
+
const rawBytes = message.value;
|
|
2334
2659
|
const eos = opts.eosMainContext;
|
|
2335
2660
|
const nextOffsetStr = (parseInt(message.offset, 10) + 1).toString();
|
|
2336
2661
|
const commitOffset = eos ? async () => {
|
|
@@ -2375,7 +2700,8 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2375
2700
|
schemaMap,
|
|
2376
2701
|
interceptors,
|
|
2377
2702
|
dlq,
|
|
2378
|
-
deps
|
|
2703
|
+
deps,
|
|
2704
|
+
serdeMap
|
|
2379
2705
|
);
|
|
2380
2706
|
if (envelope === null) {
|
|
2381
2707
|
await commitOffset?.();
|
|
@@ -2384,7 +2710,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2384
2710
|
if (opts.deduplication) {
|
|
2385
2711
|
const isDuplicate = await applyDeduplication(
|
|
2386
2712
|
envelope,
|
|
2387
|
-
|
|
2713
|
+
rawBytes,
|
|
2388
2714
|
opts.deduplication,
|
|
2389
2715
|
dlq,
|
|
2390
2716
|
deps
|
|
@@ -2401,7 +2727,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2401
2727
|
`[KafkaClient] TTL expired on ${topic2}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
|
|
2402
2728
|
);
|
|
2403
2729
|
if (dlq) {
|
|
2404
|
-
await sendToDlq(topic2,
|
|
2730
|
+
await sendToDlq(topic2, rawBytes, deps, {
|
|
2405
2731
|
error: new Error(`Message TTL expired: age ${ageMs}ms`),
|
|
2406
2732
|
attempt: 0,
|
|
2407
2733
|
originalHeaders: envelope.headers
|
|
@@ -2433,7 +2759,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2433
2759
|
},
|
|
2434
2760
|
{
|
|
2435
2761
|
envelope,
|
|
2436
|
-
rawMessages: [
|
|
2762
|
+
rawMessages: [rawBytes],
|
|
2437
2763
|
interceptors,
|
|
2438
2764
|
dlq,
|
|
2439
2765
|
retry,
|
|
@@ -2446,6 +2772,7 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2446
2772
|
const { batch, heartbeat, resolveOffset, commitOffsetsIfNecessary } = payload;
|
|
2447
2773
|
const {
|
|
2448
2774
|
schemaMap,
|
|
2775
|
+
serdeMap,
|
|
2449
2776
|
handleBatch,
|
|
2450
2777
|
interceptors,
|
|
2451
2778
|
dlq,
|
|
@@ -2501,6 +2828,7 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2501
2828
|
const envelopes = [];
|
|
2502
2829
|
const rawMessages = [];
|
|
2503
2830
|
for (const message of batch.messages) {
|
|
2831
|
+
const rawBytes = message.value;
|
|
2504
2832
|
const envelope = await parseSingleMessage(
|
|
2505
2833
|
message,
|
|
2506
2834
|
batch.topic,
|
|
@@ -2508,14 +2836,14 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2508
2836
|
schemaMap,
|
|
2509
2837
|
interceptors,
|
|
2510
2838
|
dlq,
|
|
2511
|
-
deps
|
|
2839
|
+
deps,
|
|
2840
|
+
serdeMap
|
|
2512
2841
|
);
|
|
2513
2842
|
if (envelope === null) continue;
|
|
2514
2843
|
if (opts.deduplication) {
|
|
2515
|
-
const raw = message.value.toString();
|
|
2516
2844
|
const isDuplicate = await applyDeduplication(
|
|
2517
2845
|
envelope,
|
|
2518
|
-
|
|
2846
|
+
rawBytes,
|
|
2519
2847
|
opts.deduplication,
|
|
2520
2848
|
dlq,
|
|
2521
2849
|
deps
|
|
@@ -2529,7 +2857,7 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2529
2857
|
`[KafkaClient] TTL expired on ${batch.topic}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
|
|
2530
2858
|
);
|
|
2531
2859
|
if (dlq) {
|
|
2532
|
-
await sendToDlq(batch.topic,
|
|
2860
|
+
await sendToDlq(batch.topic, rawBytes, deps, {
|
|
2533
2861
|
error: new Error(`Message TTL expired: age ${ageMs}ms`),
|
|
2534
2862
|
attempt: 0,
|
|
2535
2863
|
originalHeaders: envelope.headers
|
|
@@ -2548,7 +2876,7 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2548
2876
|
}
|
|
2549
2877
|
}
|
|
2550
2878
|
envelopes.push(envelope);
|
|
2551
|
-
rawMessages.push(
|
|
2879
|
+
rawMessages.push(rawBytes);
|
|
2552
2880
|
}
|
|
2553
2881
|
if (envelopes.length === 0) {
|
|
2554
2882
|
await commitBatchOffset?.();
|
|
@@ -2711,7 +3039,7 @@ function resumeTopicAllPartitions(ctx, gid, topic2) {
|
|
|
2711
3039
|
async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
|
|
2712
3040
|
validateTopicConsumerOpts(topics, options);
|
|
2713
3041
|
const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
|
|
2714
|
-
const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachMessage", setupOptions);
|
|
3042
|
+
const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachMessage", setupOptions);
|
|
2715
3043
|
if (options.circuitBreaker)
|
|
2716
3044
|
ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
|
|
2717
3045
|
const deps = messageDepsFor(ctx, gid, options);
|
|
@@ -2722,6 +3050,7 @@ async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
|
|
|
2722
3050
|
payload,
|
|
2723
3051
|
{
|
|
2724
3052
|
schemaMap,
|
|
3053
|
+
serdeMap,
|
|
2725
3054
|
handleMessage,
|
|
2726
3055
|
interceptors,
|
|
2727
3056
|
dlq,
|
|
@@ -2749,6 +3078,7 @@ async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
|
|
|
2749
3078
|
dlq,
|
|
2750
3079
|
interceptors,
|
|
2751
3080
|
schemaMap,
|
|
3081
|
+
serdeMap,
|
|
2752
3082
|
assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
|
|
2753
3083
|
});
|
|
2754
3084
|
}
|
|
@@ -2762,7 +3092,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
|
|
|
2762
3092
|
);
|
|
2763
3093
|
}
|
|
2764
3094
|
const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
|
|
2765
|
-
const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachBatch", setupOptions);
|
|
3095
|
+
const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachBatch", setupOptions);
|
|
2766
3096
|
if (options.circuitBreaker)
|
|
2767
3097
|
ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
|
|
2768
3098
|
const deps = messageDepsFor(ctx, gid, options);
|
|
@@ -2773,6 +3103,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
|
|
|
2773
3103
|
payload,
|
|
2774
3104
|
{
|
|
2775
3105
|
schemaMap,
|
|
3106
|
+
serdeMap,
|
|
2776
3107
|
handleBatch,
|
|
2777
3108
|
interceptors,
|
|
2778
3109
|
dlq,
|
|
@@ -2810,6 +3141,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
|
|
|
2810
3141
|
dlq,
|
|
2811
3142
|
interceptors,
|
|
2812
3143
|
schemaMap,
|
|
3144
|
+
serdeMap,
|
|
2813
3145
|
assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
|
|
2814
3146
|
});
|
|
2815
3147
|
}
|
|
@@ -2822,7 +3154,7 @@ async function startTransactionalConsumerImpl(ctx, topics, handler, options = {}
|
|
|
2822
3154
|
);
|
|
2823
3155
|
}
|
|
2824
3156
|
const setupOptions = { ...options, autoCommit: false };
|
|
2825
|
-
const { consumer, schemaMap, gid, readyPromise } = await setupConsumer(
|
|
3157
|
+
const { consumer, schemaMap, serdeMap, gid, readyPromise } = await setupConsumer(
|
|
2826
3158
|
ctx,
|
|
2827
3159
|
topics,
|
|
2828
3160
|
"eachMessage",
|
|
@@ -2839,7 +3171,8 @@ async function startTransactionalConsumerImpl(ctx, topics, handler, options = {}
|
|
|
2839
3171
|
schemaMap,
|
|
2840
3172
|
options.interceptors ?? [],
|
|
2841
3173
|
false,
|
|
2842
|
-
deps
|
|
3174
|
+
deps,
|
|
3175
|
+
serdeMap
|
|
2843
3176
|
);
|
|
2844
3177
|
const nextOffset = String(Number.parseInt(message.offset, 10) + 1);
|
|
2845
3178
|
if (envelope === null) {
|
|
@@ -2908,7 +3241,7 @@ function stopConsumerByGid(ctx, gid) {
|
|
|
2908
3241
|
return stopConsumerImpl(ctx, gid);
|
|
2909
3242
|
}
|
|
2910
3243
|
|
|
2911
|
-
// src/client/kafka.client/consumer/window.ts
|
|
3244
|
+
// src/client/kafka.client/consumer/features/window.ts
|
|
2912
3245
|
async function startWindowConsumerImpl(ctx, topic2, handler, options) {
|
|
2913
3246
|
const { maxMessages, maxMs, ...consumerOptions } = options;
|
|
2914
3247
|
if (maxMessages <= 0)
|
|
@@ -2922,6 +3255,7 @@ async function startWindowConsumerImpl(ctx, topic2, handler, options) {
|
|
|
2922
3255
|
const buffer = [];
|
|
2923
3256
|
let flushTimer = null;
|
|
2924
3257
|
let windowStart = 0;
|
|
3258
|
+
const onLost = consumerOptions.onMessageLost ?? ctx.onMessageLost;
|
|
2925
3259
|
const flush = async (trigger) => {
|
|
2926
3260
|
if (flushTimer !== null) {
|
|
2927
3261
|
clearTimeout(flushTimer);
|
|
@@ -2929,17 +3263,32 @@ async function startWindowConsumerImpl(ctx, topic2, handler, options) {
|
|
|
2929
3263
|
}
|
|
2930
3264
|
if (buffer.length === 0) return;
|
|
2931
3265
|
const envelopes = buffer.splice(0);
|
|
2932
|
-
|
|
3266
|
+
try {
|
|
3267
|
+
await handler(envelopes, { trigger, windowStart, windowEnd: Date.now() });
|
|
3268
|
+
} catch (err) {
|
|
3269
|
+
const error = toError(err);
|
|
3270
|
+
ctx.logger.error(
|
|
3271
|
+
`startWindowConsumer: ${trigger}-triggered flush failed \u2014 window of ${envelopes.length} message(s) lost:`,
|
|
3272
|
+
error.stack
|
|
3273
|
+
);
|
|
3274
|
+
for (const envelope of envelopes) {
|
|
3275
|
+
await Promise.resolve(
|
|
3276
|
+
onLost?.({
|
|
3277
|
+
topic: envelope.topic,
|
|
3278
|
+
error,
|
|
3279
|
+
attempt: 0,
|
|
3280
|
+
headers: envelope.headers
|
|
3281
|
+
})
|
|
3282
|
+
).catch(() => {
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
2933
3286
|
};
|
|
2934
3287
|
const scheduleFlush = () => {
|
|
2935
3288
|
if (flushTimer !== null) return;
|
|
2936
3289
|
flushTimer = setTimeout(() => {
|
|
2937
3290
|
flushTimer = null;
|
|
2938
|
-
flush("time")
|
|
2939
|
-
ctx.logger.warn(
|
|
2940
|
-
`startWindowConsumer: time-triggered flush error \u2014 ${toError(err).message}`
|
|
2941
|
-
);
|
|
2942
|
-
});
|
|
3291
|
+
void flush("time");
|
|
2943
3292
|
}, maxMs);
|
|
2944
3293
|
};
|
|
2945
3294
|
const handle = await startConsumerImpl(
|
|
@@ -2955,40 +3304,13 @@ async function startWindowConsumerImpl(ctx, topic2, handler, options) {
|
|
|
2955
3304
|
);
|
|
2956
3305
|
const originalStop = handle.stop.bind(handle);
|
|
2957
3306
|
handle.stop = async () => {
|
|
2958
|
-
|
|
2959
|
-
clearTimeout(flushTimer);
|
|
2960
|
-
flushTimer = null;
|
|
2961
|
-
}
|
|
2962
|
-
if (buffer.length > 0) {
|
|
2963
|
-
const envelopes = buffer.splice(0);
|
|
2964
|
-
await handler(envelopes, {
|
|
2965
|
-
trigger: "time",
|
|
2966
|
-
windowStart,
|
|
2967
|
-
windowEnd: Date.now()
|
|
2968
|
-
}).catch(async (err) => {
|
|
2969
|
-
const error = toError(err);
|
|
2970
|
-
ctx.logger.warn(
|
|
2971
|
-
`startWindowConsumer: shutdown flush error \u2014 ${error.message}`
|
|
2972
|
-
);
|
|
2973
|
-
for (const envelope of envelopes) {
|
|
2974
|
-
await Promise.resolve(
|
|
2975
|
-
ctx.onMessageLost?.({
|
|
2976
|
-
topic: envelope.topic,
|
|
2977
|
-
error,
|
|
2978
|
-
attempt: 0,
|
|
2979
|
-
headers: envelope.headers
|
|
2980
|
-
})
|
|
2981
|
-
).catch(() => {
|
|
2982
|
-
});
|
|
2983
|
-
}
|
|
2984
|
-
});
|
|
2985
|
-
}
|
|
3307
|
+
await flush("time");
|
|
2986
3308
|
return originalStop();
|
|
2987
3309
|
};
|
|
2988
3310
|
return handle;
|
|
2989
3311
|
}
|
|
2990
3312
|
|
|
2991
|
-
// src/client/kafka.client/consumer/routed.ts
|
|
3313
|
+
// src/client/kafka.client/consumer/features/routed.ts
|
|
2992
3314
|
async function startRoutedConsumerImpl(ctx, topics, routing, options) {
|
|
2993
3315
|
const { header, routes, fallback } = routing;
|
|
2994
3316
|
const handleMessage = async (envelope) => {
|
|
@@ -3003,7 +3325,120 @@ async function startRoutedConsumerImpl(ctx, topics, routing, options) {
|
|
|
3003
3325
|
return startConsumerImpl(ctx, topics, handleMessage, options);
|
|
3004
3326
|
}
|
|
3005
3327
|
|
|
3006
|
-
// src/client/kafka.client/consumer/
|
|
3328
|
+
// src/client/kafka.client/consumer/features/delayed.ts
|
|
3329
|
+
function delayedTopicName(topic2) {
|
|
3330
|
+
return `${topic2}.delayed`;
|
|
3331
|
+
}
|
|
3332
|
+
async function startDelayedRelayImpl(ctx, topics, options) {
|
|
3333
|
+
if (topics.length === 0) {
|
|
3334
|
+
throw new Error("startDelayedRelay: at least one topic is required");
|
|
3335
|
+
}
|
|
3336
|
+
const gid = options?.groupId ?? `${ctx.defaultGroupId}-delayed-relay`;
|
|
3337
|
+
if (ctx.runningConsumers.has(gid)) {
|
|
3338
|
+
throw new Error(
|
|
3339
|
+
`startDelayedRelay("${gid}") called twice \u2014 this group is already consuming. Call stopConsumer("${gid}") first or pass a different groupId.`
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
const delayedTopics = topics.map(delayedTopicName);
|
|
3343
|
+
for (const t of delayedTopics) await ensureTopic(ctx, t);
|
|
3344
|
+
const txProducer = await createRetryTxProducer(ctx, `${gid}-tx`);
|
|
3345
|
+
let resolveReady;
|
|
3346
|
+
const readyPromise = new Promise((resolve) => {
|
|
3347
|
+
resolveReady = resolve;
|
|
3348
|
+
});
|
|
3349
|
+
const consumer = getOrCreateConsumer(
|
|
3350
|
+
gid,
|
|
3351
|
+
false,
|
|
3352
|
+
false,
|
|
3353
|
+
ctx.consumerOpsDeps,
|
|
3354
|
+
void 0,
|
|
3355
|
+
resolveReady
|
|
3356
|
+
);
|
|
3357
|
+
await consumer.connect();
|
|
3358
|
+
await subscribeWithRetry(consumer, delayedTopics, ctx.logger);
|
|
3359
|
+
await consumer.run({
|
|
3360
|
+
eachMessage: async ({ topic: stagingTopic, partition, message }) => {
|
|
3361
|
+
const nextOffset = {
|
|
3362
|
+
topic: stagingTopic,
|
|
3363
|
+
partition,
|
|
3364
|
+
offset: (parseInt(message.offset, 10) + 1).toString()
|
|
3365
|
+
};
|
|
3366
|
+
if (!message.value) {
|
|
3367
|
+
await consumer.commitOffsets([nextOffset]);
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
const headers = decodeHeaders(message.headers);
|
|
3371
|
+
const target = headers[HEADER_DELAYED_TARGET] ?? stagingTopic.replace(/\.delayed$/, "");
|
|
3372
|
+
const until = parseInt(
|
|
3373
|
+
headers[HEADER_DELAYED_UNTIL] ?? "0",
|
|
3374
|
+
10
|
|
3375
|
+
);
|
|
3376
|
+
const remaining = until - Date.now();
|
|
3377
|
+
if (remaining > 0) {
|
|
3378
|
+
consumer.pause([{ topic: stagingTopic, partitions: [partition] }]);
|
|
3379
|
+
await sleep(remaining);
|
|
3380
|
+
consumer.resume([{ topic: stagingTopic, partitions: [partition] }]);
|
|
3381
|
+
}
|
|
3382
|
+
const forwardHeaders = Object.fromEntries(
|
|
3383
|
+
Object.entries(headers).filter(
|
|
3384
|
+
([k]) => k !== HEADER_DELAYED_UNTIL && k !== HEADER_DELAYED_TARGET
|
|
3385
|
+
)
|
|
3386
|
+
);
|
|
3387
|
+
const tx = await txProducer.transaction();
|
|
3388
|
+
try {
|
|
3389
|
+
await tx.send({
|
|
3390
|
+
topic: target,
|
|
3391
|
+
messages: [
|
|
3392
|
+
{
|
|
3393
|
+
// Forward the ORIGINAL wire bytes unchanged — no re-serialization,
|
|
3394
|
+
// so binary payloads (Avro/Protobuf) are relayed losslessly.
|
|
3395
|
+
value: message.value,
|
|
3396
|
+
key: message.key ? message.key.toString() : null,
|
|
3397
|
+
headers: forwardHeaders
|
|
3398
|
+
}
|
|
3399
|
+
]
|
|
3400
|
+
});
|
|
3401
|
+
await tx.sendOffsets({
|
|
3402
|
+
consumer,
|
|
3403
|
+
topics: [
|
|
3404
|
+
{
|
|
3405
|
+
topic: nextOffset.topic,
|
|
3406
|
+
partitions: [
|
|
3407
|
+
{ partition: nextOffset.partition, offset: nextOffset.offset }
|
|
3408
|
+
]
|
|
3409
|
+
}
|
|
3410
|
+
]
|
|
3411
|
+
});
|
|
3412
|
+
await tx.commit();
|
|
3413
|
+
ctx.logger.debug?.(
|
|
3414
|
+
`Delayed message relayed to "${target}" (deadline ${new Date(until).toISOString()})`
|
|
3415
|
+
);
|
|
3416
|
+
} catch (txErr) {
|
|
3417
|
+
try {
|
|
3418
|
+
await tx.abort();
|
|
3419
|
+
} catch {
|
|
3420
|
+
}
|
|
3421
|
+
ctx.logger.error(
|
|
3422
|
+
`Delayed relay to "${target}" failed \u2014 message will be redelivered:`,
|
|
3423
|
+
toError(txErr).stack
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
});
|
|
3428
|
+
ctx.runningConsumers.set(gid, "eachMessage");
|
|
3429
|
+
ctx.logger.log(
|
|
3430
|
+
`Delayed relay started for: ${delayedTopics.join(", ")} (group: ${gid})`
|
|
3431
|
+
);
|
|
3432
|
+
return {
|
|
3433
|
+
groupId: gid,
|
|
3434
|
+
ready: () => readyPromise,
|
|
3435
|
+
stop: async () => {
|
|
3436
|
+
await stopConsumerImpl(ctx, gid);
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// src/client/kafka.client/consumer/features/snapshot.ts
|
|
3007
3442
|
async function readSnapshotImpl(ctx, topic2, options = {}) {
|
|
3008
3443
|
await ctx.adminOps.ensureConnected();
|
|
3009
3444
|
let offsets;
|
|
@@ -3269,6 +3704,7 @@ var KafkaClient = class {
|
|
|
3269
3704
|
* ```
|
|
3270
3705
|
*/
|
|
3271
3706
|
constructor(clientId, groupId, brokers, options) {
|
|
3707
|
+
validateClientOptions(clientId, groupId, brokers, options);
|
|
3272
3708
|
this.clientId = clientId;
|
|
3273
3709
|
const logger = options?.logger ?? {
|
|
3274
3710
|
log: (msg) => console.log(`[KafkaClient:${clientId}] ${msg}`),
|
|
@@ -3276,12 +3712,14 @@ var KafkaClient = class {
|
|
|
3276
3712
|
error: (msg, ...args) => console.error(`[KafkaClient:${clientId}] ${msg}`, ...args),
|
|
3277
3713
|
debug: (msg, ...args) => console.debug(`[KafkaClient:${clientId}] ${msg}`, ...args)
|
|
3278
3714
|
};
|
|
3279
|
-
const
|
|
3715
|
+
const security = resolveSecurityOptions(options?.security, brokers, logger);
|
|
3716
|
+
const transport = options?.transport ?? new ConfluentTransport(clientId, brokers, security);
|
|
3280
3717
|
const producer = transport.producer();
|
|
3281
3718
|
const runningConsumers = /* @__PURE__ */ new Map();
|
|
3282
3719
|
const consumers = /* @__PURE__ */ new Map();
|
|
3283
3720
|
const consumerCreationOptions = /* @__PURE__ */ new Map();
|
|
3284
3721
|
const schemaRegistry = /* @__PURE__ */ new Map();
|
|
3722
|
+
const serde = options?.serde ?? new JsonSerde();
|
|
3285
3723
|
const adminOps = new AdminOps({
|
|
3286
3724
|
admin: transport.admin(),
|
|
3287
3725
|
logger,
|
|
@@ -3308,8 +3746,10 @@ var KafkaClient = class {
|
|
|
3308
3746
|
autoCreateTopicsEnabled: options?.autoCreateTopics ?? false,
|
|
3309
3747
|
strictSchemasEnabled: options?.strictSchemas ?? true,
|
|
3310
3748
|
numPartitions: options?.numPartitions ?? 1,
|
|
3749
|
+
serde,
|
|
3311
3750
|
txId: options?.transactionalId ?? `${clientId}-tx`,
|
|
3312
3751
|
clockRecoveryTopics: options?.clockRecovery?.topics ?? [],
|
|
3752
|
+
clockRecoveryTimeoutMs: options?.clockRecovery?.timeoutMs ?? 3e4,
|
|
3313
3753
|
lagThrottleOpts: options?.lagThrottle,
|
|
3314
3754
|
instrumentation: options?.instrumentation ?? [],
|
|
3315
3755
|
onMessageLost: options?.onMessageLost,
|
|
@@ -3319,6 +3759,7 @@ var KafkaClient = class {
|
|
|
3319
3759
|
producer,
|
|
3320
3760
|
txProducer: void 0,
|
|
3321
3761
|
txProducerInitPromise: void 0,
|
|
3762
|
+
_txChain: Promise.resolve(),
|
|
3322
3763
|
retryTxProducers: /* @__PURE__ */ new Map(),
|
|
3323
3764
|
consumers,
|
|
3324
3765
|
runningConsumers,
|
|
@@ -3340,6 +3781,7 @@ var KafkaClient = class {
|
|
|
3340
3781
|
strictSchemasEnabled: options?.strictSchemas ?? true,
|
|
3341
3782
|
instrumentation: options?.instrumentation ?? [],
|
|
3342
3783
|
logger,
|
|
3784
|
+
serde,
|
|
3343
3785
|
nextLamportClock: () => 0
|
|
3344
3786
|
// patched below
|
|
3345
3787
|
},
|
|
@@ -3358,6 +3800,7 @@ var KafkaClient = class {
|
|
|
3358
3800
|
strictSchemasEnabled: options?.strictSchemas ?? true,
|
|
3359
3801
|
instrumentation: options?.instrumentation ?? [],
|
|
3360
3802
|
logger,
|
|
3803
|
+
serde,
|
|
3361
3804
|
nextLamportClock: () => ++ctx._lamportClock
|
|
3362
3805
|
};
|
|
3363
3806
|
ctx.retryTopicDeps = buildRetryTopicDeps(ctx);
|
|
@@ -3442,6 +3885,31 @@ var KafkaClient = class {
|
|
|
3442
3885
|
startRoutedConsumer(topics, routing, options) {
|
|
3443
3886
|
return startRoutedConsumerImpl(this.ctx, topics, routing, options);
|
|
3444
3887
|
}
|
|
3888
|
+
// ── Consumer: delayed delivery relay ──────────────────────────────
|
|
3889
|
+
/**
|
|
3890
|
+
* Start a relay that delivers messages produced with
|
|
3891
|
+
* `SendOptions.deliverAfterMs` from `<topic>.delayed` to their target topic
|
|
3892
|
+
* once their deadline passes.
|
|
3893
|
+
*
|
|
3894
|
+
* Forwarding is transactional (produce + source-offset commit are atomic),
|
|
3895
|
+
* so no duplicates are relayed even if the relay crashes mid-forward.
|
|
3896
|
+
* Delivery time is a lower bound — the relay must be running for delayed
|
|
3897
|
+
* messages to be delivered at all.
|
|
3898
|
+
*
|
|
3899
|
+
* @param topics Target topic name(s) whose `<topic>.delayed` staging topics to relay.
|
|
3900
|
+
* @param options Optional `groupId` override (default: `<defaultGroupId>-delayed-relay`).
|
|
3901
|
+
*
|
|
3902
|
+
* @example
|
|
3903
|
+
* ```ts
|
|
3904
|
+
* await kafka.startDelayedRelay(['orders.reminder']);
|
|
3905
|
+
* await kafka.sendMessage('orders.reminder', payload, { deliverAfterMs: 60_000 });
|
|
3906
|
+
* // → delivered to orders.reminder ~60 s later
|
|
3907
|
+
* ```
|
|
3908
|
+
*/
|
|
3909
|
+
async startDelayedRelay(topics, options) {
|
|
3910
|
+
const list = Array.isArray(topics) ? topics : [topics];
|
|
3911
|
+
return startDelayedRelayImpl(this.ctx, list, options);
|
|
3912
|
+
}
|
|
3445
3913
|
// ── Consumer: transactional EOS ───────────────────────────────────
|
|
3446
3914
|
/** @inheritDoc */
|
|
3447
3915
|
async startTransactionalConsumer(topics, handler, options = {}) {
|
|
@@ -3587,34 +4055,909 @@ var KafkaClient = class {
|
|
|
3587
4055
|
function topic(name) {
|
|
3588
4056
|
return {
|
|
3589
4057
|
/** Provide an explicit message type without a runtime schema. */
|
|
3590
|
-
type: () => ({
|
|
4058
|
+
type: () => keyable({
|
|
3591
4059
|
__topic: name,
|
|
3592
4060
|
__type: void 0
|
|
3593
4061
|
}),
|
|
3594
|
-
schema: (schema) => ({
|
|
4062
|
+
schema: (schema) => keyable({
|
|
3595
4063
|
__topic: name,
|
|
3596
4064
|
__type: void 0,
|
|
3597
4065
|
__schema: schema
|
|
3598
4066
|
})
|
|
3599
4067
|
};
|
|
3600
4068
|
}
|
|
4069
|
+
function keyable(desc) {
|
|
4070
|
+
return {
|
|
4071
|
+
...desc,
|
|
4072
|
+
key: (extractor) => keyable({ ...desc, __key: extractor }),
|
|
4073
|
+
serde: (serde) => keyable({ ...desc, __serde: serde })
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
// src/client/message/versioned-schema.ts
|
|
4078
|
+
function versionedSchema(versions, options) {
|
|
4079
|
+
const registered = Object.keys(versions).map(Number).filter((v) => Number.isInteger(v) && v > 0).sort((a, b) => a - b);
|
|
4080
|
+
if (registered.length === 0) {
|
|
4081
|
+
throw new Error(
|
|
4082
|
+
"versionedSchema: at least one schema version must be registered (keys must be positive integers)"
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
const latestVersion = registered[registered.length - 1];
|
|
4086
|
+
return {
|
|
4087
|
+
async parse(data, ctx) {
|
|
4088
|
+
const version = ctx?.version ?? latestVersion;
|
|
4089
|
+
const schema = versions[version];
|
|
4090
|
+
if (!schema) {
|
|
4091
|
+
throw new Error(
|
|
4092
|
+
`versionedSchema: no schema registered for version ${version}${ctx?.topic ? ` (topic "${ctx.topic}")` : ""} \u2014 registered versions: ${registered.join(", ")}`
|
|
4093
|
+
);
|
|
4094
|
+
}
|
|
4095
|
+
const parsed = await schema.parse(data, ctx);
|
|
4096
|
+
if (version < latestVersion && options?.migrate) {
|
|
4097
|
+
return options.migrate(parsed, version, latestVersion);
|
|
4098
|
+
}
|
|
4099
|
+
return parsed;
|
|
4100
|
+
}
|
|
4101
|
+
};
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
// src/client/message/schema-registry.ts
|
|
4105
|
+
var SchemaRegistryClient = class {
|
|
4106
|
+
constructor(options) {
|
|
4107
|
+
this.options = options;
|
|
4108
|
+
if (!options.baseUrl) {
|
|
4109
|
+
throw new Error("SchemaRegistryClient: baseUrl is required");
|
|
4110
|
+
}
|
|
4111
|
+
this.fetchFn = options.fetchFn ?? fetch;
|
|
4112
|
+
this.cacheTtlMs = options.cacheTtlMs ?? 3e5;
|
|
4113
|
+
}
|
|
4114
|
+
options;
|
|
4115
|
+
fetchFn;
|
|
4116
|
+
cacheTtlMs;
|
|
4117
|
+
latestCache = /* @__PURE__ */ new Map();
|
|
4118
|
+
/**
|
|
4119
|
+
* `id → schema` cache. Schema ids are immutable in a Confluent-compatible
|
|
4120
|
+
* registry (a given id always maps to the same schema string), so entries
|
|
4121
|
+
* are cached for the lifetime of the client with no TTL.
|
|
4122
|
+
*/
|
|
4123
|
+
byIdCache = /* @__PURE__ */ new Map();
|
|
4124
|
+
headers() {
|
|
4125
|
+
const h = {
|
|
4126
|
+
"Content-Type": "application/vnd.schemaregistry.v1+json"
|
|
4127
|
+
};
|
|
4128
|
+
if (this.options.auth) {
|
|
4129
|
+
const { username, password } = this.options.auth;
|
|
4130
|
+
h["Authorization"] = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
|
|
4131
|
+
}
|
|
4132
|
+
return h;
|
|
4133
|
+
}
|
|
4134
|
+
async request(method, path, body) {
|
|
4135
|
+
const url = `${this.options.baseUrl.replace(/\/$/, "")}${path}`;
|
|
4136
|
+
const res = await this.fetchFn(url, {
|
|
4137
|
+
method,
|
|
4138
|
+
headers: this.headers(),
|
|
4139
|
+
...body !== void 0 && { body: JSON.stringify(body) }
|
|
4140
|
+
});
|
|
4141
|
+
if (!res.ok) {
|
|
4142
|
+
const text = await res.text().catch(() => "");
|
|
4143
|
+
throw new Error(
|
|
4144
|
+
`SchemaRegistry ${method} ${path} failed: ${res.status} ${res.statusText}${text ? ` \u2014 ${text}` : ""}`
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
return await res.json();
|
|
4148
|
+
}
|
|
4149
|
+
/** Fetch the latest schema registered under `subject`. Cached for `cacheTtlMs`. */
|
|
4150
|
+
async getLatestSchema(subject) {
|
|
4151
|
+
const cached = this.latestCache.get(subject);
|
|
4152
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
4153
|
+
const raw = await this.request("GET", `/subjects/${encodeURIComponent(subject)}/versions/latest`);
|
|
4154
|
+
const value = {
|
|
4155
|
+
id: raw.id,
|
|
4156
|
+
version: raw.version,
|
|
4157
|
+
schema: raw.schema
|
|
4158
|
+
};
|
|
4159
|
+
this.latestCache.set(subject, {
|
|
4160
|
+
value,
|
|
4161
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
4162
|
+
});
|
|
4163
|
+
return value;
|
|
4164
|
+
}
|
|
4165
|
+
/**
|
|
4166
|
+
* Fetch a schema by its globally unique registry id (`GET /schemas/ids/{id}`).
|
|
4167
|
+
*
|
|
4168
|
+
* Used by the Avro/Protobuf serdes on the deserialize path: the writer schema
|
|
4169
|
+
* id is read from the Confluent wire-format prefix, then resolved here. Results
|
|
4170
|
+
* are cached forever (schema ids are immutable), so a given id triggers exactly
|
|
4171
|
+
* one registry round-trip regardless of how many messages reference it.
|
|
4172
|
+
*/
|
|
4173
|
+
async getSchemaById(id) {
|
|
4174
|
+
const cached = this.byIdCache.get(id);
|
|
4175
|
+
if (cached) return cached;
|
|
4176
|
+
const raw = await this.request(
|
|
4177
|
+
"GET",
|
|
4178
|
+
`/schemas/ids/${id}`
|
|
4179
|
+
);
|
|
4180
|
+
const value = { id, schema: raw.schema, schemaType: raw.schemaType };
|
|
4181
|
+
this.byIdCache.set(id, value);
|
|
4182
|
+
return value;
|
|
4183
|
+
}
|
|
4184
|
+
/** Fetch a specific schema version of a subject. */
|
|
4185
|
+
async getSchemaVersion(subject, version) {
|
|
4186
|
+
const raw = await this.request(
|
|
4187
|
+
"GET",
|
|
4188
|
+
`/subjects/${encodeURIComponent(subject)}/versions/${version}`
|
|
4189
|
+
);
|
|
4190
|
+
return { id: raw.id, version: raw.version, schema: raw.schema };
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* Register a schema under `subject` (idempotent — re-registering the same
|
|
4194
|
+
* schema returns the existing id). Returns the registry-assigned schema id.
|
|
4195
|
+
*/
|
|
4196
|
+
async registerSchema(subject, schema, schemaType = "JSON") {
|
|
4197
|
+
this.latestCache.delete(subject);
|
|
4198
|
+
return this.request(
|
|
4199
|
+
"POST",
|
|
4200
|
+
`/subjects/${encodeURIComponent(subject)}/versions`,
|
|
4201
|
+
{ schema, schemaType }
|
|
4202
|
+
);
|
|
4203
|
+
}
|
|
4204
|
+
/**
|
|
4205
|
+
* Test `schema` against the subject's compatibility policy without registering.
|
|
4206
|
+
* Returns `true` when the registry reports the schema as compatible.
|
|
4207
|
+
*/
|
|
4208
|
+
async checkCompatibility(subject, schema, schemaType = "JSON") {
|
|
4209
|
+
const res = await this.request(
|
|
4210
|
+
"POST",
|
|
4211
|
+
`/compatibility/subjects/${encodeURIComponent(subject)}/versions/latest`,
|
|
4212
|
+
{ schema, schemaType }
|
|
4213
|
+
);
|
|
4214
|
+
return res.is_compatible;
|
|
4215
|
+
}
|
|
4216
|
+
};
|
|
4217
|
+
function registrySchema(client, subject, options) {
|
|
4218
|
+
const enforceVersion = options?.enforceVersion ?? true;
|
|
4219
|
+
return {
|
|
4220
|
+
async parse(data, ctx) {
|
|
4221
|
+
const latest = await client.getLatestSchema(subject);
|
|
4222
|
+
if (enforceVersion && ctx?.version !== void 0 && ctx.version > latest.version) {
|
|
4223
|
+
throw new Error(
|
|
4224
|
+
`registrySchema: message version ${ctx.version} for subject "${subject}" is newer than the latest registered version ${latest.version} \u2014 register the schema before producing with it`
|
|
4225
|
+
);
|
|
4226
|
+
}
|
|
4227
|
+
if (options?.validator) {
|
|
4228
|
+
return options.validator.parse(data, ctx);
|
|
4229
|
+
}
|
|
4230
|
+
return data;
|
|
4231
|
+
}
|
|
4232
|
+
};
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
// src/client/outbox/outbox.store.ts
|
|
4236
|
+
var InMemoryOutboxStore = class {
|
|
4237
|
+
/** Insertion-ordered rows. `published` flips to true after `markPublished`. */
|
|
4238
|
+
rows = [];
|
|
4239
|
+
/**
|
|
4240
|
+
* Append a message to the outbox. In a real store this INSERT would run inside
|
|
4241
|
+
* the same DB transaction as the corresponding business write.
|
|
4242
|
+
*/
|
|
4243
|
+
add(message) {
|
|
4244
|
+
this.rows.push({ message, published: false });
|
|
4245
|
+
}
|
|
4246
|
+
async fetchUnpublished(limit) {
|
|
4247
|
+
const out = [];
|
|
4248
|
+
for (const row of this.rows) {
|
|
4249
|
+
if (row.published) continue;
|
|
4250
|
+
out.push(row.message);
|
|
4251
|
+
if (out.length >= limit) break;
|
|
4252
|
+
}
|
|
4253
|
+
return out;
|
|
4254
|
+
}
|
|
4255
|
+
async markPublished(ids) {
|
|
4256
|
+
const idSet = new Set(ids);
|
|
4257
|
+
for (const row of this.rows) {
|
|
4258
|
+
if (idSet.has(row.message.id)) row.published = true;
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
/** Test helper: count of rows not yet marked published. */
|
|
4262
|
+
get pendingCount() {
|
|
4263
|
+
return this.rows.filter((r) => !r.published).length;
|
|
4264
|
+
}
|
|
4265
|
+
/** Test helper: count of rows marked published. */
|
|
4266
|
+
get publishedCount() {
|
|
4267
|
+
return this.rows.filter((r) => r.published).length;
|
|
4268
|
+
}
|
|
4269
|
+
};
|
|
4270
|
+
|
|
4271
|
+
// src/client/outbox/outbox.relay.ts
|
|
4272
|
+
function toError2(e) {
|
|
4273
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
4274
|
+
}
|
|
4275
|
+
function startOutboxRelay(kafka, store, options = {}) {
|
|
4276
|
+
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
4277
|
+
const batchSize = options.batchSize ?? 100;
|
|
4278
|
+
const onError = options.onError ?? ((error, batch) => {
|
|
4279
|
+
console.error(
|
|
4280
|
+
`[outbox] batch of ${batch.length} message(s) failed \u2014 will retry:`,
|
|
4281
|
+
error
|
|
4282
|
+
);
|
|
4283
|
+
});
|
|
4284
|
+
const onPublished = options.onPublished;
|
|
4285
|
+
let stopped = false;
|
|
4286
|
+
let running = false;
|
|
4287
|
+
let inFlight = Promise.resolve();
|
|
4288
|
+
const iterate = async () => {
|
|
4289
|
+
let batch = [];
|
|
4290
|
+
try {
|
|
4291
|
+
batch = await store.fetchUnpublished(batchSize);
|
|
4292
|
+
if (batch.length === 0) return;
|
|
4293
|
+
await kafka.transaction(async (tx) => {
|
|
4294
|
+
for (const msg of batch) {
|
|
4295
|
+
await tx.send(msg.topic, msg.payload, {
|
|
4296
|
+
key: msg.key,
|
|
4297
|
+
headers: msg.headers,
|
|
4298
|
+
correlationId: msg.correlationId,
|
|
4299
|
+
eventId: msg.eventId
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
await store.markPublished(batch.map((m) => m.id));
|
|
4304
|
+
onPublished?.(batch.length);
|
|
4305
|
+
} catch (err) {
|
|
4306
|
+
onError(toError2(err), batch);
|
|
4307
|
+
}
|
|
4308
|
+
};
|
|
4309
|
+
const tick = () => {
|
|
4310
|
+
if (stopped || running) return;
|
|
4311
|
+
running = true;
|
|
4312
|
+
inFlight = iterate().finally(() => {
|
|
4313
|
+
running = false;
|
|
4314
|
+
});
|
|
4315
|
+
};
|
|
4316
|
+
const timer = setInterval(tick, pollIntervalMs);
|
|
4317
|
+
timer.unref?.();
|
|
4318
|
+
return {
|
|
4319
|
+
stop: async () => {
|
|
4320
|
+
stopped = true;
|
|
4321
|
+
clearInterval(timer);
|
|
4322
|
+
await inFlight;
|
|
4323
|
+
}
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
// src/client/security/providers.ts
|
|
4328
|
+
var defaultImport = (specifier) => import(specifier);
|
|
4329
|
+
function awsMskIamProvider(options) {
|
|
4330
|
+
const importFn = options.importFn ?? defaultImport;
|
|
4331
|
+
return async () => {
|
|
4332
|
+
let signer;
|
|
4333
|
+
try {
|
|
4334
|
+
signer = await importFn("aws-msk-iam-sasl-signer-js");
|
|
4335
|
+
} catch {
|
|
4336
|
+
throw new Error(
|
|
4337
|
+
"awsMskIamProvider: package 'aws-msk-iam-sasl-signer-js' is not installed. Run `npm install aws-msk-iam-sasl-signer-js` to enable MSK IAM authentication."
|
|
4338
|
+
);
|
|
4339
|
+
}
|
|
4340
|
+
const { token, expiryTime } = await signer.generateAuthToken({
|
|
4341
|
+
region: options.region
|
|
4342
|
+
});
|
|
4343
|
+
return {
|
|
4344
|
+
value: token,
|
|
4345
|
+
principal: "msk-iam",
|
|
4346
|
+
// expiryTime is epoch ms per the signer's contract
|
|
4347
|
+
lifetimeMs: expiryTime
|
|
4348
|
+
};
|
|
4349
|
+
};
|
|
4350
|
+
}
|
|
4351
|
+
function gcpAccessTokenProvider(options = {}) {
|
|
4352
|
+
const importFn = options.importFn ?? defaultImport;
|
|
4353
|
+
const ttlMs = options.tokenTtlMs ?? 50 * 6e4;
|
|
4354
|
+
return async () => {
|
|
4355
|
+
let lib;
|
|
4356
|
+
try {
|
|
4357
|
+
lib = await importFn("google-auth-library");
|
|
4358
|
+
} catch {
|
|
4359
|
+
throw new Error(
|
|
4360
|
+
"gcpAccessTokenProvider: package 'google-auth-library' is not installed. Run `npm install google-auth-library` to enable GCP authentication."
|
|
4361
|
+
);
|
|
4362
|
+
}
|
|
4363
|
+
const auth = new lib.GoogleAuth({
|
|
4364
|
+
scopes: options.scopes ?? ["https://www.googleapis.com/auth/cloud-platform"]
|
|
4365
|
+
});
|
|
4366
|
+
const token = await auth.getAccessToken();
|
|
4367
|
+
if (!token) {
|
|
4368
|
+
throw new Error(
|
|
4369
|
+
"gcpAccessTokenProvider: google-auth-library returned no access token \u2014 check Application Default Credentials."
|
|
4370
|
+
);
|
|
4371
|
+
}
|
|
4372
|
+
return {
|
|
4373
|
+
value: token,
|
|
4374
|
+
principal: options.principal ?? "gcp",
|
|
4375
|
+
lifetimeMs: Date.now() + ttlMs
|
|
4376
|
+
};
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
// src/client/security/acl.ts
|
|
4381
|
+
function addResource(out, r) {
|
|
4382
|
+
const key = `${r.resourceType}:${r.patternType}:${r.name}`;
|
|
4383
|
+
const existing = out.get(key);
|
|
4384
|
+
if (existing) {
|
|
4385
|
+
for (const op of r.operations)
|
|
4386
|
+
if (!existing.operations.includes(op)) existing.operations.push(op);
|
|
4387
|
+
if (!existing.reason.includes(r.reason))
|
|
4388
|
+
existing.reason += `; ${r.reason}`;
|
|
4389
|
+
} else {
|
|
4390
|
+
out.set(key, { ...r, operations: [...r.operations] });
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
function describeRequiredAcls(input) {
|
|
4394
|
+
const out = /* @__PURE__ */ new Map();
|
|
4395
|
+
const f = input.features ?? {};
|
|
4396
|
+
const produce = input.produceTopics ?? [];
|
|
4397
|
+
const consume = input.consumeTopics ?? [];
|
|
4398
|
+
const groups = input.groupIds ?? [];
|
|
4399
|
+
for (const t of produce) {
|
|
4400
|
+
addResource(out, {
|
|
4401
|
+
resourceType: "topic",
|
|
4402
|
+
patternType: "literal",
|
|
4403
|
+
name: t,
|
|
4404
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4405
|
+
reason: "sendMessage/sendBatch"
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
for (const t of consume) {
|
|
4409
|
+
addResource(out, {
|
|
4410
|
+
resourceType: "topic",
|
|
4411
|
+
patternType: "literal",
|
|
4412
|
+
name: t,
|
|
4413
|
+
operations: ["READ", "DESCRIBE"],
|
|
4414
|
+
reason: "startConsumer"
|
|
4415
|
+
});
|
|
4416
|
+
}
|
|
4417
|
+
for (const g of groups) {
|
|
4418
|
+
addResource(out, {
|
|
4419
|
+
resourceType: "group",
|
|
4420
|
+
patternType: "literal",
|
|
4421
|
+
name: g,
|
|
4422
|
+
operations: ["READ", "DESCRIBE"],
|
|
4423
|
+
reason: "consumer group membership + offset commits"
|
|
4424
|
+
});
|
|
4425
|
+
}
|
|
4426
|
+
if (f.dlq) {
|
|
4427
|
+
for (const t of consume) {
|
|
4428
|
+
addResource(out, {
|
|
4429
|
+
resourceType: "topic",
|
|
4430
|
+
patternType: "literal",
|
|
4431
|
+
name: `${t}.dlq`,
|
|
4432
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4433
|
+
reason: "dlq: true \u2014 failed messages routed to DLQ"
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
if (f.retryTopics) {
|
|
4438
|
+
for (const t of consume) {
|
|
4439
|
+
for (let level = 1; level <= f.retryTopics.maxRetries; level++) {
|
|
4440
|
+
addResource(out, {
|
|
4441
|
+
resourceType: "topic",
|
|
4442
|
+
patternType: "literal",
|
|
4443
|
+
name: `${t}.retry.${level}`,
|
|
4444
|
+
operations: ["READ", "WRITE", "DESCRIBE"],
|
|
4445
|
+
reason: "retryTopics \u2014 retry chain produce + companion consume"
|
|
4446
|
+
});
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
for (const g of groups) {
|
|
4450
|
+
addResource(out, {
|
|
4451
|
+
resourceType: "group",
|
|
4452
|
+
patternType: "prefixed",
|
|
4453
|
+
name: `${g}-retry.`,
|
|
4454
|
+
operations: ["READ", "DESCRIBE"],
|
|
4455
|
+
reason: "retryTopics \u2014 companion retry-level consumer groups"
|
|
4456
|
+
});
|
|
4457
|
+
addResource(out, {
|
|
4458
|
+
resourceType: "transactional-id",
|
|
4459
|
+
patternType: "prefixed",
|
|
4460
|
+
name: `${g}-`,
|
|
4461
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4462
|
+
reason: "retryTopics \u2014 EOS routing transactions per retry level"
|
|
4463
|
+
});
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
if (f.delayedDelivery) {
|
|
4467
|
+
for (const t of [.../* @__PURE__ */ new Set([...produce, ...consume])]) {
|
|
4468
|
+
addResource(out, {
|
|
4469
|
+
resourceType: "topic",
|
|
4470
|
+
patternType: "literal",
|
|
4471
|
+
name: `${t}.delayed`,
|
|
4472
|
+
operations: ["READ", "WRITE", "DESCRIBE"],
|
|
4473
|
+
reason: "deliverAfterMs staging + startDelayedRelay consume"
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
for (const g of groups) {
|
|
4477
|
+
addResource(out, {
|
|
4478
|
+
resourceType: "group",
|
|
4479
|
+
patternType: "literal",
|
|
4480
|
+
name: `${g}-delayed-relay`,
|
|
4481
|
+
operations: ["READ", "DESCRIBE"],
|
|
4482
|
+
reason: "startDelayedRelay consumer group"
|
|
4483
|
+
});
|
|
4484
|
+
addResource(out, {
|
|
4485
|
+
resourceType: "transactional-id",
|
|
4486
|
+
patternType: "literal",
|
|
4487
|
+
name: `${g}-delayed-relay-tx`,
|
|
4488
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4489
|
+
reason: "startDelayedRelay transactional forwarding"
|
|
4490
|
+
});
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
if (f.duplicatesTopic) {
|
|
4494
|
+
if (typeof f.duplicatesTopic === "string") {
|
|
4495
|
+
addResource(out, {
|
|
4496
|
+
resourceType: "topic",
|
|
4497
|
+
patternType: "literal",
|
|
4498
|
+
name: f.duplicatesTopic,
|
|
4499
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4500
|
+
reason: "deduplication.strategy 'topic' \u2014 custom duplicates topic"
|
|
4501
|
+
});
|
|
4502
|
+
} else {
|
|
4503
|
+
for (const t of consume) {
|
|
4504
|
+
addResource(out, {
|
|
4505
|
+
resourceType: "topic",
|
|
4506
|
+
patternType: "literal",
|
|
4507
|
+
name: `${t}.duplicates`,
|
|
4508
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4509
|
+
reason: "deduplication.strategy 'topic'"
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
if (f.dlqReplay) {
|
|
4515
|
+
for (const t of consume) {
|
|
4516
|
+
addResource(out, {
|
|
4517
|
+
resourceType: "group",
|
|
4518
|
+
patternType: "prefixed",
|
|
4519
|
+
name: `${t}.dlq-replay`,
|
|
4520
|
+
operations: ["READ", "DESCRIBE", "DELETE"],
|
|
4521
|
+
reason: "replayDlq \u2014 ephemeral/stable replay groups (deleted after use)"
|
|
4522
|
+
});
|
|
4523
|
+
addResource(out, {
|
|
4524
|
+
resourceType: "topic",
|
|
4525
|
+
patternType: "literal",
|
|
4526
|
+
name: `${t}.dlq`,
|
|
4527
|
+
operations: ["READ", "DESCRIBE"],
|
|
4528
|
+
reason: "replayDlq \u2014 reads the DLQ"
|
|
4529
|
+
});
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
if (f.snapshots) {
|
|
4533
|
+
addResource(out, {
|
|
4534
|
+
resourceType: "group",
|
|
4535
|
+
patternType: "prefixed",
|
|
4536
|
+
name: `${input.clientId}-snapshot-`,
|
|
4537
|
+
operations: ["READ", "DESCRIBE", "DELETE"],
|
|
4538
|
+
reason: "readSnapshot \u2014 timestamped ephemeral groups (deleted after use)"
|
|
4539
|
+
});
|
|
4540
|
+
}
|
|
4541
|
+
if (f.clockRecovery) {
|
|
4542
|
+
addResource(out, {
|
|
4543
|
+
resourceType: "group",
|
|
4544
|
+
patternType: "prefixed",
|
|
4545
|
+
name: `${input.clientId}-clock-recovery-`,
|
|
4546
|
+
operations: ["READ", "DESCRIBE", "DELETE"],
|
|
4547
|
+
reason: "clockRecovery \u2014 timestamped ephemeral groups (deleted after use)"
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
if (f.transactions) {
|
|
4551
|
+
addResource(out, {
|
|
4552
|
+
resourceType: "transactional-id",
|
|
4553
|
+
patternType: "literal",
|
|
4554
|
+
name: `${input.clientId}-tx`,
|
|
4555
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4556
|
+
reason: "transaction() \u2014 default transactionalId (override-aware: adjust if you set one)"
|
|
4557
|
+
});
|
|
4558
|
+
}
|
|
4559
|
+
if (f.autoCreateTopics) {
|
|
4560
|
+
addResource(out, {
|
|
4561
|
+
resourceType: "cluster",
|
|
4562
|
+
patternType: "literal",
|
|
4563
|
+
name: "kafka-cluster",
|
|
4564
|
+
operations: ["CREATE"],
|
|
4565
|
+
reason: "autoCreateTopics: true \u2014 not recommended in production"
|
|
4566
|
+
});
|
|
4567
|
+
}
|
|
4568
|
+
return [...out.values()];
|
|
4569
|
+
}
|
|
4570
|
+
function toKafkaAclCommands(resources, principal, bootstrapServer = "<bootstrap-server>") {
|
|
4571
|
+
return resources.map((r) => {
|
|
4572
|
+
const ops = r.operations.map((o) => `--operation ${o}`).join(" ");
|
|
4573
|
+
const resourceFlag = r.resourceType === "topic" ? `--topic '${r.name}'` : r.resourceType === "group" ? `--group '${r.name}'` : r.resourceType === "transactional-id" ? `--transactional-id '${r.name}'` : "--cluster";
|
|
4574
|
+
const pattern = r.patternType === "prefixed" ? " --resource-pattern-type prefixed" : "";
|
|
4575
|
+
return `kafka-acls.sh --bootstrap-server ${bootstrapServer} --add --allow-principal '${principal}' ${ops} ${resourceFlag}${pattern} # ${r.reason}`;
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
var MSK_TOPIC_ACTIONS = {
|
|
4579
|
+
READ: ["kafka-cluster:ReadData", "kafka-cluster:DescribeTopic"],
|
|
4580
|
+
WRITE: ["kafka-cluster:WriteData", "kafka-cluster:DescribeTopic"],
|
|
4581
|
+
DESCRIBE: ["kafka-cluster:DescribeTopic"],
|
|
4582
|
+
CREATE: ["kafka-cluster:CreateTopic"],
|
|
4583
|
+
DELETE: ["kafka-cluster:DeleteTopic"]
|
|
4584
|
+
};
|
|
4585
|
+
var MSK_GROUP_ACTIONS = {
|
|
4586
|
+
READ: ["kafka-cluster:AlterGroup", "kafka-cluster:DescribeGroup"],
|
|
4587
|
+
DESCRIBE: ["kafka-cluster:DescribeGroup"],
|
|
4588
|
+
DELETE: ["kafka-cluster:DeleteGroup"]
|
|
4589
|
+
};
|
|
4590
|
+
var MSK_TX_ACTIONS = {
|
|
4591
|
+
WRITE: [
|
|
4592
|
+
"kafka-cluster:AlterTransactionalId",
|
|
4593
|
+
"kafka-cluster:DescribeTransactionalId"
|
|
4594
|
+
],
|
|
4595
|
+
DESCRIBE: ["kafka-cluster:DescribeTransactionalId"]
|
|
4596
|
+
};
|
|
4597
|
+
function toMskIamPolicy(resources, cluster) {
|
|
4598
|
+
const { region, accountId, clusterName, clusterUuid } = cluster;
|
|
4599
|
+
const arn = (type, name) => `arn:aws:kafka:${region}:${accountId}:${type}/${clusterName}/${clusterUuid}/${name}`;
|
|
4600
|
+
const statements = [
|
|
4601
|
+
{
|
|
4602
|
+
Sid: "Connect",
|
|
4603
|
+
Effect: "Allow",
|
|
4604
|
+
Action: ["kafka-cluster:Connect"],
|
|
4605
|
+
Resource: [
|
|
4606
|
+
`arn:aws:kafka:${region}:${accountId}:cluster/${clusterName}/${clusterUuid}`
|
|
4607
|
+
]
|
|
4608
|
+
}
|
|
4609
|
+
];
|
|
4610
|
+
let sid = 0;
|
|
4611
|
+
for (const r of resources) {
|
|
4612
|
+
const suffix = r.patternType === "prefixed" ? `${r.name}*` : r.name;
|
|
4613
|
+
let actions = [];
|
|
4614
|
+
let resource;
|
|
4615
|
+
if (r.resourceType === "topic") {
|
|
4616
|
+
actions = [...new Set(r.operations.flatMap((o) => MSK_TOPIC_ACTIONS[o] ?? []))];
|
|
4617
|
+
resource = arn("topic", suffix);
|
|
4618
|
+
} else if (r.resourceType === "group") {
|
|
4619
|
+
actions = [...new Set(r.operations.flatMap((o) => MSK_GROUP_ACTIONS[o] ?? []))];
|
|
4620
|
+
resource = arn("group", suffix);
|
|
4621
|
+
} else if (r.resourceType === "transactional-id") {
|
|
4622
|
+
actions = [...new Set(r.operations.flatMap((o) => MSK_TX_ACTIONS[o] ?? []))];
|
|
4623
|
+
resource = arn("transactional-id", suffix);
|
|
4624
|
+
} else {
|
|
4625
|
+
actions = ["kafka-cluster:CreateTopic"];
|
|
4626
|
+
resource = `arn:aws:kafka:${region}:${accountId}:topic/${clusterName}/${clusterUuid}/*`;
|
|
4627
|
+
}
|
|
4628
|
+
if (actions.length === 0 || !resource) continue;
|
|
4629
|
+
statements.push({
|
|
4630
|
+
Sid: `Acl${sid++}`,
|
|
4631
|
+
Effect: "Allow",
|
|
4632
|
+
Action: actions,
|
|
4633
|
+
Resource: [resource]
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
return { Version: "2012-10-17", Statement: statements };
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4639
|
+
// src/client/config/from-env.ts
|
|
4640
|
+
var TRUE_VALUES = /* @__PURE__ */ new Set(["true", "1", "yes"]);
|
|
4641
|
+
var FALSE_VALUES = /* @__PURE__ */ new Set(["false", "0", "no"]);
|
|
4642
|
+
function parseBool(name, raw) {
|
|
4643
|
+
const normalized = raw.trim().toLowerCase();
|
|
4644
|
+
if (TRUE_VALUES.has(normalized)) return true;
|
|
4645
|
+
if (FALSE_VALUES.has(normalized)) return false;
|
|
4646
|
+
throw new Error(
|
|
4647
|
+
`Invalid boolean for ${name}: "${raw}". Use one of true/false, 1/0, yes/no (case-insensitive).`
|
|
4648
|
+
);
|
|
4649
|
+
}
|
|
4650
|
+
function parseNum(name, raw) {
|
|
4651
|
+
const value = Number(raw.trim());
|
|
4652
|
+
if (Number.isNaN(value)) {
|
|
4653
|
+
throw new Error(`Invalid number for ${name}: "${raw}".`);
|
|
4654
|
+
}
|
|
4655
|
+
return value;
|
|
4656
|
+
}
|
|
4657
|
+
function parseList(raw) {
|
|
4658
|
+
return raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
4659
|
+
}
|
|
4660
|
+
function parseEnum(name, raw, allowed) {
|
|
4661
|
+
const value = raw.trim();
|
|
4662
|
+
if (!allowed.includes(value)) {
|
|
4663
|
+
throw new Error(
|
|
4664
|
+
`Invalid value for ${name}: "${raw}". Allowed: ${allowed.join(", ")}.`
|
|
4665
|
+
);
|
|
4666
|
+
}
|
|
4667
|
+
return value;
|
|
4668
|
+
}
|
|
4669
|
+
function readVar(env, key, apply) {
|
|
4670
|
+
const raw = env[key];
|
|
4671
|
+
if (raw === void 0 || raw.trim() === "") return;
|
|
4672
|
+
apply(raw);
|
|
4673
|
+
}
|
|
4674
|
+
function kafkaClientConfigFromEnv(env = process.env, prefix = "KAFKA_") {
|
|
4675
|
+
const options = {};
|
|
4676
|
+
const result = { options };
|
|
4677
|
+
readVar(env, `${prefix}CLIENT_ID`, (raw) => {
|
|
4678
|
+
result.clientId = raw.trim();
|
|
4679
|
+
});
|
|
4680
|
+
readVar(env, `${prefix}GROUP_ID`, (raw) => {
|
|
4681
|
+
result.groupId = raw.trim();
|
|
4682
|
+
});
|
|
4683
|
+
readVar(env, `${prefix}BROKERS`, (raw) => {
|
|
4684
|
+
result.brokers = parseList(raw);
|
|
4685
|
+
});
|
|
4686
|
+
readVar(env, `${prefix}AUTO_CREATE_TOPICS`, (raw) => {
|
|
4687
|
+
options.autoCreateTopics = parseBool(`${prefix}AUTO_CREATE_TOPICS`, raw);
|
|
4688
|
+
});
|
|
4689
|
+
readVar(env, `${prefix}STRICT_SCHEMAS`, (raw) => {
|
|
4690
|
+
options.strictSchemas = parseBool(`${prefix}STRICT_SCHEMAS`, raw);
|
|
4691
|
+
});
|
|
4692
|
+
readVar(env, `${prefix}NUM_PARTITIONS`, (raw) => {
|
|
4693
|
+
options.numPartitions = parseNum(`${prefix}NUM_PARTITIONS`, raw);
|
|
4694
|
+
});
|
|
4695
|
+
readVar(env, `${prefix}TRANSACTIONAL_ID`, (raw) => {
|
|
4696
|
+
options.transactionalId = raw.trim();
|
|
4697
|
+
});
|
|
4698
|
+
readVar(env, `${prefix}CLOCK_RECOVERY_TOPICS`, (raw) => {
|
|
4699
|
+
const topics = parseList(raw);
|
|
4700
|
+
if (topics.length === 0) return;
|
|
4701
|
+
options.clockRecovery = { topics };
|
|
4702
|
+
});
|
|
4703
|
+
readVar(env, `${prefix}CLOCK_RECOVERY_TIMEOUT_MS`, (raw) => {
|
|
4704
|
+
const timeoutMs = parseNum(`${prefix}CLOCK_RECOVERY_TIMEOUT_MS`, raw);
|
|
4705
|
+
if (options.clockRecovery) {
|
|
4706
|
+
options.clockRecovery.timeoutMs = timeoutMs;
|
|
4707
|
+
}
|
|
4708
|
+
});
|
|
4709
|
+
readVar(env, `${prefix}LAG_THROTTLE_MAX_LAG`, (raw) => {
|
|
4710
|
+
options.lagThrottle = {
|
|
4711
|
+
maxLag: parseNum(`${prefix}LAG_THROTTLE_MAX_LAG`, raw)
|
|
4712
|
+
};
|
|
4713
|
+
});
|
|
4714
|
+
readVar(env, `${prefix}LAG_THROTTLE_GROUP_ID`, (raw) => {
|
|
4715
|
+
if (options.lagThrottle) options.lagThrottle.groupId = raw.trim();
|
|
4716
|
+
});
|
|
4717
|
+
readVar(env, `${prefix}LAG_THROTTLE_POLL_INTERVAL_MS`, (raw) => {
|
|
4718
|
+
if (options.lagThrottle) {
|
|
4719
|
+
options.lagThrottle.pollIntervalMs = parseNum(
|
|
4720
|
+
`${prefix}LAG_THROTTLE_POLL_INTERVAL_MS`,
|
|
4721
|
+
raw
|
|
4722
|
+
);
|
|
4723
|
+
}
|
|
4724
|
+
});
|
|
4725
|
+
readVar(env, `${prefix}LAG_THROTTLE_MAX_WAIT_MS`, (raw) => {
|
|
4726
|
+
if (options.lagThrottle) {
|
|
4727
|
+
options.lagThrottle.maxWaitMs = parseNum(
|
|
4728
|
+
`${prefix}LAG_THROTTLE_MAX_WAIT_MS`,
|
|
4729
|
+
raw
|
|
4730
|
+
);
|
|
4731
|
+
}
|
|
4732
|
+
});
|
|
4733
|
+
const security = securityFromEnv(env, prefix);
|
|
4734
|
+
if (security) options.security = security;
|
|
4735
|
+
return result;
|
|
4736
|
+
}
|
|
4737
|
+
function securityFromEnv(env, prefix) {
|
|
4738
|
+
let ssl;
|
|
4739
|
+
let allowInsecure;
|
|
4740
|
+
let mechanism;
|
|
4741
|
+
let username;
|
|
4742
|
+
let password;
|
|
4743
|
+
readVar(env, `${prefix}SSL`, (raw) => {
|
|
4744
|
+
ssl = parseBool(`${prefix}SSL`, raw);
|
|
4745
|
+
});
|
|
4746
|
+
readVar(env, `${prefix}ALLOW_INSECURE`, (raw) => {
|
|
4747
|
+
allowInsecure = parseBool(`${prefix}ALLOW_INSECURE`, raw);
|
|
4748
|
+
});
|
|
4749
|
+
readVar(env, `${prefix}SASL_MECHANISM`, (raw) => {
|
|
4750
|
+
mechanism = parseEnum(`${prefix}SASL_MECHANISM`, raw, [
|
|
4751
|
+
"plain",
|
|
4752
|
+
"scram-sha-256",
|
|
4753
|
+
"scram-sha-512"
|
|
4754
|
+
]);
|
|
4755
|
+
});
|
|
4756
|
+
readVar(env, `${prefix}SASL_USERNAME`, (raw) => {
|
|
4757
|
+
username = raw.trim();
|
|
4758
|
+
});
|
|
4759
|
+
readVar(env, `${prefix}SASL_PASSWORD`, (raw) => {
|
|
4760
|
+
password = raw;
|
|
4761
|
+
});
|
|
4762
|
+
if (ssl === void 0 && allowInsecure === void 0 && mechanism === void 0 && username === void 0 && password === void 0) {
|
|
4763
|
+
return void 0;
|
|
4764
|
+
}
|
|
4765
|
+
const security = {};
|
|
4766
|
+
if (ssl !== void 0) security.ssl = ssl;
|
|
4767
|
+
if (allowInsecure !== void 0) security.allowInsecure = allowInsecure;
|
|
4768
|
+
if (mechanism !== void 0 || username !== void 0 || password !== void 0) {
|
|
4769
|
+
if (mechanism === void 0 || username === void 0 || password === void 0) {
|
|
4770
|
+
throw new Error(
|
|
4771
|
+
`Incomplete SASL configuration: ${prefix}SASL_MECHANISM, ${prefix}SASL_USERNAME, and ${prefix}SASL_PASSWORD must all be set together (oauthbearer must be configured in code).`
|
|
4772
|
+
);
|
|
4773
|
+
}
|
|
4774
|
+
const sasl = { mechanism, username, password };
|
|
4775
|
+
security.sasl = sasl;
|
|
4776
|
+
}
|
|
4777
|
+
return security;
|
|
4778
|
+
}
|
|
4779
|
+
function consumerOptionsFromEnv(env = process.env, prefix = "KAFKA_CONSUMER_") {
|
|
4780
|
+
const options = {};
|
|
4781
|
+
readVar(env, `${prefix}GROUP_ID`, (raw) => {
|
|
4782
|
+
options.groupId = raw.trim();
|
|
4783
|
+
});
|
|
4784
|
+
readVar(env, `${prefix}FROM_BEGINNING`, (raw) => {
|
|
4785
|
+
options.fromBeginning = parseBool(`${prefix}FROM_BEGINNING`, raw);
|
|
4786
|
+
});
|
|
4787
|
+
readVar(env, `${prefix}AUTO_COMMIT`, (raw) => {
|
|
4788
|
+
options.autoCommit = parseBool(`${prefix}AUTO_COMMIT`, raw);
|
|
4789
|
+
});
|
|
4790
|
+
readVar(env, `${prefix}DLQ`, (raw) => {
|
|
4791
|
+
options.dlq = parseBool(`${prefix}DLQ`, raw);
|
|
4792
|
+
});
|
|
4793
|
+
readVar(env, `${prefix}RETRY_MAX_RETRIES`, (raw) => {
|
|
4794
|
+
const retry = {
|
|
4795
|
+
maxRetries: parseNum(`${prefix}RETRY_MAX_RETRIES`, raw)
|
|
4796
|
+
};
|
|
4797
|
+
options.retry = retry;
|
|
4798
|
+
});
|
|
4799
|
+
readVar(env, `${prefix}RETRY_BACKOFF_MS`, (raw) => {
|
|
4800
|
+
if (options.retry) {
|
|
4801
|
+
options.retry.backoffMs = parseNum(`${prefix}RETRY_BACKOFF_MS`, raw);
|
|
4802
|
+
}
|
|
4803
|
+
});
|
|
4804
|
+
readVar(env, `${prefix}RETRY_MAX_BACKOFF_MS`, (raw) => {
|
|
4805
|
+
if (options.retry) {
|
|
4806
|
+
options.retry.maxBackoffMs = parseNum(`${prefix}RETRY_MAX_BACKOFF_MS`, raw);
|
|
4807
|
+
}
|
|
4808
|
+
});
|
|
4809
|
+
readVar(env, `${prefix}RETRY_TOPICS`, (raw) => {
|
|
4810
|
+
options.retryTopics = parseBool(`${prefix}RETRY_TOPICS`, raw);
|
|
4811
|
+
});
|
|
4812
|
+
readVar(env, `${prefix}RETRY_TOPIC_ASSIGNMENT_TIMEOUT_MS`, (raw) => {
|
|
4813
|
+
options.retryTopicAssignmentTimeoutMs = parseNum(
|
|
4814
|
+
`${prefix}RETRY_TOPIC_ASSIGNMENT_TIMEOUT_MS`,
|
|
4815
|
+
raw
|
|
4816
|
+
);
|
|
4817
|
+
});
|
|
4818
|
+
readVar(env, `${prefix}HANDLER_TIMEOUT_MS`, (raw) => {
|
|
4819
|
+
options.handlerTimeoutMs = parseNum(`${prefix}HANDLER_TIMEOUT_MS`, raw);
|
|
4820
|
+
});
|
|
4821
|
+
readVar(env, `${prefix}MESSAGE_TTL_MS`, (raw) => {
|
|
4822
|
+
options.messageTtlMs = parseNum(`${prefix}MESSAGE_TTL_MS`, raw);
|
|
4823
|
+
});
|
|
4824
|
+
readVar(env, `${prefix}DEDUPLICATION_STRATEGY`, (raw) => {
|
|
4825
|
+
const strategy = parseEnum(`${prefix}DEDUPLICATION_STRATEGY`, raw, [
|
|
4826
|
+
"drop",
|
|
4827
|
+
"dlq",
|
|
4828
|
+
"topic"
|
|
4829
|
+
]);
|
|
4830
|
+
const dedup = { strategy };
|
|
4831
|
+
options.deduplication = dedup;
|
|
4832
|
+
});
|
|
4833
|
+
readVar(env, `${prefix}DEDUPLICATION_TOPIC`, (raw) => {
|
|
4834
|
+
if (options.deduplication) {
|
|
4835
|
+
options.deduplication.duplicatesTopic = raw.trim();
|
|
4836
|
+
}
|
|
4837
|
+
});
|
|
4838
|
+
readVar(env, `${prefix}CIRCUIT_BREAKER_THRESHOLD`, (raw) => {
|
|
4839
|
+
const cb = {
|
|
4840
|
+
threshold: parseNum(`${prefix}CIRCUIT_BREAKER_THRESHOLD`, raw)
|
|
4841
|
+
};
|
|
4842
|
+
options.circuitBreaker = cb;
|
|
4843
|
+
});
|
|
4844
|
+
readVar(env, `${prefix}CIRCUIT_BREAKER_RECOVERY_MS`, (raw) => {
|
|
4845
|
+
if (options.circuitBreaker) {
|
|
4846
|
+
options.circuitBreaker.recoveryMs = parseNum(
|
|
4847
|
+
`${prefix}CIRCUIT_BREAKER_RECOVERY_MS`,
|
|
4848
|
+
raw
|
|
4849
|
+
);
|
|
4850
|
+
}
|
|
4851
|
+
});
|
|
4852
|
+
readVar(env, `${prefix}CIRCUIT_BREAKER_WINDOW_SIZE`, (raw) => {
|
|
4853
|
+
if (options.circuitBreaker) {
|
|
4854
|
+
options.circuitBreaker.windowSize = parseNum(
|
|
4855
|
+
`${prefix}CIRCUIT_BREAKER_WINDOW_SIZE`,
|
|
4856
|
+
raw
|
|
4857
|
+
);
|
|
4858
|
+
}
|
|
4859
|
+
});
|
|
4860
|
+
readVar(env, `${prefix}CIRCUIT_BREAKER_HALF_OPEN_SUCCESSES`, (raw) => {
|
|
4861
|
+
if (options.circuitBreaker) {
|
|
4862
|
+
options.circuitBreaker.halfOpenSuccesses = parseNum(
|
|
4863
|
+
`${prefix}CIRCUIT_BREAKER_HALF_OPEN_SUCCESSES`,
|
|
4864
|
+
raw
|
|
4865
|
+
);
|
|
4866
|
+
}
|
|
4867
|
+
});
|
|
4868
|
+
readVar(env, `${prefix}QUEUE_HIGH_WATER_MARK`, (raw) => {
|
|
4869
|
+
options.queueHighWaterMark = parseNum(`${prefix}QUEUE_HIGH_WATER_MARK`, raw);
|
|
4870
|
+
});
|
|
4871
|
+
readVar(env, `${prefix}PARTITION_ASSIGNER`, (raw) => {
|
|
4872
|
+
options.partitionAssigner = parseEnum(`${prefix}PARTITION_ASSIGNER`, raw, [
|
|
4873
|
+
"roundrobin",
|
|
4874
|
+
"range",
|
|
4875
|
+
"cooperative-sticky"
|
|
4876
|
+
]);
|
|
4877
|
+
});
|
|
4878
|
+
readVar(env, `${prefix}GROUP_INSTANCE_ID`, (raw) => {
|
|
4879
|
+
options.groupInstanceId = raw.trim();
|
|
4880
|
+
});
|
|
4881
|
+
readVar(env, `${prefix}SUBSCRIBE_RETRY_RETRIES`, (raw) => {
|
|
4882
|
+
const subscribeRetry = {
|
|
4883
|
+
retries: parseNum(`${prefix}SUBSCRIBE_RETRY_RETRIES`, raw)
|
|
4884
|
+
};
|
|
4885
|
+
options.subscribeRetry = subscribeRetry;
|
|
4886
|
+
});
|
|
4887
|
+
readVar(env, `${prefix}SUBSCRIBE_RETRY_DELAY_MS`, (raw) => {
|
|
4888
|
+
if (options.subscribeRetry) {
|
|
4889
|
+
options.subscribeRetry.backoffMs = parseNum(
|
|
4890
|
+
`${prefix}SUBSCRIBE_RETRY_DELAY_MS`,
|
|
4891
|
+
raw
|
|
4892
|
+
);
|
|
4893
|
+
}
|
|
4894
|
+
});
|
|
4895
|
+
return options;
|
|
4896
|
+
}
|
|
4897
|
+
var NESTED_CONSUMER_KEYS = [
|
|
4898
|
+
"retry",
|
|
4899
|
+
"deduplication",
|
|
4900
|
+
"circuitBreaker",
|
|
4901
|
+
"subscribeRetry"
|
|
4902
|
+
];
|
|
4903
|
+
function isPlainObject(value) {
|
|
4904
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4905
|
+
}
|
|
4906
|
+
function mergeConsumerOptions(...layers) {
|
|
4907
|
+
const result = {};
|
|
4908
|
+
for (const layer of layers) {
|
|
4909
|
+
if (!layer) continue;
|
|
4910
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
4911
|
+
if (value === void 0) continue;
|
|
4912
|
+
if (NESTED_CONSUMER_KEYS.includes(key) && isPlainObject(value) && isPlainObject(result[key])) {
|
|
4913
|
+
result[key] = {
|
|
4914
|
+
...result[key],
|
|
4915
|
+
...value
|
|
4916
|
+
};
|
|
4917
|
+
} else {
|
|
4918
|
+
result[key] = value;
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
return result;
|
|
4923
|
+
}
|
|
3601
4924
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3602
4925
|
0 && (module.exports = {
|
|
4926
|
+
ConfluentTransport,
|
|
3603
4927
|
HEADER_CORRELATION_ID,
|
|
4928
|
+
HEADER_DELAYED_TARGET,
|
|
4929
|
+
HEADER_DELAYED_UNTIL,
|
|
3604
4930
|
HEADER_EVENT_ID,
|
|
3605
4931
|
HEADER_LAMPORT_CLOCK,
|
|
3606
4932
|
HEADER_SCHEMA_VERSION,
|
|
3607
4933
|
HEADER_TIMESTAMP,
|
|
3608
4934
|
HEADER_TRACEPARENT,
|
|
4935
|
+
InMemoryDedupStore,
|
|
4936
|
+
InMemoryOutboxStore,
|
|
4937
|
+
JsonSerde,
|
|
3609
4938
|
KafkaClient,
|
|
3610
4939
|
KafkaProcessingError,
|
|
3611
4940
|
KafkaRetryExhaustedError,
|
|
3612
4941
|
KafkaValidationError,
|
|
4942
|
+
SchemaRegistryClient,
|
|
4943
|
+
awsMskIamProvider,
|
|
3613
4944
|
buildEnvelopeHeaders,
|
|
4945
|
+
consumerOptionsFromEnv,
|
|
3614
4946
|
decodeHeaders,
|
|
4947
|
+
describeRequiredAcls,
|
|
3615
4948
|
extractEnvelope,
|
|
4949
|
+
gcpAccessTokenProvider,
|
|
3616
4950
|
getEnvelopeContext,
|
|
4951
|
+
kafkaClientConfigFromEnv,
|
|
4952
|
+
mergeConsumerOptions,
|
|
4953
|
+
registrySchema,
|
|
4954
|
+
resolveSecurityOptions,
|
|
3617
4955
|
runWithEnvelopeContext,
|
|
3618
|
-
|
|
4956
|
+
startOutboxRelay,
|
|
4957
|
+
toError,
|
|
4958
|
+
toKafkaAclCommands,
|
|
4959
|
+
toMskIamPolicy,
|
|
4960
|
+
topic,
|
|
4961
|
+
versionedSchema
|
|
3619
4962
|
});
|
|
3620
4963
|
//# sourceMappingURL=core.js.map
|