@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/index.js
CHANGED
|
@@ -29,13 +29,19 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
29
29
|
// src/index.ts
|
|
30
30
|
var index_exports = {};
|
|
31
31
|
__export(index_exports, {
|
|
32
|
+
ConfluentTransport: () => ConfluentTransport,
|
|
32
33
|
HEADER_CORRELATION_ID: () => HEADER_CORRELATION_ID,
|
|
34
|
+
HEADER_DELAYED_TARGET: () => HEADER_DELAYED_TARGET,
|
|
35
|
+
HEADER_DELAYED_UNTIL: () => HEADER_DELAYED_UNTIL,
|
|
33
36
|
HEADER_EVENT_ID: () => HEADER_EVENT_ID,
|
|
34
37
|
HEADER_LAMPORT_CLOCK: () => HEADER_LAMPORT_CLOCK,
|
|
35
38
|
HEADER_SCHEMA_VERSION: () => HEADER_SCHEMA_VERSION,
|
|
36
39
|
HEADER_TIMESTAMP: () => HEADER_TIMESTAMP,
|
|
37
40
|
HEADER_TRACEPARENT: () => HEADER_TRACEPARENT,
|
|
41
|
+
InMemoryDedupStore: () => InMemoryDedupStore,
|
|
42
|
+
InMemoryOutboxStore: () => InMemoryOutboxStore,
|
|
38
43
|
InjectKafkaClient: () => InjectKafkaClient,
|
|
44
|
+
JsonSerde: () => JsonSerde,
|
|
39
45
|
KAFKA_CLIENT: () => KAFKA_CLIENT,
|
|
40
46
|
KAFKA_SUBSCRIBER_METADATA: () => KAFKA_SUBSCRIBER_METADATA,
|
|
41
47
|
KafkaClient: () => KafkaClient,
|
|
@@ -45,24 +51,39 @@ __export(index_exports, {
|
|
|
45
51
|
KafkaProcessingError: () => KafkaProcessingError,
|
|
46
52
|
KafkaRetryExhaustedError: () => KafkaRetryExhaustedError,
|
|
47
53
|
KafkaValidationError: () => KafkaValidationError,
|
|
54
|
+
SchemaRegistryClient: () => SchemaRegistryClient,
|
|
48
55
|
SubscribeTo: () => SubscribeTo,
|
|
56
|
+
awsMskIamProvider: () => awsMskIamProvider,
|
|
49
57
|
buildEnvelopeHeaders: () => buildEnvelopeHeaders,
|
|
58
|
+
consumerOptionsFromEnv: () => consumerOptionsFromEnv,
|
|
50
59
|
decodeHeaders: () => decodeHeaders,
|
|
60
|
+
describeRequiredAcls: () => describeRequiredAcls,
|
|
51
61
|
extractEnvelope: () => extractEnvelope,
|
|
62
|
+
gcpAccessTokenProvider: () => gcpAccessTokenProvider,
|
|
52
63
|
getEnvelopeContext: () => getEnvelopeContext,
|
|
53
64
|
getKafkaClientToken: () => getKafkaClientToken,
|
|
65
|
+
kafkaClientConfigFromEnv: () => kafkaClientConfigFromEnv,
|
|
66
|
+
mergeConsumerOptions: () => mergeConsumerOptions,
|
|
67
|
+
registrySchema: () => registrySchema,
|
|
68
|
+
resolveSecurityOptions: () => resolveSecurityOptions,
|
|
54
69
|
runWithEnvelopeContext: () => runWithEnvelopeContext,
|
|
55
|
-
|
|
70
|
+
startOutboxRelay: () => startOutboxRelay,
|
|
71
|
+
toError: () => toError,
|
|
72
|
+
toKafkaAclCommands: () => toKafkaAclCommands,
|
|
73
|
+
toMskIamPolicy: () => toMskIamPolicy,
|
|
74
|
+
topic: () => topic,
|
|
75
|
+
versionedSchema: () => versionedSchema
|
|
56
76
|
});
|
|
57
77
|
module.exports = __toCommonJS(index_exports);
|
|
58
78
|
|
|
59
|
-
// src/client/
|
|
79
|
+
// src/client/transport/confluent.transport.ts
|
|
60
80
|
var import_kafka_javascript = require("@confluentinc/kafka-javascript");
|
|
61
81
|
var { Kafka: KafkaClass, logLevel: KafkaLogLevel, PartitionAssigners } = import_kafka_javascript.KafkaJS;
|
|
62
82
|
var ConfluentTransaction = class {
|
|
63
83
|
constructor(tx) {
|
|
64
84
|
this.tx = tx;
|
|
65
85
|
}
|
|
86
|
+
tx;
|
|
66
87
|
async send(record) {
|
|
67
88
|
await this.tx.send(record);
|
|
68
89
|
}
|
|
@@ -84,10 +105,17 @@ var ConfluentProducer = class {
|
|
|
84
105
|
constructor(producer) {
|
|
85
106
|
this.producer = producer;
|
|
86
107
|
}
|
|
108
|
+
producer;
|
|
109
|
+
connectPromise;
|
|
87
110
|
async connect() {
|
|
88
|
-
|
|
111
|
+
this.connectPromise ??= this.producer.connect().catch((err) => {
|
|
112
|
+
this.connectPromise = void 0;
|
|
113
|
+
throw err;
|
|
114
|
+
});
|
|
115
|
+
return this.connectPromise;
|
|
89
116
|
}
|
|
90
117
|
async disconnect() {
|
|
118
|
+
this.connectPromise = void 0;
|
|
91
119
|
await this.producer.disconnect();
|
|
92
120
|
}
|
|
93
121
|
async send(record) {
|
|
@@ -102,6 +130,7 @@ var ConfluentConsumer = class {
|
|
|
102
130
|
constructor(consumer) {
|
|
103
131
|
this.consumer = consumer;
|
|
104
132
|
}
|
|
133
|
+
consumer;
|
|
105
134
|
/** Returns the underlying KafkaJS.Consumer — used by ConfluentTransaction.sendOffsets. */
|
|
106
135
|
getNative() {
|
|
107
136
|
return this.consumer;
|
|
@@ -141,6 +170,7 @@ var ConfluentAdmin = class {
|
|
|
141
170
|
constructor(admin) {
|
|
142
171
|
this.admin = admin;
|
|
143
172
|
}
|
|
173
|
+
admin;
|
|
144
174
|
async connect() {
|
|
145
175
|
await this.admin.connect();
|
|
146
176
|
}
|
|
@@ -154,7 +184,7 @@ var ConfluentAdmin = class {
|
|
|
154
184
|
return this.admin.fetchTopicOffsets(topic2);
|
|
155
185
|
}
|
|
156
186
|
async fetchTopicOffsetsByTimestamp(topic2, timestamp) {
|
|
157
|
-
return this.admin.
|
|
187
|
+
return this.admin.fetchTopicOffsetsByTimestamp(topic2, timestamp);
|
|
158
188
|
}
|
|
159
189
|
async fetchOffsets(options) {
|
|
160
190
|
return this.admin.fetchOffsets(options);
|
|
@@ -180,10 +210,29 @@ var ConfluentAdmin = class {
|
|
|
180
210
|
};
|
|
181
211
|
var ConfluentTransport = class {
|
|
182
212
|
kafka;
|
|
183
|
-
constructor(clientId, brokers) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
213
|
+
constructor(clientId, brokers, security) {
|
|
214
|
+
const kafkaJS = { clientId, brokers, logLevel: KafkaLogLevel.ERROR };
|
|
215
|
+
if (security?.ssl !== void 0) kafkaJS.ssl = security.ssl;
|
|
216
|
+
if (security?.sasl) {
|
|
217
|
+
if (security.sasl.mechanism === "oauthbearer") {
|
|
218
|
+
const provider = security.sasl.oauthBearerProvider;
|
|
219
|
+
kafkaJS.sasl = {
|
|
220
|
+
mechanism: "oauthbearer",
|
|
221
|
+
oauthBearerProvider: async () => {
|
|
222
|
+
const token = await provider();
|
|
223
|
+
return {
|
|
224
|
+
value: token.value,
|
|
225
|
+
principal: token.principal ?? "kafka-client",
|
|
226
|
+
lifetime: token.lifetimeMs ?? Date.now() + 15 * 6e4,
|
|
227
|
+
...token.extensions && { extensions: token.extensions }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
} else {
|
|
232
|
+
kafkaJS.sasl = security.sasl;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
this.kafka = new KafkaClass({ kafkaJS });
|
|
187
236
|
}
|
|
188
237
|
producer(options) {
|
|
189
238
|
const native = this.kafka.producer({
|
|
@@ -210,6 +259,9 @@ var ConfluentTransport = class {
|
|
|
210
259
|
partitionAssigners: [assigner]
|
|
211
260
|
}
|
|
212
261
|
};
|
|
262
|
+
if (options.groupInstanceId) {
|
|
263
|
+
config["group.instance.id"] = options.groupInstanceId;
|
|
264
|
+
}
|
|
213
265
|
if (options.onRebalance) {
|
|
214
266
|
const cb = options.onRebalance;
|
|
215
267
|
config.rebalance_cb = (err, assignment) => {
|
|
@@ -227,6 +279,37 @@ var ConfluentTransport = class {
|
|
|
227
279
|
}
|
|
228
280
|
};
|
|
229
281
|
|
|
282
|
+
// src/client/message/serde.ts
|
|
283
|
+
var JsonSerde = class {
|
|
284
|
+
/** JSON-stringify the validated payload. Returns a UTF-8 string. */
|
|
285
|
+
serialize(value) {
|
|
286
|
+
return JSON.stringify(value);
|
|
287
|
+
}
|
|
288
|
+
/** JSON-parse UTF-8 wire bytes into an object. */
|
|
289
|
+
deserialize(data) {
|
|
290
|
+
return JSON.parse(data.toString("utf8"));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// src/client/kafka.client/infra/dedup.store.ts
|
|
295
|
+
var InMemoryDedupStore = class {
|
|
296
|
+
constructor(states) {
|
|
297
|
+
this.states = states;
|
|
298
|
+
}
|
|
299
|
+
states;
|
|
300
|
+
getLastClock(groupId, topicPartition) {
|
|
301
|
+
return this.states.get(groupId)?.get(topicPartition);
|
|
302
|
+
}
|
|
303
|
+
setLastClock(groupId, topicPartition, clock) {
|
|
304
|
+
let group = this.states.get(groupId);
|
|
305
|
+
if (!group) {
|
|
306
|
+
group = /* @__PURE__ */ new Map();
|
|
307
|
+
this.states.set(groupId, group);
|
|
308
|
+
}
|
|
309
|
+
group.set(topicPartition, clock);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
230
313
|
// src/client/message/envelope.ts
|
|
231
314
|
var import_node_async_hooks = require("async_hooks");
|
|
232
315
|
var import_node_crypto = require("crypto");
|
|
@@ -236,6 +319,8 @@ var HEADER_TIMESTAMP = "x-timestamp";
|
|
|
236
319
|
var HEADER_SCHEMA_VERSION = "x-schema-version";
|
|
237
320
|
var HEADER_TRACEPARENT = "traceparent";
|
|
238
321
|
var HEADER_LAMPORT_CLOCK = "x-lamport-clock";
|
|
322
|
+
var HEADER_DELAYED_UNTIL = "x-delayed-until";
|
|
323
|
+
var HEADER_DELAYED_TARGET = "x-delayed-target";
|
|
239
324
|
var envelopeStorage = new import_node_async_hooks.AsyncLocalStorage();
|
|
240
325
|
function getEnvelopeContext() {
|
|
241
326
|
return envelopeStorage.getStore();
|
|
@@ -290,6 +375,9 @@ function extractEnvelope(payload, headers, topic2, partition, offset) {
|
|
|
290
375
|
}
|
|
291
376
|
|
|
292
377
|
// src/client/errors.ts
|
|
378
|
+
function toError(error) {
|
|
379
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
380
|
+
}
|
|
293
381
|
var KafkaProcessingError = class extends Error {
|
|
294
382
|
constructor(message, topic2, originalMessage, options) {
|
|
295
383
|
super(message, options);
|
|
@@ -298,6 +386,8 @@ var KafkaProcessingError = class extends Error {
|
|
|
298
386
|
this.name = "KafkaProcessingError";
|
|
299
387
|
if (options?.cause) this.cause = options.cause;
|
|
300
388
|
}
|
|
389
|
+
topic;
|
|
390
|
+
originalMessage;
|
|
301
391
|
};
|
|
302
392
|
var KafkaValidationError = class extends Error {
|
|
303
393
|
constructor(topic2, originalMessage, options) {
|
|
@@ -307,6 +397,8 @@ var KafkaValidationError = class extends Error {
|
|
|
307
397
|
this.name = "KafkaValidationError";
|
|
308
398
|
if (options?.cause) this.cause = options.cause;
|
|
309
399
|
}
|
|
400
|
+
topic;
|
|
401
|
+
originalMessage;
|
|
310
402
|
};
|
|
311
403
|
var KafkaRetryExhaustedError = class extends KafkaProcessingError {
|
|
312
404
|
constructor(topic2, originalMessage, attempts, options) {
|
|
@@ -319,9 +411,13 @@ var KafkaRetryExhaustedError = class extends KafkaProcessingError {
|
|
|
319
411
|
this.attempts = attempts;
|
|
320
412
|
this.name = "KafkaRetryExhaustedError";
|
|
321
413
|
}
|
|
414
|
+
attempts;
|
|
322
415
|
};
|
|
323
416
|
|
|
324
417
|
// src/client/kafka.client/producer/ops.ts
|
|
418
|
+
function resolveSerde(topicOrDesc, clientSerde) {
|
|
419
|
+
return topicOrDesc?.__serde ?? clientSerde;
|
|
420
|
+
}
|
|
325
421
|
function resolveTopicName(topicOrDescriptor) {
|
|
326
422
|
if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
|
|
327
423
|
if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
|
|
@@ -368,6 +464,7 @@ async function validateMessage(topicOrDesc, message, deps, ctx) {
|
|
|
368
464
|
}
|
|
369
465
|
async function buildSendPayload(topicOrDesc, messages, deps, compression) {
|
|
370
466
|
const topic2 = resolveTopicName(topicOrDesc);
|
|
467
|
+
const serde = resolveSerde(topicOrDesc, deps.serde);
|
|
371
468
|
const builtMessages = await Promise.all(
|
|
372
469
|
messages.map(async (m) => {
|
|
373
470
|
const envelopeHeaders = buildEnvelopeHeaders({
|
|
@@ -387,11 +484,16 @@ async function buildSendPayload(topicOrDesc, messages, deps, compression) {
|
|
|
387
484
|
headers: envelopeHeaders,
|
|
388
485
|
version: m.schemaVersion ?? 1
|
|
389
486
|
};
|
|
487
|
+
const validated = await validateMessage(topicOrDesc, m.value, deps, sendCtx);
|
|
390
488
|
return {
|
|
391
|
-
value:
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
489
|
+
value: await serde.serialize(validated, {
|
|
490
|
+
topic: topic2,
|
|
491
|
+
headers: envelopeHeaders,
|
|
492
|
+
isKey: false
|
|
493
|
+
}),
|
|
494
|
+
// Explicit key wins; otherwise fall back to the descriptor's .key()
|
|
495
|
+
// extractor (runs on the original, pre-validation payload).
|
|
496
|
+
key: m.key ?? topicOrDesc?.__key?.(m.value) ?? null,
|
|
395
497
|
headers: envelopeHeaders
|
|
396
498
|
};
|
|
397
499
|
})
|
|
@@ -400,7 +502,7 @@ async function buildSendPayload(topicOrDesc, messages, deps, compression) {
|
|
|
400
502
|
}
|
|
401
503
|
|
|
402
504
|
// src/client/kafka.client/consumer/ops.ts
|
|
403
|
-
function getOrCreateConsumer(groupId, fromBeginning, autoCommit, deps, partitionAssigner, onFirstAssignment) {
|
|
505
|
+
function getOrCreateConsumer(groupId, fromBeginning, autoCommit, deps, partitionAssigner, onFirstAssignment, groupInstanceId) {
|
|
404
506
|
const { consumers, consumerCreationOptions, transport, onRebalance, logger } = deps;
|
|
405
507
|
if (consumers.has(groupId)) {
|
|
406
508
|
const prev = consumerCreationOptions.get(groupId);
|
|
@@ -433,6 +535,7 @@ function getOrCreateConsumer(groupId, fromBeginning, autoCommit, deps, partition
|
|
|
433
535
|
fromBeginning,
|
|
434
536
|
autoCommit,
|
|
435
537
|
partitionAssigner: partitionAssigner ?? "cooperative-sticky",
|
|
538
|
+
groupInstanceId,
|
|
436
539
|
onRebalance: (type, assignments) => {
|
|
437
540
|
if (type === "assign") fireOnAssignment();
|
|
438
541
|
else if (type === "revoke") scheduleSettle();
|
|
@@ -472,12 +575,22 @@ function buildSchemaMap(topics, schemaRegistry, optionSchemas, logger) {
|
|
|
472
575
|
}
|
|
473
576
|
return schemaMap;
|
|
474
577
|
}
|
|
578
|
+
function buildSerdeMap(topics) {
|
|
579
|
+
let serdeMap;
|
|
580
|
+
for (const t of topics) {
|
|
581
|
+
if (t?.__serde) {
|
|
582
|
+
(serdeMap ??= /* @__PURE__ */ new Map()).set(resolveTopicName(t), t.__serde);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return serdeMap;
|
|
586
|
+
}
|
|
475
587
|
|
|
476
588
|
// src/client/kafka.client/admin/ops.ts
|
|
477
589
|
var AdminOps = class {
|
|
478
590
|
constructor(deps) {
|
|
479
591
|
this.deps = deps;
|
|
480
592
|
}
|
|
593
|
+
deps;
|
|
481
594
|
isConnected = false;
|
|
482
595
|
/** Underlying admin client — used by index.ts for topic validation. */
|
|
483
596
|
get admin() {
|
|
@@ -584,7 +697,10 @@ var AdminOps = class {
|
|
|
584
697
|
const found = results.find(
|
|
585
698
|
(r) => r.partition === partition
|
|
586
699
|
);
|
|
587
|
-
return { partition, offset: found
|
|
700
|
+
if (found) return { partition, offset: found.offset };
|
|
701
|
+
const topicOffsets = await this.deps.admin.fetchTopicOffsets(topic2);
|
|
702
|
+
const po = topicOffsets.find((o) => o.partition === partition);
|
|
703
|
+
return { partition, offset: po?.high ?? "0" };
|
|
588
704
|
})
|
|
589
705
|
);
|
|
590
706
|
await this.deps.admin.setOffsets({ groupId: gid, topic: topic2, partitions: offsets });
|
|
@@ -665,7 +781,8 @@ var AdminOps = class {
|
|
|
665
781
|
name: t.name,
|
|
666
782
|
partitions: t.partitions.map((p) => ({
|
|
667
783
|
partition: p.partitionId ?? p.partition ?? 0,
|
|
668
|
-
|
|
784
|
+
// -1 is Kafka's own "no leader" sentinel; 0 is a valid broker id
|
|
785
|
+
leader: p.leader ?? -1,
|
|
669
786
|
replicas: (p.replicas ?? []).map(
|
|
670
787
|
(r) => typeof r === "number" ? r : r.nodeId
|
|
671
788
|
),
|
|
@@ -749,23 +866,9 @@ var AdminOps = class {
|
|
|
749
866
|
};
|
|
750
867
|
|
|
751
868
|
// src/client/kafka.client/consumer/pipeline.ts
|
|
752
|
-
function toError(error) {
|
|
753
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
754
|
-
}
|
|
755
869
|
function sleep(ms) {
|
|
756
870
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
757
871
|
}
|
|
758
|
-
function parseJsonMessage(raw, topic2, logger) {
|
|
759
|
-
try {
|
|
760
|
-
return JSON.parse(raw);
|
|
761
|
-
} catch (error) {
|
|
762
|
-
logger.error(
|
|
763
|
-
`Failed to parse message from topic ${topic2}:`,
|
|
764
|
-
toError(error).stack
|
|
765
|
-
);
|
|
766
|
-
return null;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
872
|
async function validateWithSchema(message, raw, topic2, schemaMap, interceptors, dlq, deps) {
|
|
770
873
|
const schema = schemaMap.get(topic2);
|
|
771
874
|
if (!schema) return message;
|
|
@@ -1035,6 +1138,7 @@ async function executeWithRetry(fn, ctx, deps) {
|
|
|
1035
1138
|
for (const env of envelopes) deps.onMessage?.(env);
|
|
1036
1139
|
return;
|
|
1037
1140
|
}
|
|
1141
|
+
deps.onFailure?.(envelopes[0]);
|
|
1038
1142
|
const isLastAttempt = attempt === maxAttempts;
|
|
1039
1143
|
const reportedError = isLastAttempt && maxAttempts > 1 ? new KafkaRetryExhaustedError(
|
|
1040
1144
|
topic2,
|
|
@@ -1119,8 +1223,13 @@ async function subscribeWithRetry(consumer, topics, logger, retryOpts) {
|
|
|
1119
1223
|
}
|
|
1120
1224
|
}
|
|
1121
1225
|
|
|
1122
|
-
// src/client/kafka.client/consumer/dlq-replay.ts
|
|
1226
|
+
// src/client/kafka.client/consumer/features/dlq-replay.ts
|
|
1123
1227
|
async function replayDlqTopic(topic2, deps, options = {}) {
|
|
1228
|
+
if (topic2.endsWith(".dlq")) {
|
|
1229
|
+
throw new Error(
|
|
1230
|
+
`replayDlq: pass the ORIGINAL topic name \u2014 "${topic2}" already ends in ".dlq" (the ".dlq" suffix is appended internally, so this would read "${topic2}.dlq")`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1124
1233
|
const dlqTopic = `${topic2}.dlq`;
|
|
1125
1234
|
const partitionOffsets = await deps.fetchTopicOffsets(dlqTopic);
|
|
1126
1235
|
const activePartitions = partitionOffsets.filter(
|
|
@@ -1152,15 +1261,15 @@ async function replayDlqTopic(topic2, deps, options = {}) {
|
|
|
1152
1261
|
const originalHeaders = Object.fromEntries(
|
|
1153
1262
|
Object.entries(headers).filter(([k]) => !deps.dlqHeaderKeys.has(k))
|
|
1154
1263
|
);
|
|
1155
|
-
const
|
|
1156
|
-
const shouldProcess = !options.filter || options.filter(headers,
|
|
1264
|
+
const bytes = message.value;
|
|
1265
|
+
const shouldProcess = !options.filter || options.filter(headers, bytes.toString("utf8"));
|
|
1157
1266
|
if (!targetTopic || !shouldProcess) {
|
|
1158
1267
|
skipped++;
|
|
1159
1268
|
} else if (options.dryRun) {
|
|
1160
1269
|
deps.logger.log(`[DLQ replay dry-run] Would replay to "${targetTopic}"`);
|
|
1161
1270
|
replayed++;
|
|
1162
1271
|
} else {
|
|
1163
|
-
await deps.send(targetTopic, [{ value, headers: originalHeaders }]);
|
|
1272
|
+
await deps.send(targetTopic, [{ value: bytes, headers: originalHeaders }]);
|
|
1164
1273
|
replayed++;
|
|
1165
1274
|
}
|
|
1166
1275
|
const allDone = Array.from(highWatermarks.entries()).every(
|
|
@@ -1186,6 +1295,7 @@ var MetricsManager = class {
|
|
|
1186
1295
|
constructor(deps) {
|
|
1187
1296
|
this.deps = deps;
|
|
1188
1297
|
}
|
|
1298
|
+
deps;
|
|
1189
1299
|
topicMetrics = /* @__PURE__ */ new Map();
|
|
1190
1300
|
metricsFor(topic2) {
|
|
1191
1301
|
let m = this.topicMetrics.get(topic2);
|
|
@@ -1211,16 +1321,25 @@ var MetricsManager = class {
|
|
|
1211
1321
|
for (const inst of this.deps.instrumentation) inst.onRetry?.(envelope, attempt, maxRetries);
|
|
1212
1322
|
}
|
|
1213
1323
|
/**
|
|
1214
|
-
* Increment the DLQ counter for the envelope's topic
|
|
1215
|
-
*
|
|
1324
|
+
* Increment the DLQ counter for the envelope's topic and fire all `onDlq` instrumentation hooks.
|
|
1325
|
+
* Circuit breaker failures are recorded separately via `notifyFailure` at the
|
|
1326
|
+
* handler-error boundary — dead-lettering itself is not a circuit event.
|
|
1216
1327
|
* @param envelope The message envelope being sent to the DLQ.
|
|
1217
1328
|
* @param reason The reason the message is being dead-lettered.
|
|
1218
|
-
* @param gid Consumer group ID — used to drive circuit breaker state.
|
|
1219
1329
|
*/
|
|
1220
|
-
notifyDlq(envelope, reason
|
|
1330
|
+
notifyDlq(envelope, reason) {
|
|
1221
1331
|
this.metricsFor(envelope.topic).dlqCount++;
|
|
1222
1332
|
for (const inst of this.deps.instrumentation) inst.onDlq?.(envelope, reason);
|
|
1223
|
-
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Notify the circuit breaker of a handler failure. Fired on every failed
|
|
1336
|
+
* handler attempt (in-process retries and retry-topic levels included),
|
|
1337
|
+
* independent of whether the message is ultimately dead-lettered.
|
|
1338
|
+
* @param envelope The message envelope whose handler failed.
|
|
1339
|
+
* @param gid Consumer group ID — used to drive circuit breaker state.
|
|
1340
|
+
*/
|
|
1341
|
+
notifyFailure(envelope, gid) {
|
|
1342
|
+
this.deps.onCircuitFailure(envelope, gid);
|
|
1224
1343
|
}
|
|
1225
1344
|
/**
|
|
1226
1345
|
* Increment the deduplication counter for the envelope's topic and fire all `onDuplicate` hooks.
|
|
@@ -1279,6 +1398,7 @@ var InFlightTracker = class {
|
|
|
1279
1398
|
constructor(warn) {
|
|
1280
1399
|
this.warn = warn;
|
|
1281
1400
|
}
|
|
1401
|
+
warn;
|
|
1282
1402
|
inFlightTotal = 0;
|
|
1283
1403
|
drainResolvers = [];
|
|
1284
1404
|
/**
|
|
@@ -1289,10 +1409,16 @@ var InFlightTracker = class {
|
|
|
1289
1409
|
*/
|
|
1290
1410
|
track(fn) {
|
|
1291
1411
|
this.inFlightTotal++;
|
|
1292
|
-
|
|
1412
|
+
const done = () => {
|
|
1293
1413
|
this.inFlightTotal--;
|
|
1294
1414
|
if (this.inFlightTotal === 0) this.drainResolvers.splice(0).forEach((r) => r());
|
|
1295
|
-
}
|
|
1415
|
+
};
|
|
1416
|
+
try {
|
|
1417
|
+
return fn().finally(done);
|
|
1418
|
+
} catch (err) {
|
|
1419
|
+
done();
|
|
1420
|
+
throw err;
|
|
1421
|
+
}
|
|
1296
1422
|
}
|
|
1297
1423
|
/**
|
|
1298
1424
|
* Resolve when all tracked handlers have completed, or after `timeoutMs` elapses.
|
|
@@ -1326,6 +1452,7 @@ var CircuitBreakerManager = class {
|
|
|
1326
1452
|
constructor(deps) {
|
|
1327
1453
|
this.deps = deps;
|
|
1328
1454
|
}
|
|
1455
|
+
deps;
|
|
1329
1456
|
states = /* @__PURE__ */ new Map();
|
|
1330
1457
|
configs = /* @__PURE__ */ new Map();
|
|
1331
1458
|
/**
|
|
@@ -1470,6 +1597,9 @@ var AsyncQueue = class {
|
|
|
1470
1597
|
this.onFull = onFull;
|
|
1471
1598
|
this.onDrained = onDrained;
|
|
1472
1599
|
}
|
|
1600
|
+
highWaterMark;
|
|
1601
|
+
onFull;
|
|
1602
|
+
onDrained;
|
|
1473
1603
|
items = [];
|
|
1474
1604
|
waiting = [];
|
|
1475
1605
|
closed = false;
|
|
@@ -1481,6 +1611,7 @@ var AsyncQueue = class {
|
|
|
1481
1611
|
* @param item The value to enqueue.
|
|
1482
1612
|
*/
|
|
1483
1613
|
push(item) {
|
|
1614
|
+
if (this.closed) return;
|
|
1484
1615
|
if (this.waiting.length > 0) {
|
|
1485
1616
|
this.waiting.shift().resolve({ value: item, done: false });
|
|
1486
1617
|
} else {
|
|
@@ -1531,6 +1662,101 @@ var AsyncQueue = class {
|
|
|
1531
1662
|
}
|
|
1532
1663
|
};
|
|
1533
1664
|
|
|
1665
|
+
// src/client/kafka.client/validate-options.ts
|
|
1666
|
+
function validateClientOptions(clientId, groupId, brokers, options) {
|
|
1667
|
+
const problems = [];
|
|
1668
|
+
if (typeof clientId !== "string" || clientId.trim() === "") {
|
|
1669
|
+
problems.push("clientId must be a non-empty string");
|
|
1670
|
+
}
|
|
1671
|
+
if (typeof groupId !== "string" || groupId.trim() === "") {
|
|
1672
|
+
problems.push("groupId must be a non-empty string");
|
|
1673
|
+
}
|
|
1674
|
+
if (!Array.isArray(brokers) || brokers.length === 0 && !options?.transport) {
|
|
1675
|
+
problems.push("brokers must be a non-empty array of broker addresses");
|
|
1676
|
+
} else if (brokers.some((b) => typeof b !== "string" || b.trim() === "")) {
|
|
1677
|
+
problems.push("brokers must not contain empty entries");
|
|
1678
|
+
}
|
|
1679
|
+
if (options) {
|
|
1680
|
+
const {
|
|
1681
|
+
numPartitions,
|
|
1682
|
+
transactionalId,
|
|
1683
|
+
clockRecovery,
|
|
1684
|
+
lagThrottle
|
|
1685
|
+
} = options;
|
|
1686
|
+
if (numPartitions !== void 0 && (!Number.isInteger(numPartitions) || numPartitions < 1)) {
|
|
1687
|
+
problems.push(
|
|
1688
|
+
`numPartitions must be a positive integer (got ${numPartitions})`
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
if (transactionalId !== void 0 && transactionalId.trim() === "") {
|
|
1692
|
+
problems.push("transactionalId must be a non-empty string when set");
|
|
1693
|
+
}
|
|
1694
|
+
if (clockRecovery) {
|
|
1695
|
+
if (!Array.isArray(clockRecovery.topics)) {
|
|
1696
|
+
problems.push("clockRecovery.topics must be an array of topic names");
|
|
1697
|
+
}
|
|
1698
|
+
if (clockRecovery.timeoutMs !== void 0 && !(clockRecovery.timeoutMs > 0)) {
|
|
1699
|
+
problems.push(
|
|
1700
|
+
`clockRecovery.timeoutMs must be > 0 (got ${clockRecovery.timeoutMs})`
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (lagThrottle) {
|
|
1705
|
+
if (!(lagThrottle.maxLag >= 0)) {
|
|
1706
|
+
problems.push(`lagThrottle.maxLag must be >= 0 (got ${lagThrottle.maxLag})`);
|
|
1707
|
+
}
|
|
1708
|
+
if (lagThrottle.pollIntervalMs !== void 0 && !(lagThrottle.pollIntervalMs > 0)) {
|
|
1709
|
+
problems.push(
|
|
1710
|
+
`lagThrottle.pollIntervalMs must be > 0 (got ${lagThrottle.pollIntervalMs})`
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
if (lagThrottle.maxWaitMs !== void 0 && !(lagThrottle.maxWaitMs >= 0)) {
|
|
1714
|
+
problems.push(
|
|
1715
|
+
`lagThrottle.maxWaitMs must be >= 0 (got ${lagThrottle.maxWaitMs})`
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
if (problems.length > 0) {
|
|
1721
|
+
throw new Error(
|
|
1722
|
+
`KafkaClient: invalid configuration:
|
|
1723
|
+
- ${problems.join("\n- ")}`
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/client/security/resolve-security.ts
|
|
1729
|
+
var LOCAL_HOST_PATTERNS = [
|
|
1730
|
+
/^localhost(:\d+)?$/i,
|
|
1731
|
+
/^127\.\d+\.\d+\.\d+(:\d+)?$/,
|
|
1732
|
+
/^\[?::1\]?(:\d+)?$/,
|
|
1733
|
+
/^0\.0\.0\.0(:\d+)?$/,
|
|
1734
|
+
/^host\.docker\.internal(:\d+)?$/i
|
|
1735
|
+
];
|
|
1736
|
+
function isLocalBroker(broker) {
|
|
1737
|
+
return LOCAL_HOST_PATTERNS.some((re) => re.test(broker.trim()));
|
|
1738
|
+
}
|
|
1739
|
+
function resolveSecurityOptions(security, brokers, logger) {
|
|
1740
|
+
const hasRemoteBroker = brokers.some((b) => !isLocalBroker(b));
|
|
1741
|
+
if (!security?.sasl && security?.ssl !== true) {
|
|
1742
|
+
if (hasRemoteBroker && !security?.allowInsecure) {
|
|
1743
|
+
logger.warn(
|
|
1744
|
+
"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."
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
return security;
|
|
1748
|
+
}
|
|
1749
|
+
if (security.sasl && security.ssl === void 0) {
|
|
1750
|
+
return { ...security, ssl: true };
|
|
1751
|
+
}
|
|
1752
|
+
if (security.sasl && security.ssl === false) {
|
|
1753
|
+
logger.warn(
|
|
1754
|
+
"SASL credentials are configured with `ssl: false` \u2014 credentials will be sent over plaintext. This is only safe on fully trusted networks."
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
return security;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1534
1760
|
// src/client/kafka.client/producer/lifecycle.ts
|
|
1535
1761
|
var _activeTransactionalIds = /* @__PURE__ */ new Set();
|
|
1536
1762
|
async function ensureTopic(ctx, topic2) {
|
|
@@ -1664,6 +1890,7 @@ async function recoverLamportClockImpl(ctx, topics) {
|
|
|
1664
1890
|
const remaining = new Set(
|
|
1665
1891
|
partitionsToRead.map((p) => `${p.topic}:${p.partition}`)
|
|
1666
1892
|
);
|
|
1893
|
+
let settled = false;
|
|
1667
1894
|
const cleanup = () => {
|
|
1668
1895
|
consumer.disconnect().catch(() => {
|
|
1669
1896
|
}).finally(() => {
|
|
@@ -1671,6 +1898,16 @@ async function recoverLamportClockImpl(ctx, topics) {
|
|
|
1671
1898
|
});
|
|
1672
1899
|
});
|
|
1673
1900
|
};
|
|
1901
|
+
const timeoutTimer = setTimeout(() => {
|
|
1902
|
+
if (settled) return;
|
|
1903
|
+
settled = true;
|
|
1904
|
+
ctx.logger.warn(
|
|
1905
|
+
`Clock recovery: timed out after ${ctx.clockRecoveryTimeoutMs} ms with ${remaining.size} partition(s) unread \u2014 proceeding with partial result`
|
|
1906
|
+
);
|
|
1907
|
+
cleanup();
|
|
1908
|
+
resolve();
|
|
1909
|
+
}, ctx.clockRecoveryTimeoutMs);
|
|
1910
|
+
timeoutTimer.unref?.();
|
|
1674
1911
|
consumer.connect().then(async () => {
|
|
1675
1912
|
const uniqueTopics = [
|
|
1676
1913
|
...new Set(partitionsToRead.map((p) => p.topic))
|
|
@@ -1691,13 +1928,18 @@ async function recoverLamportClockImpl(ctx, topics) {
|
|
|
1691
1928
|
const clock = Number(raw);
|
|
1692
1929
|
if (!Number.isNaN(clock) && clock > maxClock) maxClock = clock;
|
|
1693
1930
|
}
|
|
1694
|
-
if (remaining.size === 0) {
|
|
1931
|
+
if (remaining.size === 0 && !settled) {
|
|
1932
|
+
settled = true;
|
|
1933
|
+
clearTimeout(timeoutTimer);
|
|
1695
1934
|
cleanup();
|
|
1696
1935
|
resolve();
|
|
1697
1936
|
}
|
|
1698
1937
|
}
|
|
1699
1938
|
})
|
|
1700
1939
|
).catch((err) => {
|
|
1940
|
+
if (settled) return;
|
|
1941
|
+
settled = true;
|
|
1942
|
+
clearTimeout(timeoutTimer);
|
|
1701
1943
|
cleanup();
|
|
1702
1944
|
reject(err);
|
|
1703
1945
|
});
|
|
@@ -1738,6 +1980,15 @@ async function preparePayload(ctx, topicOrDesc, messages, compression) {
|
|
|
1738
1980
|
await ensureTopic(ctx, payload.topic);
|
|
1739
1981
|
return payload;
|
|
1740
1982
|
}
|
|
1983
|
+
async function redirectToDelayed(ctx, payload, deliverAfterMs) {
|
|
1984
|
+
const until = String(Date.now() + deliverAfterMs);
|
|
1985
|
+
for (const m of payload.messages) {
|
|
1986
|
+
m.headers[HEADER_DELAYED_UNTIL] = until;
|
|
1987
|
+
m.headers[HEADER_DELAYED_TARGET] = payload.topic;
|
|
1988
|
+
}
|
|
1989
|
+
payload.topic = `${payload.topic}.delayed`;
|
|
1990
|
+
await ensureTopic(ctx, payload.topic);
|
|
1991
|
+
}
|
|
1741
1992
|
async function sendMessageImpl(ctx, topicOrDesc, message, options = {}) {
|
|
1742
1993
|
await waitIfThrottled(ctx);
|
|
1743
1994
|
const payload = await preparePayload(
|
|
@@ -1755,6 +2006,9 @@ async function sendMessageImpl(ctx, topicOrDesc, message, options = {}) {
|
|
|
1755
2006
|
],
|
|
1756
2007
|
options.compression
|
|
1757
2008
|
);
|
|
2009
|
+
if (options.deliverAfterMs && options.deliverAfterMs > 0) {
|
|
2010
|
+
await redirectToDelayed(ctx, payload, options.deliverAfterMs);
|
|
2011
|
+
}
|
|
1758
2012
|
await ctx.producer.send(payload);
|
|
1759
2013
|
ctx.metrics.notifyAfterSend(payload.topic, payload.messages.length);
|
|
1760
2014
|
}
|
|
@@ -1766,6 +2020,9 @@ async function sendBatchImpl(ctx, topicOrDesc, messages, options) {
|
|
|
1766
2020
|
messages,
|
|
1767
2021
|
options?.compression
|
|
1768
2022
|
);
|
|
2023
|
+
if (options?.deliverAfterMs && options.deliverAfterMs > 0) {
|
|
2024
|
+
await redirectToDelayed(ctx, payload, options.deliverAfterMs);
|
|
2025
|
+
}
|
|
1769
2026
|
await ctx.producer.send(payload);
|
|
1770
2027
|
ctx.metrics.notifyAfterSend(payload.topic, payload.messages.length);
|
|
1771
2028
|
}
|
|
@@ -1802,6 +2059,17 @@ async function transactionImpl(ctx, fn) {
|
|
|
1802
2059
|
});
|
|
1803
2060
|
}
|
|
1804
2061
|
ctx.txProducer = await ctx.txProducerInitPromise;
|
|
2062
|
+
const prev = ctx._txChain;
|
|
2063
|
+
let release;
|
|
2064
|
+
ctx._txChain = new Promise((r) => release = r);
|
|
2065
|
+
await prev;
|
|
2066
|
+
try {
|
|
2067
|
+
await runTransaction(ctx, fn);
|
|
2068
|
+
} finally {
|
|
2069
|
+
release();
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
async function runTransaction(ctx, fn) {
|
|
1805
2073
|
const tx = await ctx.txProducer.transaction();
|
|
1806
2074
|
try {
|
|
1807
2075
|
const txCtx = {
|
|
@@ -1875,7 +2143,7 @@ async function waitForPartitionAssignment(consumer, topics, logger, timeoutMs =
|
|
|
1875
2143
|
`Retry consumer did not receive partition assignments for [${topics.join(", ")}] within ${timeoutMs}ms`
|
|
1876
2144
|
);
|
|
1877
2145
|
}
|
|
1878
|
-
async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopics, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs) {
|
|
2146
|
+
async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopics, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
|
|
1879
2147
|
const {
|
|
1880
2148
|
logger,
|
|
1881
2149
|
producer,
|
|
@@ -1921,20 +2189,35 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
1921
2189
|
await sleep(remaining);
|
|
1922
2190
|
consumer.resume([{ topic: levelTopic, partitions: [partition] }]);
|
|
1923
2191
|
}
|
|
1924
|
-
const
|
|
1925
|
-
const parsed = parseJsonMessage(raw, levelTopic, logger);
|
|
1926
|
-
if (parsed === null) {
|
|
1927
|
-
await consumer.commitOffsets([nextOffset]);
|
|
1928
|
-
return;
|
|
1929
|
-
}
|
|
2192
|
+
const rawBytes = message.value;
|
|
1930
2193
|
const currentMaxRetries = parseInt(
|
|
1931
2194
|
headers[RETRY_HEADER_MAX_RETRIES] ?? String(retry.maxRetries),
|
|
1932
2195
|
10
|
|
1933
2196
|
);
|
|
1934
2197
|
const originalTopic = headers[RETRY_HEADER_ORIGINAL_TOPIC] ?? levelTopic.replace(/\.retry\.\d+$/, "");
|
|
2198
|
+
const serde = serdeMap?.get(originalTopic) ?? deps.serde;
|
|
2199
|
+
let parsed;
|
|
2200
|
+
try {
|
|
2201
|
+
parsed = await serde.deserialize(rawBytes, {
|
|
2202
|
+
topic: originalTopic,
|
|
2203
|
+
headers,
|
|
2204
|
+
isKey: false
|
|
2205
|
+
});
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
logger.error(
|
|
2208
|
+
`Failed to deserialize retry message from topic ${levelTopic}:`,
|
|
2209
|
+
toError(err).stack
|
|
2210
|
+
);
|
|
2211
|
+
await consumer.commitOffsets([nextOffset]);
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
if (parsed === null) {
|
|
2215
|
+
await consumer.commitOffsets([nextOffset]);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
1935
2218
|
const validated = await validateWithSchema(
|
|
1936
2219
|
parsed,
|
|
1937
|
-
|
|
2220
|
+
rawBytes,
|
|
1938
2221
|
originalTopic,
|
|
1939
2222
|
schemaMap,
|
|
1940
2223
|
interceptors,
|
|
@@ -1969,6 +2252,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
1969
2252
|
await consumer.commitOffsets([nextOffset]);
|
|
1970
2253
|
return;
|
|
1971
2254
|
}
|
|
2255
|
+
deps.onFailure?.(envelope);
|
|
1972
2256
|
const exhausted = level >= currentMaxRetries;
|
|
1973
2257
|
const reportedError = exhausted && currentMaxRetries > 1 ? new KafkaRetryExhaustedError(
|
|
1974
2258
|
originalTopic,
|
|
@@ -1987,7 +2271,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
1987
2271
|
const delay = Math.floor(Math.random() * cap);
|
|
1988
2272
|
const { topic: rtTopic, messages: rtMsgs } = buildRetryTopicPayload(
|
|
1989
2273
|
originalTopic,
|
|
1990
|
-
[
|
|
2274
|
+
[rawBytes],
|
|
1991
2275
|
nextLevel,
|
|
1992
2276
|
currentMaxRetries,
|
|
1993
2277
|
delay,
|
|
@@ -2029,7 +2313,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
2029
2313
|
} else if (dlq) {
|
|
2030
2314
|
const { topic: dTopic, messages: dMsgs } = buildDlqPayload(
|
|
2031
2315
|
originalTopic,
|
|
2032
|
-
|
|
2316
|
+
rawBytes,
|
|
2033
2317
|
{
|
|
2034
2318
|
error,
|
|
2035
2319
|
// +1 to account for the main consumer's initial attempt before routing.
|
|
@@ -2091,7 +2375,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
|
|
|
2091
2375
|
`Retry level ${level}/${retry.maxRetries} consumer started for: ${originalTopics.join(", ")} (group: ${levelGroupId})`
|
|
2092
2376
|
);
|
|
2093
2377
|
}
|
|
2094
|
-
async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs) {
|
|
2378
|
+
async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
|
|
2095
2379
|
const levelGroupIds = new Array(retry.maxRetries);
|
|
2096
2380
|
await Promise.all(
|
|
2097
2381
|
Array.from({ length: retry.maxRetries }, async (_, i) => {
|
|
@@ -2109,7 +2393,8 @@ async function startRetryTopicConsumers(originalTopics, originalGroupId, handleM
|
|
|
2109
2393
|
interceptors,
|
|
2110
2394
|
schemaMap,
|
|
2111
2395
|
deps,
|
|
2112
|
-
assignmentTimeoutMs
|
|
2396
|
+
assignmentTimeoutMs,
|
|
2397
|
+
serdeMap
|
|
2113
2398
|
);
|
|
2114
2399
|
levelGroupIds[i] = levelGroupId;
|
|
2115
2400
|
})
|
|
@@ -2184,7 +2469,8 @@ async function setupConsumer(ctx, topics, mode, options) {
|
|
|
2184
2469
|
options.autoCommit ?? true,
|
|
2185
2470
|
ctx.consumerOpsDeps,
|
|
2186
2471
|
options.partitionAssigner,
|
|
2187
|
-
resolveReady
|
|
2472
|
+
resolveReady,
|
|
2473
|
+
options.groupInstanceId
|
|
2188
2474
|
);
|
|
2189
2475
|
const schemaMap = buildSchemaMap(
|
|
2190
2476
|
stringTopics,
|
|
@@ -2192,10 +2478,14 @@ async function setupConsumer(ctx, topics, mode, options) {
|
|
|
2192
2478
|
optionSchemas,
|
|
2193
2479
|
ctx.logger
|
|
2194
2480
|
);
|
|
2481
|
+
const serdeMap = buildSerdeMap(stringTopics);
|
|
2195
2482
|
const topicNames = stringTopics.map((t) => resolveTopicName(t));
|
|
2196
2483
|
const subscribeTopics = [...topicNames, ...regexTopics];
|
|
2197
2484
|
await ensureConsumerTopics(ctx, topicNames, dlq, options.deduplication);
|
|
2198
2485
|
await consumer.connect();
|
|
2486
|
+
if (dlq || options.retryTopics || options.deduplication) {
|
|
2487
|
+
await ctx.producer.connect();
|
|
2488
|
+
}
|
|
2199
2489
|
await subscribeWithRetry(
|
|
2200
2490
|
consumer,
|
|
2201
2491
|
subscribeTopics,
|
|
@@ -2206,19 +2496,19 @@ async function setupConsumer(ctx, topics, mode, options) {
|
|
|
2206
2496
|
ctx.logger.log(
|
|
2207
2497
|
`${mode === "eachBatch" ? "Batch consumer" : "Consumer"} subscribed to topics: ${displayTopics}`
|
|
2208
2498
|
);
|
|
2209
|
-
return { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, hasRegex, readyPromise };
|
|
2499
|
+
return { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, hasRegex, readyPromise };
|
|
2210
2500
|
}
|
|
2211
2501
|
function resolveDeduplicationContext(ctx, groupId, options) {
|
|
2212
2502
|
if (!options) return void 0;
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
return { options, state: ctx.dedupStates.get(groupId) };
|
|
2503
|
+
const store = options.store ?? new InMemoryDedupStore(ctx.dedupStates);
|
|
2504
|
+
return { options, store, groupId };
|
|
2216
2505
|
}
|
|
2217
2506
|
function messageDepsFor(ctx, gid, options) {
|
|
2218
2507
|
const notifyRetry = ctx.metrics.notifyRetry.bind(ctx.metrics);
|
|
2219
2508
|
return {
|
|
2220
2509
|
logger: ctx.logger,
|
|
2221
2510
|
producer: ctx.producer,
|
|
2511
|
+
serde: ctx.serde,
|
|
2222
2512
|
instrumentation: ctx.instrumentation,
|
|
2223
2513
|
onMessageLost: options?.onMessageLost ?? ctx.onMessageLost,
|
|
2224
2514
|
onTtlExpired: ctx.onTtlExpired,
|
|
@@ -2226,15 +2516,17 @@ function messageDepsFor(ctx, gid, options) {
|
|
|
2226
2516
|
notifyRetry(envelope, attempt, max);
|
|
2227
2517
|
return options.onRetry(envelope, attempt, max);
|
|
2228
2518
|
} : notifyRetry,
|
|
2229
|
-
onDlq: (envelope, reason) => ctx.metrics.notifyDlq(envelope, reason
|
|
2519
|
+
onDlq: (envelope, reason) => ctx.metrics.notifyDlq(envelope, reason),
|
|
2230
2520
|
onDuplicate: ctx.metrics.notifyDuplicate.bind(ctx.metrics),
|
|
2231
|
-
onMessage: (envelope) => ctx.metrics.notifyMessage(envelope, gid)
|
|
2521
|
+
onMessage: (envelope) => ctx.metrics.notifyMessage(envelope, gid),
|
|
2522
|
+
onFailure: (envelope) => ctx.metrics.notifyFailure(envelope, gid)
|
|
2232
2523
|
};
|
|
2233
2524
|
}
|
|
2234
2525
|
function buildRetryTopicDeps(ctx) {
|
|
2235
2526
|
return {
|
|
2236
2527
|
logger: ctx.logger,
|
|
2237
2528
|
producer: ctx.producer,
|
|
2529
|
+
serde: ctx.serde,
|
|
2238
2530
|
instrumentation: ctx.instrumentation,
|
|
2239
2531
|
onMessageLost: ctx.onMessageLost,
|
|
2240
2532
|
onRetry: ctx.metrics.notifyRetry.bind(ctx.metrics),
|
|
@@ -2252,7 +2544,7 @@ async function makeEosMainContext(ctx, gid, consumer, options) {
|
|
|
2252
2544
|
return { txProducer, consumer };
|
|
2253
2545
|
}
|
|
2254
2546
|
async function launchRetryChain(ctx, gid, topicNames, handleMessage, opts) {
|
|
2255
|
-
const { retry, dlq, interceptors, schemaMap, assignmentTimeoutMs } = opts;
|
|
2547
|
+
const { retry, dlq, interceptors, schemaMap, serdeMap, assignmentTimeoutMs } = opts;
|
|
2256
2548
|
if (!ctx.autoCreateTopicsEnabled) {
|
|
2257
2549
|
await ctx.adminOps.validateRetryTopicsExist(topicNames, retry.maxRetries);
|
|
2258
2550
|
}
|
|
@@ -2267,11 +2559,17 @@ async function launchRetryChain(ctx, gid, topicNames, handleMessage, opts) {
|
|
|
2267
2559
|
schemaMap,
|
|
2268
2560
|
{
|
|
2269
2561
|
...ctx.retryTopicDeps,
|
|
2562
|
+
// Bind circuit breaker events to the MAIN consumer group so failures and
|
|
2563
|
+
// successes inside the retry chain drive the same breaker as the main
|
|
2564
|
+
// consumer (the retry chain has no breaker config of its own).
|
|
2565
|
+
onFailure: (envelope) => ctx.metrics.notifyFailure(envelope, gid),
|
|
2566
|
+
onMessage: (envelope) => ctx.metrics.notifyMessage(envelope, gid),
|
|
2270
2567
|
onLevelStarted: (levelGroupId) => {
|
|
2271
2568
|
ctx.companionGroupIds.get(gid).push(levelGroupId);
|
|
2272
2569
|
}
|
|
2273
2570
|
},
|
|
2274
|
-
assignmentTimeoutMs
|
|
2571
|
+
assignmentTimeoutMs,
|
|
2572
|
+
serdeMap
|
|
2275
2573
|
);
|
|
2276
2574
|
}
|
|
2277
2575
|
|
|
@@ -2282,7 +2580,15 @@ async function applyDeduplication(envelope, raw, dedup, dlq, deps) {
|
|
|
2282
2580
|
const incomingClock = Number(clockRaw);
|
|
2283
2581
|
if (Number.isNaN(incomingClock)) return false;
|
|
2284
2582
|
const stateKey = `${envelope.topic}:${envelope.partition}`;
|
|
2285
|
-
|
|
2583
|
+
let lastProcessedClock;
|
|
2584
|
+
try {
|
|
2585
|
+
lastProcessedClock = await dedup.store.getLastClock(dedup.groupId, stateKey) ?? -1;
|
|
2586
|
+
} catch (err) {
|
|
2587
|
+
deps.logger.error(
|
|
2588
|
+
`Dedup store getLastClock failed on ${envelope.topic}[${envelope.partition}] \u2014 treating message as not a duplicate (fail-open): ${err.message}`
|
|
2589
|
+
);
|
|
2590
|
+
return false;
|
|
2591
|
+
}
|
|
2286
2592
|
if (incomingClock <= lastProcessedClock) {
|
|
2287
2593
|
const meta = {
|
|
2288
2594
|
incomingClock,
|
|
@@ -2312,21 +2618,38 @@ async function applyDeduplication(envelope, raw, dedup, dlq, deps) {
|
|
|
2312
2618
|
}
|
|
2313
2619
|
return true;
|
|
2314
2620
|
}
|
|
2315
|
-
|
|
2621
|
+
try {
|
|
2622
|
+
await dedup.store.setLastClock(dedup.groupId, stateKey, incomingClock);
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
deps.logger.error(
|
|
2625
|
+
`Dedup store setLastClock failed on ${envelope.topic}[${envelope.partition}] \u2014 processing message anyway (fail-open): ${err.message}`
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2316
2628
|
return false;
|
|
2317
2629
|
}
|
|
2318
|
-
async function parseSingleMessage(message, topic2, partition, schemaMap, interceptors, dlq, deps) {
|
|
2630
|
+
async function parseSingleMessage(message, topic2, partition, schemaMap, interceptors, dlq, deps, serdeMap) {
|
|
2319
2631
|
if (!message.value) {
|
|
2320
2632
|
deps.logger.warn(`Received empty message from topic ${topic2}`);
|
|
2321
2633
|
return null;
|
|
2322
2634
|
}
|
|
2323
|
-
const
|
|
2324
|
-
const parsed = parseJsonMessage(raw, topic2, deps.logger);
|
|
2325
|
-
if (parsed === null) return null;
|
|
2635
|
+
const bytes = message.value;
|
|
2326
2636
|
const headers = decodeHeaders(message.headers);
|
|
2637
|
+
const serde = serdeMap?.get(topic2) ?? deps.serde;
|
|
2638
|
+
let parsed;
|
|
2639
|
+
try {
|
|
2640
|
+
parsed = await serde.deserialize(bytes, { topic: topic2, headers, isKey: false });
|
|
2641
|
+
} catch (error) {
|
|
2642
|
+
deps.logger.error(
|
|
2643
|
+
`Failed to deserialize message from topic ${topic2}:`,
|
|
2644
|
+
toError(error).stack
|
|
2645
|
+
);
|
|
2646
|
+
return null;
|
|
2647
|
+
}
|
|
2648
|
+
if (parsed === null) return null;
|
|
2327
2649
|
const validated = await validateWithSchema(
|
|
2328
2650
|
parsed,
|
|
2329
|
-
|
|
2651
|
+
// Forward the ORIGINAL bytes to DLQ on validation failure (binary-safe).
|
|
2652
|
+
bytes,
|
|
2330
2653
|
topic2,
|
|
2331
2654
|
schemaMap,
|
|
2332
2655
|
interceptors,
|
|
@@ -2340,6 +2663,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2340
2663
|
const { topic: topic2, partition, message } = payload;
|
|
2341
2664
|
const {
|
|
2342
2665
|
schemaMap,
|
|
2666
|
+
serdeMap,
|
|
2343
2667
|
handleMessage,
|
|
2344
2668
|
interceptors,
|
|
2345
2669
|
dlq,
|
|
@@ -2348,6 +2672,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2348
2672
|
timeoutMs,
|
|
2349
2673
|
wrapWithTimeout
|
|
2350
2674
|
} = opts;
|
|
2675
|
+
const rawBytes = message.value;
|
|
2351
2676
|
const eos = opts.eosMainContext;
|
|
2352
2677
|
const nextOffsetStr = (parseInt(message.offset, 10) + 1).toString();
|
|
2353
2678
|
const commitOffset = eos ? async () => {
|
|
@@ -2392,7 +2717,8 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2392
2717
|
schemaMap,
|
|
2393
2718
|
interceptors,
|
|
2394
2719
|
dlq,
|
|
2395
|
-
deps
|
|
2720
|
+
deps,
|
|
2721
|
+
serdeMap
|
|
2396
2722
|
);
|
|
2397
2723
|
if (envelope === null) {
|
|
2398
2724
|
await commitOffset?.();
|
|
@@ -2401,7 +2727,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2401
2727
|
if (opts.deduplication) {
|
|
2402
2728
|
const isDuplicate = await applyDeduplication(
|
|
2403
2729
|
envelope,
|
|
2404
|
-
|
|
2730
|
+
rawBytes,
|
|
2405
2731
|
opts.deduplication,
|
|
2406
2732
|
dlq,
|
|
2407
2733
|
deps
|
|
@@ -2418,7 +2744,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2418
2744
|
`[KafkaClient] TTL expired on ${topic2}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
|
|
2419
2745
|
);
|
|
2420
2746
|
if (dlq) {
|
|
2421
|
-
await sendToDlq(topic2,
|
|
2747
|
+
await sendToDlq(topic2, rawBytes, deps, {
|
|
2422
2748
|
error: new Error(`Message TTL expired: age ${ageMs}ms`),
|
|
2423
2749
|
attempt: 0,
|
|
2424
2750
|
originalHeaders: envelope.headers
|
|
@@ -2450,7 +2776,7 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
2450
2776
|
},
|
|
2451
2777
|
{
|
|
2452
2778
|
envelope,
|
|
2453
|
-
rawMessages: [
|
|
2779
|
+
rawMessages: [rawBytes],
|
|
2454
2780
|
interceptors,
|
|
2455
2781
|
dlq,
|
|
2456
2782
|
retry,
|
|
@@ -2463,6 +2789,7 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2463
2789
|
const { batch, heartbeat, resolveOffset, commitOffsetsIfNecessary } = payload;
|
|
2464
2790
|
const {
|
|
2465
2791
|
schemaMap,
|
|
2792
|
+
serdeMap,
|
|
2466
2793
|
handleBatch,
|
|
2467
2794
|
interceptors,
|
|
2468
2795
|
dlq,
|
|
@@ -2518,6 +2845,7 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2518
2845
|
const envelopes = [];
|
|
2519
2846
|
const rawMessages = [];
|
|
2520
2847
|
for (const message of batch.messages) {
|
|
2848
|
+
const rawBytes = message.value;
|
|
2521
2849
|
const envelope = await parseSingleMessage(
|
|
2522
2850
|
message,
|
|
2523
2851
|
batch.topic,
|
|
@@ -2525,14 +2853,14 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2525
2853
|
schemaMap,
|
|
2526
2854
|
interceptors,
|
|
2527
2855
|
dlq,
|
|
2528
|
-
deps
|
|
2856
|
+
deps,
|
|
2857
|
+
serdeMap
|
|
2529
2858
|
);
|
|
2530
2859
|
if (envelope === null) continue;
|
|
2531
2860
|
if (opts.deduplication) {
|
|
2532
|
-
const raw = message.value.toString();
|
|
2533
2861
|
const isDuplicate = await applyDeduplication(
|
|
2534
2862
|
envelope,
|
|
2535
|
-
|
|
2863
|
+
rawBytes,
|
|
2536
2864
|
opts.deduplication,
|
|
2537
2865
|
dlq,
|
|
2538
2866
|
deps
|
|
@@ -2546,7 +2874,7 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2546
2874
|
`[KafkaClient] TTL expired on ${batch.topic}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
|
|
2547
2875
|
);
|
|
2548
2876
|
if (dlq) {
|
|
2549
|
-
await sendToDlq(batch.topic,
|
|
2877
|
+
await sendToDlq(batch.topic, rawBytes, deps, {
|
|
2550
2878
|
error: new Error(`Message TTL expired: age ${ageMs}ms`),
|
|
2551
2879
|
attempt: 0,
|
|
2552
2880
|
originalHeaders: envelope.headers
|
|
@@ -2565,7 +2893,7 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
2565
2893
|
}
|
|
2566
2894
|
}
|
|
2567
2895
|
envelopes.push(envelope);
|
|
2568
|
-
rawMessages.push(
|
|
2896
|
+
rawMessages.push(rawBytes);
|
|
2569
2897
|
}
|
|
2570
2898
|
if (envelopes.length === 0) {
|
|
2571
2899
|
await commitBatchOffset?.();
|
|
@@ -2728,7 +3056,7 @@ function resumeTopicAllPartitions(ctx, gid, topic2) {
|
|
|
2728
3056
|
async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
|
|
2729
3057
|
validateTopicConsumerOpts(topics, options);
|
|
2730
3058
|
const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
|
|
2731
|
-
const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachMessage", setupOptions);
|
|
3059
|
+
const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachMessage", setupOptions);
|
|
2732
3060
|
if (options.circuitBreaker)
|
|
2733
3061
|
ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
|
|
2734
3062
|
const deps = messageDepsFor(ctx, gid, options);
|
|
@@ -2739,6 +3067,7 @@ async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
|
|
|
2739
3067
|
payload,
|
|
2740
3068
|
{
|
|
2741
3069
|
schemaMap,
|
|
3070
|
+
serdeMap,
|
|
2742
3071
|
handleMessage,
|
|
2743
3072
|
interceptors,
|
|
2744
3073
|
dlq,
|
|
@@ -2766,6 +3095,7 @@ async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
|
|
|
2766
3095
|
dlq,
|
|
2767
3096
|
interceptors,
|
|
2768
3097
|
schemaMap,
|
|
3098
|
+
serdeMap,
|
|
2769
3099
|
assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
|
|
2770
3100
|
});
|
|
2771
3101
|
}
|
|
@@ -2779,7 +3109,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
|
|
|
2779
3109
|
);
|
|
2780
3110
|
}
|
|
2781
3111
|
const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
|
|
2782
|
-
const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachBatch", setupOptions);
|
|
3112
|
+
const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachBatch", setupOptions);
|
|
2783
3113
|
if (options.circuitBreaker)
|
|
2784
3114
|
ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
|
|
2785
3115
|
const deps = messageDepsFor(ctx, gid, options);
|
|
@@ -2790,6 +3120,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
|
|
|
2790
3120
|
payload,
|
|
2791
3121
|
{
|
|
2792
3122
|
schemaMap,
|
|
3123
|
+
serdeMap,
|
|
2793
3124
|
handleBatch,
|
|
2794
3125
|
interceptors,
|
|
2795
3126
|
dlq,
|
|
@@ -2827,6 +3158,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
|
|
|
2827
3158
|
dlq,
|
|
2828
3159
|
interceptors,
|
|
2829
3160
|
schemaMap,
|
|
3161
|
+
serdeMap,
|
|
2830
3162
|
assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
|
|
2831
3163
|
});
|
|
2832
3164
|
}
|
|
@@ -2839,7 +3171,7 @@ async function startTransactionalConsumerImpl(ctx, topics, handler, options = {}
|
|
|
2839
3171
|
);
|
|
2840
3172
|
}
|
|
2841
3173
|
const setupOptions = { ...options, autoCommit: false };
|
|
2842
|
-
const { consumer, schemaMap, gid, readyPromise } = await setupConsumer(
|
|
3174
|
+
const { consumer, schemaMap, serdeMap, gid, readyPromise } = await setupConsumer(
|
|
2843
3175
|
ctx,
|
|
2844
3176
|
topics,
|
|
2845
3177
|
"eachMessage",
|
|
@@ -2856,7 +3188,8 @@ async function startTransactionalConsumerImpl(ctx, topics, handler, options = {}
|
|
|
2856
3188
|
schemaMap,
|
|
2857
3189
|
options.interceptors ?? [],
|
|
2858
3190
|
false,
|
|
2859
|
-
deps
|
|
3191
|
+
deps,
|
|
3192
|
+
serdeMap
|
|
2860
3193
|
);
|
|
2861
3194
|
const nextOffset = String(Number.parseInt(message.offset, 10) + 1);
|
|
2862
3195
|
if (envelope === null) {
|
|
@@ -2925,7 +3258,7 @@ function stopConsumerByGid(ctx, gid) {
|
|
|
2925
3258
|
return stopConsumerImpl(ctx, gid);
|
|
2926
3259
|
}
|
|
2927
3260
|
|
|
2928
|
-
// src/client/kafka.client/consumer/window.ts
|
|
3261
|
+
// src/client/kafka.client/consumer/features/window.ts
|
|
2929
3262
|
async function startWindowConsumerImpl(ctx, topic2, handler, options) {
|
|
2930
3263
|
const { maxMessages, maxMs, ...consumerOptions } = options;
|
|
2931
3264
|
if (maxMessages <= 0)
|
|
@@ -2939,6 +3272,7 @@ async function startWindowConsumerImpl(ctx, topic2, handler, options) {
|
|
|
2939
3272
|
const buffer = [];
|
|
2940
3273
|
let flushTimer = null;
|
|
2941
3274
|
let windowStart = 0;
|
|
3275
|
+
const onLost = consumerOptions.onMessageLost ?? ctx.onMessageLost;
|
|
2942
3276
|
const flush = async (trigger) => {
|
|
2943
3277
|
if (flushTimer !== null) {
|
|
2944
3278
|
clearTimeout(flushTimer);
|
|
@@ -2946,17 +3280,32 @@ async function startWindowConsumerImpl(ctx, topic2, handler, options) {
|
|
|
2946
3280
|
}
|
|
2947
3281
|
if (buffer.length === 0) return;
|
|
2948
3282
|
const envelopes = buffer.splice(0);
|
|
2949
|
-
|
|
3283
|
+
try {
|
|
3284
|
+
await handler(envelopes, { trigger, windowStart, windowEnd: Date.now() });
|
|
3285
|
+
} catch (err) {
|
|
3286
|
+
const error = toError(err);
|
|
3287
|
+
ctx.logger.error(
|
|
3288
|
+
`startWindowConsumer: ${trigger}-triggered flush failed \u2014 window of ${envelopes.length} message(s) lost:`,
|
|
3289
|
+
error.stack
|
|
3290
|
+
);
|
|
3291
|
+
for (const envelope of envelopes) {
|
|
3292
|
+
await Promise.resolve(
|
|
3293
|
+
onLost?.({
|
|
3294
|
+
topic: envelope.topic,
|
|
3295
|
+
error,
|
|
3296
|
+
attempt: 0,
|
|
3297
|
+
headers: envelope.headers
|
|
3298
|
+
})
|
|
3299
|
+
).catch(() => {
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
2950
3303
|
};
|
|
2951
3304
|
const scheduleFlush = () => {
|
|
2952
3305
|
if (flushTimer !== null) return;
|
|
2953
3306
|
flushTimer = setTimeout(() => {
|
|
2954
3307
|
flushTimer = null;
|
|
2955
|
-
flush("time")
|
|
2956
|
-
ctx.logger.warn(
|
|
2957
|
-
`startWindowConsumer: time-triggered flush error \u2014 ${toError(err).message}`
|
|
2958
|
-
);
|
|
2959
|
-
});
|
|
3308
|
+
void flush("time");
|
|
2960
3309
|
}, maxMs);
|
|
2961
3310
|
};
|
|
2962
3311
|
const handle = await startConsumerImpl(
|
|
@@ -2972,40 +3321,13 @@ async function startWindowConsumerImpl(ctx, topic2, handler, options) {
|
|
|
2972
3321
|
);
|
|
2973
3322
|
const originalStop = handle.stop.bind(handle);
|
|
2974
3323
|
handle.stop = async () => {
|
|
2975
|
-
|
|
2976
|
-
clearTimeout(flushTimer);
|
|
2977
|
-
flushTimer = null;
|
|
2978
|
-
}
|
|
2979
|
-
if (buffer.length > 0) {
|
|
2980
|
-
const envelopes = buffer.splice(0);
|
|
2981
|
-
await handler(envelopes, {
|
|
2982
|
-
trigger: "time",
|
|
2983
|
-
windowStart,
|
|
2984
|
-
windowEnd: Date.now()
|
|
2985
|
-
}).catch(async (err) => {
|
|
2986
|
-
const error = toError(err);
|
|
2987
|
-
ctx.logger.warn(
|
|
2988
|
-
`startWindowConsumer: shutdown flush error \u2014 ${error.message}`
|
|
2989
|
-
);
|
|
2990
|
-
for (const envelope of envelopes) {
|
|
2991
|
-
await Promise.resolve(
|
|
2992
|
-
ctx.onMessageLost?.({
|
|
2993
|
-
topic: envelope.topic,
|
|
2994
|
-
error,
|
|
2995
|
-
attempt: 0,
|
|
2996
|
-
headers: envelope.headers
|
|
2997
|
-
})
|
|
2998
|
-
).catch(() => {
|
|
2999
|
-
});
|
|
3000
|
-
}
|
|
3001
|
-
});
|
|
3002
|
-
}
|
|
3324
|
+
await flush("time");
|
|
3003
3325
|
return originalStop();
|
|
3004
3326
|
};
|
|
3005
3327
|
return handle;
|
|
3006
3328
|
}
|
|
3007
3329
|
|
|
3008
|
-
// src/client/kafka.client/consumer/routed.ts
|
|
3330
|
+
// src/client/kafka.client/consumer/features/routed.ts
|
|
3009
3331
|
async function startRoutedConsumerImpl(ctx, topics, routing, options) {
|
|
3010
3332
|
const { header, routes, fallback } = routing;
|
|
3011
3333
|
const handleMessage = async (envelope) => {
|
|
@@ -3020,7 +3342,120 @@ async function startRoutedConsumerImpl(ctx, topics, routing, options) {
|
|
|
3020
3342
|
return startConsumerImpl(ctx, topics, handleMessage, options);
|
|
3021
3343
|
}
|
|
3022
3344
|
|
|
3023
|
-
// src/client/kafka.client/consumer/
|
|
3345
|
+
// src/client/kafka.client/consumer/features/delayed.ts
|
|
3346
|
+
function delayedTopicName(topic2) {
|
|
3347
|
+
return `${topic2}.delayed`;
|
|
3348
|
+
}
|
|
3349
|
+
async function startDelayedRelayImpl(ctx, topics, options) {
|
|
3350
|
+
if (topics.length === 0) {
|
|
3351
|
+
throw new Error("startDelayedRelay: at least one topic is required");
|
|
3352
|
+
}
|
|
3353
|
+
const gid = options?.groupId ?? `${ctx.defaultGroupId}-delayed-relay`;
|
|
3354
|
+
if (ctx.runningConsumers.has(gid)) {
|
|
3355
|
+
throw new Error(
|
|
3356
|
+
`startDelayedRelay("${gid}") called twice \u2014 this group is already consuming. Call stopConsumer("${gid}") first or pass a different groupId.`
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
const delayedTopics = topics.map(delayedTopicName);
|
|
3360
|
+
for (const t of delayedTopics) await ensureTopic(ctx, t);
|
|
3361
|
+
const txProducer = await createRetryTxProducer(ctx, `${gid}-tx`);
|
|
3362
|
+
let resolveReady;
|
|
3363
|
+
const readyPromise = new Promise((resolve) => {
|
|
3364
|
+
resolveReady = resolve;
|
|
3365
|
+
});
|
|
3366
|
+
const consumer = getOrCreateConsumer(
|
|
3367
|
+
gid,
|
|
3368
|
+
false,
|
|
3369
|
+
false,
|
|
3370
|
+
ctx.consumerOpsDeps,
|
|
3371
|
+
void 0,
|
|
3372
|
+
resolveReady
|
|
3373
|
+
);
|
|
3374
|
+
await consumer.connect();
|
|
3375
|
+
await subscribeWithRetry(consumer, delayedTopics, ctx.logger);
|
|
3376
|
+
await consumer.run({
|
|
3377
|
+
eachMessage: async ({ topic: stagingTopic, partition, message }) => {
|
|
3378
|
+
const nextOffset = {
|
|
3379
|
+
topic: stagingTopic,
|
|
3380
|
+
partition,
|
|
3381
|
+
offset: (parseInt(message.offset, 10) + 1).toString()
|
|
3382
|
+
};
|
|
3383
|
+
if (!message.value) {
|
|
3384
|
+
await consumer.commitOffsets([nextOffset]);
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
const headers = decodeHeaders(message.headers);
|
|
3388
|
+
const target = headers[HEADER_DELAYED_TARGET] ?? stagingTopic.replace(/\.delayed$/, "");
|
|
3389
|
+
const until = parseInt(
|
|
3390
|
+
headers[HEADER_DELAYED_UNTIL] ?? "0",
|
|
3391
|
+
10
|
|
3392
|
+
);
|
|
3393
|
+
const remaining = until - Date.now();
|
|
3394
|
+
if (remaining > 0) {
|
|
3395
|
+
consumer.pause([{ topic: stagingTopic, partitions: [partition] }]);
|
|
3396
|
+
await sleep(remaining);
|
|
3397
|
+
consumer.resume([{ topic: stagingTopic, partitions: [partition] }]);
|
|
3398
|
+
}
|
|
3399
|
+
const forwardHeaders = Object.fromEntries(
|
|
3400
|
+
Object.entries(headers).filter(
|
|
3401
|
+
([k]) => k !== HEADER_DELAYED_UNTIL && k !== HEADER_DELAYED_TARGET
|
|
3402
|
+
)
|
|
3403
|
+
);
|
|
3404
|
+
const tx = await txProducer.transaction();
|
|
3405
|
+
try {
|
|
3406
|
+
await tx.send({
|
|
3407
|
+
topic: target,
|
|
3408
|
+
messages: [
|
|
3409
|
+
{
|
|
3410
|
+
// Forward the ORIGINAL wire bytes unchanged — no re-serialization,
|
|
3411
|
+
// so binary payloads (Avro/Protobuf) are relayed losslessly.
|
|
3412
|
+
value: message.value,
|
|
3413
|
+
key: message.key ? message.key.toString() : null,
|
|
3414
|
+
headers: forwardHeaders
|
|
3415
|
+
}
|
|
3416
|
+
]
|
|
3417
|
+
});
|
|
3418
|
+
await tx.sendOffsets({
|
|
3419
|
+
consumer,
|
|
3420
|
+
topics: [
|
|
3421
|
+
{
|
|
3422
|
+
topic: nextOffset.topic,
|
|
3423
|
+
partitions: [
|
|
3424
|
+
{ partition: nextOffset.partition, offset: nextOffset.offset }
|
|
3425
|
+
]
|
|
3426
|
+
}
|
|
3427
|
+
]
|
|
3428
|
+
});
|
|
3429
|
+
await tx.commit();
|
|
3430
|
+
ctx.logger.debug?.(
|
|
3431
|
+
`Delayed message relayed to "${target}" (deadline ${new Date(until).toISOString()})`
|
|
3432
|
+
);
|
|
3433
|
+
} catch (txErr) {
|
|
3434
|
+
try {
|
|
3435
|
+
await tx.abort();
|
|
3436
|
+
} catch {
|
|
3437
|
+
}
|
|
3438
|
+
ctx.logger.error(
|
|
3439
|
+
`Delayed relay to "${target}" failed \u2014 message will be redelivered:`,
|
|
3440
|
+
toError(txErr).stack
|
|
3441
|
+
);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
});
|
|
3445
|
+
ctx.runningConsumers.set(gid, "eachMessage");
|
|
3446
|
+
ctx.logger.log(
|
|
3447
|
+
`Delayed relay started for: ${delayedTopics.join(", ")} (group: ${gid})`
|
|
3448
|
+
);
|
|
3449
|
+
return {
|
|
3450
|
+
groupId: gid,
|
|
3451
|
+
ready: () => readyPromise,
|
|
3452
|
+
stop: async () => {
|
|
3453
|
+
await stopConsumerImpl(ctx, gid);
|
|
3454
|
+
}
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
// src/client/kafka.client/consumer/features/snapshot.ts
|
|
3024
3459
|
async function readSnapshotImpl(ctx, topic2, options = {}) {
|
|
3025
3460
|
await ctx.adminOps.ensureConnected();
|
|
3026
3461
|
let offsets;
|
|
@@ -3286,6 +3721,7 @@ var KafkaClient = class {
|
|
|
3286
3721
|
* ```
|
|
3287
3722
|
*/
|
|
3288
3723
|
constructor(clientId, groupId, brokers, options) {
|
|
3724
|
+
validateClientOptions(clientId, groupId, brokers, options);
|
|
3289
3725
|
this.clientId = clientId;
|
|
3290
3726
|
const logger = options?.logger ?? {
|
|
3291
3727
|
log: (msg) => console.log(`[KafkaClient:${clientId}] ${msg}`),
|
|
@@ -3293,12 +3729,14 @@ var KafkaClient = class {
|
|
|
3293
3729
|
error: (msg, ...args) => console.error(`[KafkaClient:${clientId}] ${msg}`, ...args),
|
|
3294
3730
|
debug: (msg, ...args) => console.debug(`[KafkaClient:${clientId}] ${msg}`, ...args)
|
|
3295
3731
|
};
|
|
3296
|
-
const
|
|
3732
|
+
const security = resolveSecurityOptions(options?.security, brokers, logger);
|
|
3733
|
+
const transport = options?.transport ?? new ConfluentTransport(clientId, brokers, security);
|
|
3297
3734
|
const producer = transport.producer();
|
|
3298
3735
|
const runningConsumers = /* @__PURE__ */ new Map();
|
|
3299
3736
|
const consumers = /* @__PURE__ */ new Map();
|
|
3300
3737
|
const consumerCreationOptions = /* @__PURE__ */ new Map();
|
|
3301
3738
|
const schemaRegistry = /* @__PURE__ */ new Map();
|
|
3739
|
+
const serde = options?.serde ?? new JsonSerde();
|
|
3302
3740
|
const adminOps = new AdminOps({
|
|
3303
3741
|
admin: transport.admin(),
|
|
3304
3742
|
logger,
|
|
@@ -3325,8 +3763,10 @@ var KafkaClient = class {
|
|
|
3325
3763
|
autoCreateTopicsEnabled: options?.autoCreateTopics ?? false,
|
|
3326
3764
|
strictSchemasEnabled: options?.strictSchemas ?? true,
|
|
3327
3765
|
numPartitions: options?.numPartitions ?? 1,
|
|
3766
|
+
serde,
|
|
3328
3767
|
txId: options?.transactionalId ?? `${clientId}-tx`,
|
|
3329
3768
|
clockRecoveryTopics: options?.clockRecovery?.topics ?? [],
|
|
3769
|
+
clockRecoveryTimeoutMs: options?.clockRecovery?.timeoutMs ?? 3e4,
|
|
3330
3770
|
lagThrottleOpts: options?.lagThrottle,
|
|
3331
3771
|
instrumentation: options?.instrumentation ?? [],
|
|
3332
3772
|
onMessageLost: options?.onMessageLost,
|
|
@@ -3336,6 +3776,7 @@ var KafkaClient = class {
|
|
|
3336
3776
|
producer,
|
|
3337
3777
|
txProducer: void 0,
|
|
3338
3778
|
txProducerInitPromise: void 0,
|
|
3779
|
+
_txChain: Promise.resolve(),
|
|
3339
3780
|
retryTxProducers: /* @__PURE__ */ new Map(),
|
|
3340
3781
|
consumers,
|
|
3341
3782
|
runningConsumers,
|
|
@@ -3357,6 +3798,7 @@ var KafkaClient = class {
|
|
|
3357
3798
|
strictSchemasEnabled: options?.strictSchemas ?? true,
|
|
3358
3799
|
instrumentation: options?.instrumentation ?? [],
|
|
3359
3800
|
logger,
|
|
3801
|
+
serde,
|
|
3360
3802
|
nextLamportClock: () => 0
|
|
3361
3803
|
// patched below
|
|
3362
3804
|
},
|
|
@@ -3375,6 +3817,7 @@ var KafkaClient = class {
|
|
|
3375
3817
|
strictSchemasEnabled: options?.strictSchemas ?? true,
|
|
3376
3818
|
instrumentation: options?.instrumentation ?? [],
|
|
3377
3819
|
logger,
|
|
3820
|
+
serde,
|
|
3378
3821
|
nextLamportClock: () => ++ctx._lamportClock
|
|
3379
3822
|
};
|
|
3380
3823
|
ctx.retryTopicDeps = buildRetryTopicDeps(ctx);
|
|
@@ -3459,6 +3902,31 @@ var KafkaClient = class {
|
|
|
3459
3902
|
startRoutedConsumer(topics, routing, options) {
|
|
3460
3903
|
return startRoutedConsumerImpl(this.ctx, topics, routing, options);
|
|
3461
3904
|
}
|
|
3905
|
+
// ── Consumer: delayed delivery relay ──────────────────────────────
|
|
3906
|
+
/**
|
|
3907
|
+
* Start a relay that delivers messages produced with
|
|
3908
|
+
* `SendOptions.deliverAfterMs` from `<topic>.delayed` to their target topic
|
|
3909
|
+
* once their deadline passes.
|
|
3910
|
+
*
|
|
3911
|
+
* Forwarding is transactional (produce + source-offset commit are atomic),
|
|
3912
|
+
* so no duplicates are relayed even if the relay crashes mid-forward.
|
|
3913
|
+
* Delivery time is a lower bound — the relay must be running for delayed
|
|
3914
|
+
* messages to be delivered at all.
|
|
3915
|
+
*
|
|
3916
|
+
* @param topics Target topic name(s) whose `<topic>.delayed` staging topics to relay.
|
|
3917
|
+
* @param options Optional `groupId` override (default: `<defaultGroupId>-delayed-relay`).
|
|
3918
|
+
*
|
|
3919
|
+
* @example
|
|
3920
|
+
* ```ts
|
|
3921
|
+
* await kafka.startDelayedRelay(['orders.reminder']);
|
|
3922
|
+
* await kafka.sendMessage('orders.reminder', payload, { deliverAfterMs: 60_000 });
|
|
3923
|
+
* // → delivered to orders.reminder ~60 s later
|
|
3924
|
+
* ```
|
|
3925
|
+
*/
|
|
3926
|
+
async startDelayedRelay(topics, options) {
|
|
3927
|
+
const list = Array.isArray(topics) ? topics : [topics];
|
|
3928
|
+
return startDelayedRelayImpl(this.ctx, list, options);
|
|
3929
|
+
}
|
|
3462
3930
|
// ── Consumer: transactional EOS ───────────────────────────────────
|
|
3463
3931
|
/** @inheritDoc */
|
|
3464
3932
|
async startTransactionalConsumer(topics, handler, options = {}) {
|
|
@@ -3604,17 +4072,872 @@ var KafkaClient = class {
|
|
|
3604
4072
|
function topic(name) {
|
|
3605
4073
|
return {
|
|
3606
4074
|
/** Provide an explicit message type without a runtime schema. */
|
|
3607
|
-
type: () => ({
|
|
4075
|
+
type: () => keyable({
|
|
3608
4076
|
__topic: name,
|
|
3609
4077
|
__type: void 0
|
|
3610
4078
|
}),
|
|
3611
|
-
schema: (schema) => ({
|
|
4079
|
+
schema: (schema) => keyable({
|
|
3612
4080
|
__topic: name,
|
|
3613
4081
|
__type: void 0,
|
|
3614
4082
|
__schema: schema
|
|
3615
4083
|
})
|
|
3616
4084
|
};
|
|
3617
4085
|
}
|
|
4086
|
+
function keyable(desc) {
|
|
4087
|
+
return {
|
|
4088
|
+
...desc,
|
|
4089
|
+
key: (extractor) => keyable({ ...desc, __key: extractor }),
|
|
4090
|
+
serde: (serde) => keyable({ ...desc, __serde: serde })
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
// src/client/message/versioned-schema.ts
|
|
4095
|
+
function versionedSchema(versions, options) {
|
|
4096
|
+
const registered = Object.keys(versions).map(Number).filter((v) => Number.isInteger(v) && v > 0).sort((a, b) => a - b);
|
|
4097
|
+
if (registered.length === 0) {
|
|
4098
|
+
throw new Error(
|
|
4099
|
+
"versionedSchema: at least one schema version must be registered (keys must be positive integers)"
|
|
4100
|
+
);
|
|
4101
|
+
}
|
|
4102
|
+
const latestVersion = registered[registered.length - 1];
|
|
4103
|
+
return {
|
|
4104
|
+
async parse(data, ctx) {
|
|
4105
|
+
const version = ctx?.version ?? latestVersion;
|
|
4106
|
+
const schema = versions[version];
|
|
4107
|
+
if (!schema) {
|
|
4108
|
+
throw new Error(
|
|
4109
|
+
`versionedSchema: no schema registered for version ${version}${ctx?.topic ? ` (topic "${ctx.topic}")` : ""} \u2014 registered versions: ${registered.join(", ")}`
|
|
4110
|
+
);
|
|
4111
|
+
}
|
|
4112
|
+
const parsed = await schema.parse(data, ctx);
|
|
4113
|
+
if (version < latestVersion && options?.migrate) {
|
|
4114
|
+
return options.migrate(parsed, version, latestVersion);
|
|
4115
|
+
}
|
|
4116
|
+
return parsed;
|
|
4117
|
+
}
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
// src/client/message/schema-registry.ts
|
|
4122
|
+
var SchemaRegistryClient = class {
|
|
4123
|
+
constructor(options) {
|
|
4124
|
+
this.options = options;
|
|
4125
|
+
if (!options.baseUrl) {
|
|
4126
|
+
throw new Error("SchemaRegistryClient: baseUrl is required");
|
|
4127
|
+
}
|
|
4128
|
+
this.fetchFn = options.fetchFn ?? fetch;
|
|
4129
|
+
this.cacheTtlMs = options.cacheTtlMs ?? 3e5;
|
|
4130
|
+
}
|
|
4131
|
+
options;
|
|
4132
|
+
fetchFn;
|
|
4133
|
+
cacheTtlMs;
|
|
4134
|
+
latestCache = /* @__PURE__ */ new Map();
|
|
4135
|
+
/**
|
|
4136
|
+
* `id → schema` cache. Schema ids are immutable in a Confluent-compatible
|
|
4137
|
+
* registry (a given id always maps to the same schema string), so entries
|
|
4138
|
+
* are cached for the lifetime of the client with no TTL.
|
|
4139
|
+
*/
|
|
4140
|
+
byIdCache = /* @__PURE__ */ new Map();
|
|
4141
|
+
headers() {
|
|
4142
|
+
const h = {
|
|
4143
|
+
"Content-Type": "application/vnd.schemaregistry.v1+json"
|
|
4144
|
+
};
|
|
4145
|
+
if (this.options.auth) {
|
|
4146
|
+
const { username, password } = this.options.auth;
|
|
4147
|
+
h["Authorization"] = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
|
|
4148
|
+
}
|
|
4149
|
+
return h;
|
|
4150
|
+
}
|
|
4151
|
+
async request(method, path, body) {
|
|
4152
|
+
const url = `${this.options.baseUrl.replace(/\/$/, "")}${path}`;
|
|
4153
|
+
const res = await this.fetchFn(url, {
|
|
4154
|
+
method,
|
|
4155
|
+
headers: this.headers(),
|
|
4156
|
+
...body !== void 0 && { body: JSON.stringify(body) }
|
|
4157
|
+
});
|
|
4158
|
+
if (!res.ok) {
|
|
4159
|
+
const text = await res.text().catch(() => "");
|
|
4160
|
+
throw new Error(
|
|
4161
|
+
`SchemaRegistry ${method} ${path} failed: ${res.status} ${res.statusText}${text ? ` \u2014 ${text}` : ""}`
|
|
4162
|
+
);
|
|
4163
|
+
}
|
|
4164
|
+
return await res.json();
|
|
4165
|
+
}
|
|
4166
|
+
/** Fetch the latest schema registered under `subject`. Cached for `cacheTtlMs`. */
|
|
4167
|
+
async getLatestSchema(subject) {
|
|
4168
|
+
const cached = this.latestCache.get(subject);
|
|
4169
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
4170
|
+
const raw = await this.request("GET", `/subjects/${encodeURIComponent(subject)}/versions/latest`);
|
|
4171
|
+
const value = {
|
|
4172
|
+
id: raw.id,
|
|
4173
|
+
version: raw.version,
|
|
4174
|
+
schema: raw.schema
|
|
4175
|
+
};
|
|
4176
|
+
this.latestCache.set(subject, {
|
|
4177
|
+
value,
|
|
4178
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
4179
|
+
});
|
|
4180
|
+
return value;
|
|
4181
|
+
}
|
|
4182
|
+
/**
|
|
4183
|
+
* Fetch a schema by its globally unique registry id (`GET /schemas/ids/{id}`).
|
|
4184
|
+
*
|
|
4185
|
+
* Used by the Avro/Protobuf serdes on the deserialize path: the writer schema
|
|
4186
|
+
* id is read from the Confluent wire-format prefix, then resolved here. Results
|
|
4187
|
+
* are cached forever (schema ids are immutable), so a given id triggers exactly
|
|
4188
|
+
* one registry round-trip regardless of how many messages reference it.
|
|
4189
|
+
*/
|
|
4190
|
+
async getSchemaById(id) {
|
|
4191
|
+
const cached = this.byIdCache.get(id);
|
|
4192
|
+
if (cached) return cached;
|
|
4193
|
+
const raw = await this.request(
|
|
4194
|
+
"GET",
|
|
4195
|
+
`/schemas/ids/${id}`
|
|
4196
|
+
);
|
|
4197
|
+
const value = { id, schema: raw.schema, schemaType: raw.schemaType };
|
|
4198
|
+
this.byIdCache.set(id, value);
|
|
4199
|
+
return value;
|
|
4200
|
+
}
|
|
4201
|
+
/** Fetch a specific schema version of a subject. */
|
|
4202
|
+
async getSchemaVersion(subject, version) {
|
|
4203
|
+
const raw = await this.request(
|
|
4204
|
+
"GET",
|
|
4205
|
+
`/subjects/${encodeURIComponent(subject)}/versions/${version}`
|
|
4206
|
+
);
|
|
4207
|
+
return { id: raw.id, version: raw.version, schema: raw.schema };
|
|
4208
|
+
}
|
|
4209
|
+
/**
|
|
4210
|
+
* Register a schema under `subject` (idempotent — re-registering the same
|
|
4211
|
+
* schema returns the existing id). Returns the registry-assigned schema id.
|
|
4212
|
+
*/
|
|
4213
|
+
async registerSchema(subject, schema, schemaType = "JSON") {
|
|
4214
|
+
this.latestCache.delete(subject);
|
|
4215
|
+
return this.request(
|
|
4216
|
+
"POST",
|
|
4217
|
+
`/subjects/${encodeURIComponent(subject)}/versions`,
|
|
4218
|
+
{ schema, schemaType }
|
|
4219
|
+
);
|
|
4220
|
+
}
|
|
4221
|
+
/**
|
|
4222
|
+
* Test `schema` against the subject's compatibility policy without registering.
|
|
4223
|
+
* Returns `true` when the registry reports the schema as compatible.
|
|
4224
|
+
*/
|
|
4225
|
+
async checkCompatibility(subject, schema, schemaType = "JSON") {
|
|
4226
|
+
const res = await this.request(
|
|
4227
|
+
"POST",
|
|
4228
|
+
`/compatibility/subjects/${encodeURIComponent(subject)}/versions/latest`,
|
|
4229
|
+
{ schema, schemaType }
|
|
4230
|
+
);
|
|
4231
|
+
return res.is_compatible;
|
|
4232
|
+
}
|
|
4233
|
+
};
|
|
4234
|
+
function registrySchema(client, subject, options) {
|
|
4235
|
+
const enforceVersion = options?.enforceVersion ?? true;
|
|
4236
|
+
return {
|
|
4237
|
+
async parse(data, ctx) {
|
|
4238
|
+
const latest = await client.getLatestSchema(subject);
|
|
4239
|
+
if (enforceVersion && ctx?.version !== void 0 && ctx.version > latest.version) {
|
|
4240
|
+
throw new Error(
|
|
4241
|
+
`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`
|
|
4242
|
+
);
|
|
4243
|
+
}
|
|
4244
|
+
if (options?.validator) {
|
|
4245
|
+
return options.validator.parse(data, ctx);
|
|
4246
|
+
}
|
|
4247
|
+
return data;
|
|
4248
|
+
}
|
|
4249
|
+
};
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
// src/client/outbox/outbox.store.ts
|
|
4253
|
+
var InMemoryOutboxStore = class {
|
|
4254
|
+
/** Insertion-ordered rows. `published` flips to true after `markPublished`. */
|
|
4255
|
+
rows = [];
|
|
4256
|
+
/**
|
|
4257
|
+
* Append a message to the outbox. In a real store this INSERT would run inside
|
|
4258
|
+
* the same DB transaction as the corresponding business write.
|
|
4259
|
+
*/
|
|
4260
|
+
add(message) {
|
|
4261
|
+
this.rows.push({ message, published: false });
|
|
4262
|
+
}
|
|
4263
|
+
async fetchUnpublished(limit) {
|
|
4264
|
+
const out = [];
|
|
4265
|
+
for (const row of this.rows) {
|
|
4266
|
+
if (row.published) continue;
|
|
4267
|
+
out.push(row.message);
|
|
4268
|
+
if (out.length >= limit) break;
|
|
4269
|
+
}
|
|
4270
|
+
return out;
|
|
4271
|
+
}
|
|
4272
|
+
async markPublished(ids) {
|
|
4273
|
+
const idSet = new Set(ids);
|
|
4274
|
+
for (const row of this.rows) {
|
|
4275
|
+
if (idSet.has(row.message.id)) row.published = true;
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
/** Test helper: count of rows not yet marked published. */
|
|
4279
|
+
get pendingCount() {
|
|
4280
|
+
return this.rows.filter((r) => !r.published).length;
|
|
4281
|
+
}
|
|
4282
|
+
/** Test helper: count of rows marked published. */
|
|
4283
|
+
get publishedCount() {
|
|
4284
|
+
return this.rows.filter((r) => r.published).length;
|
|
4285
|
+
}
|
|
4286
|
+
};
|
|
4287
|
+
|
|
4288
|
+
// src/client/outbox/outbox.relay.ts
|
|
4289
|
+
function toError2(e) {
|
|
4290
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
4291
|
+
}
|
|
4292
|
+
function startOutboxRelay(kafka, store, options = {}) {
|
|
4293
|
+
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
4294
|
+
const batchSize = options.batchSize ?? 100;
|
|
4295
|
+
const onError = options.onError ?? ((error, batch) => {
|
|
4296
|
+
console.error(
|
|
4297
|
+
`[outbox] batch of ${batch.length} message(s) failed \u2014 will retry:`,
|
|
4298
|
+
error
|
|
4299
|
+
);
|
|
4300
|
+
});
|
|
4301
|
+
const onPublished = options.onPublished;
|
|
4302
|
+
let stopped = false;
|
|
4303
|
+
let running = false;
|
|
4304
|
+
let inFlight = Promise.resolve();
|
|
4305
|
+
const iterate = async () => {
|
|
4306
|
+
let batch = [];
|
|
4307
|
+
try {
|
|
4308
|
+
batch = await store.fetchUnpublished(batchSize);
|
|
4309
|
+
if (batch.length === 0) return;
|
|
4310
|
+
await kafka.transaction(async (tx) => {
|
|
4311
|
+
for (const msg of batch) {
|
|
4312
|
+
await tx.send(msg.topic, msg.payload, {
|
|
4313
|
+
key: msg.key,
|
|
4314
|
+
headers: msg.headers,
|
|
4315
|
+
correlationId: msg.correlationId,
|
|
4316
|
+
eventId: msg.eventId
|
|
4317
|
+
});
|
|
4318
|
+
}
|
|
4319
|
+
});
|
|
4320
|
+
await store.markPublished(batch.map((m) => m.id));
|
|
4321
|
+
onPublished?.(batch.length);
|
|
4322
|
+
} catch (err) {
|
|
4323
|
+
onError(toError2(err), batch);
|
|
4324
|
+
}
|
|
4325
|
+
};
|
|
4326
|
+
const tick = () => {
|
|
4327
|
+
if (stopped || running) return;
|
|
4328
|
+
running = true;
|
|
4329
|
+
inFlight = iterate().finally(() => {
|
|
4330
|
+
running = false;
|
|
4331
|
+
});
|
|
4332
|
+
};
|
|
4333
|
+
const timer = setInterval(tick, pollIntervalMs);
|
|
4334
|
+
timer.unref?.();
|
|
4335
|
+
return {
|
|
4336
|
+
stop: async () => {
|
|
4337
|
+
stopped = true;
|
|
4338
|
+
clearInterval(timer);
|
|
4339
|
+
await inFlight;
|
|
4340
|
+
}
|
|
4341
|
+
};
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
// src/client/security/providers.ts
|
|
4345
|
+
var defaultImport = (specifier) => import(specifier);
|
|
4346
|
+
function awsMskIamProvider(options) {
|
|
4347
|
+
const importFn = options.importFn ?? defaultImport;
|
|
4348
|
+
return async () => {
|
|
4349
|
+
let signer;
|
|
4350
|
+
try {
|
|
4351
|
+
signer = await importFn("aws-msk-iam-sasl-signer-js");
|
|
4352
|
+
} catch {
|
|
4353
|
+
throw new Error(
|
|
4354
|
+
"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."
|
|
4355
|
+
);
|
|
4356
|
+
}
|
|
4357
|
+
const { token, expiryTime } = await signer.generateAuthToken({
|
|
4358
|
+
region: options.region
|
|
4359
|
+
});
|
|
4360
|
+
return {
|
|
4361
|
+
value: token,
|
|
4362
|
+
principal: "msk-iam",
|
|
4363
|
+
// expiryTime is epoch ms per the signer's contract
|
|
4364
|
+
lifetimeMs: expiryTime
|
|
4365
|
+
};
|
|
4366
|
+
};
|
|
4367
|
+
}
|
|
4368
|
+
function gcpAccessTokenProvider(options = {}) {
|
|
4369
|
+
const importFn = options.importFn ?? defaultImport;
|
|
4370
|
+
const ttlMs = options.tokenTtlMs ?? 50 * 6e4;
|
|
4371
|
+
return async () => {
|
|
4372
|
+
let lib;
|
|
4373
|
+
try {
|
|
4374
|
+
lib = await importFn("google-auth-library");
|
|
4375
|
+
} catch {
|
|
4376
|
+
throw new Error(
|
|
4377
|
+
"gcpAccessTokenProvider: package 'google-auth-library' is not installed. Run `npm install google-auth-library` to enable GCP authentication."
|
|
4378
|
+
);
|
|
4379
|
+
}
|
|
4380
|
+
const auth = new lib.GoogleAuth({
|
|
4381
|
+
scopes: options.scopes ?? ["https://www.googleapis.com/auth/cloud-platform"]
|
|
4382
|
+
});
|
|
4383
|
+
const token = await auth.getAccessToken();
|
|
4384
|
+
if (!token) {
|
|
4385
|
+
throw new Error(
|
|
4386
|
+
"gcpAccessTokenProvider: google-auth-library returned no access token \u2014 check Application Default Credentials."
|
|
4387
|
+
);
|
|
4388
|
+
}
|
|
4389
|
+
return {
|
|
4390
|
+
value: token,
|
|
4391
|
+
principal: options.principal ?? "gcp",
|
|
4392
|
+
lifetimeMs: Date.now() + ttlMs
|
|
4393
|
+
};
|
|
4394
|
+
};
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
// src/client/security/acl.ts
|
|
4398
|
+
function addResource(out, r) {
|
|
4399
|
+
const key = `${r.resourceType}:${r.patternType}:${r.name}`;
|
|
4400
|
+
const existing = out.get(key);
|
|
4401
|
+
if (existing) {
|
|
4402
|
+
for (const op of r.operations)
|
|
4403
|
+
if (!existing.operations.includes(op)) existing.operations.push(op);
|
|
4404
|
+
if (!existing.reason.includes(r.reason))
|
|
4405
|
+
existing.reason += `; ${r.reason}`;
|
|
4406
|
+
} else {
|
|
4407
|
+
out.set(key, { ...r, operations: [...r.operations] });
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
function describeRequiredAcls(input) {
|
|
4411
|
+
const out = /* @__PURE__ */ new Map();
|
|
4412
|
+
const f = input.features ?? {};
|
|
4413
|
+
const produce = input.produceTopics ?? [];
|
|
4414
|
+
const consume = input.consumeTopics ?? [];
|
|
4415
|
+
const groups = input.groupIds ?? [];
|
|
4416
|
+
for (const t of produce) {
|
|
4417
|
+
addResource(out, {
|
|
4418
|
+
resourceType: "topic",
|
|
4419
|
+
patternType: "literal",
|
|
4420
|
+
name: t,
|
|
4421
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4422
|
+
reason: "sendMessage/sendBatch"
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
for (const t of consume) {
|
|
4426
|
+
addResource(out, {
|
|
4427
|
+
resourceType: "topic",
|
|
4428
|
+
patternType: "literal",
|
|
4429
|
+
name: t,
|
|
4430
|
+
operations: ["READ", "DESCRIBE"],
|
|
4431
|
+
reason: "startConsumer"
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
4434
|
+
for (const g of groups) {
|
|
4435
|
+
addResource(out, {
|
|
4436
|
+
resourceType: "group",
|
|
4437
|
+
patternType: "literal",
|
|
4438
|
+
name: g,
|
|
4439
|
+
operations: ["READ", "DESCRIBE"],
|
|
4440
|
+
reason: "consumer group membership + offset commits"
|
|
4441
|
+
});
|
|
4442
|
+
}
|
|
4443
|
+
if (f.dlq) {
|
|
4444
|
+
for (const t of consume) {
|
|
4445
|
+
addResource(out, {
|
|
4446
|
+
resourceType: "topic",
|
|
4447
|
+
patternType: "literal",
|
|
4448
|
+
name: `${t}.dlq`,
|
|
4449
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4450
|
+
reason: "dlq: true \u2014 failed messages routed to DLQ"
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
if (f.retryTopics) {
|
|
4455
|
+
for (const t of consume) {
|
|
4456
|
+
for (let level = 1; level <= f.retryTopics.maxRetries; level++) {
|
|
4457
|
+
addResource(out, {
|
|
4458
|
+
resourceType: "topic",
|
|
4459
|
+
patternType: "literal",
|
|
4460
|
+
name: `${t}.retry.${level}`,
|
|
4461
|
+
operations: ["READ", "WRITE", "DESCRIBE"],
|
|
4462
|
+
reason: "retryTopics \u2014 retry chain produce + companion consume"
|
|
4463
|
+
});
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
for (const g of groups) {
|
|
4467
|
+
addResource(out, {
|
|
4468
|
+
resourceType: "group",
|
|
4469
|
+
patternType: "prefixed",
|
|
4470
|
+
name: `${g}-retry.`,
|
|
4471
|
+
operations: ["READ", "DESCRIBE"],
|
|
4472
|
+
reason: "retryTopics \u2014 companion retry-level consumer groups"
|
|
4473
|
+
});
|
|
4474
|
+
addResource(out, {
|
|
4475
|
+
resourceType: "transactional-id",
|
|
4476
|
+
patternType: "prefixed",
|
|
4477
|
+
name: `${g}-`,
|
|
4478
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4479
|
+
reason: "retryTopics \u2014 EOS routing transactions per retry level"
|
|
4480
|
+
});
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
if (f.delayedDelivery) {
|
|
4484
|
+
for (const t of [.../* @__PURE__ */ new Set([...produce, ...consume])]) {
|
|
4485
|
+
addResource(out, {
|
|
4486
|
+
resourceType: "topic",
|
|
4487
|
+
patternType: "literal",
|
|
4488
|
+
name: `${t}.delayed`,
|
|
4489
|
+
operations: ["READ", "WRITE", "DESCRIBE"],
|
|
4490
|
+
reason: "deliverAfterMs staging + startDelayedRelay consume"
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
for (const g of groups) {
|
|
4494
|
+
addResource(out, {
|
|
4495
|
+
resourceType: "group",
|
|
4496
|
+
patternType: "literal",
|
|
4497
|
+
name: `${g}-delayed-relay`,
|
|
4498
|
+
operations: ["READ", "DESCRIBE"],
|
|
4499
|
+
reason: "startDelayedRelay consumer group"
|
|
4500
|
+
});
|
|
4501
|
+
addResource(out, {
|
|
4502
|
+
resourceType: "transactional-id",
|
|
4503
|
+
patternType: "literal",
|
|
4504
|
+
name: `${g}-delayed-relay-tx`,
|
|
4505
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4506
|
+
reason: "startDelayedRelay transactional forwarding"
|
|
4507
|
+
});
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
if (f.duplicatesTopic) {
|
|
4511
|
+
if (typeof f.duplicatesTopic === "string") {
|
|
4512
|
+
addResource(out, {
|
|
4513
|
+
resourceType: "topic",
|
|
4514
|
+
patternType: "literal",
|
|
4515
|
+
name: f.duplicatesTopic,
|
|
4516
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4517
|
+
reason: "deduplication.strategy 'topic' \u2014 custom duplicates topic"
|
|
4518
|
+
});
|
|
4519
|
+
} else {
|
|
4520
|
+
for (const t of consume) {
|
|
4521
|
+
addResource(out, {
|
|
4522
|
+
resourceType: "topic",
|
|
4523
|
+
patternType: "literal",
|
|
4524
|
+
name: `${t}.duplicates`,
|
|
4525
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4526
|
+
reason: "deduplication.strategy 'topic'"
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
if (f.dlqReplay) {
|
|
4532
|
+
for (const t of consume) {
|
|
4533
|
+
addResource(out, {
|
|
4534
|
+
resourceType: "group",
|
|
4535
|
+
patternType: "prefixed",
|
|
4536
|
+
name: `${t}.dlq-replay`,
|
|
4537
|
+
operations: ["READ", "DESCRIBE", "DELETE"],
|
|
4538
|
+
reason: "replayDlq \u2014 ephemeral/stable replay groups (deleted after use)"
|
|
4539
|
+
});
|
|
4540
|
+
addResource(out, {
|
|
4541
|
+
resourceType: "topic",
|
|
4542
|
+
patternType: "literal",
|
|
4543
|
+
name: `${t}.dlq`,
|
|
4544
|
+
operations: ["READ", "DESCRIBE"],
|
|
4545
|
+
reason: "replayDlq \u2014 reads the DLQ"
|
|
4546
|
+
});
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
if (f.snapshots) {
|
|
4550
|
+
addResource(out, {
|
|
4551
|
+
resourceType: "group",
|
|
4552
|
+
patternType: "prefixed",
|
|
4553
|
+
name: `${input.clientId}-snapshot-`,
|
|
4554
|
+
operations: ["READ", "DESCRIBE", "DELETE"],
|
|
4555
|
+
reason: "readSnapshot \u2014 timestamped ephemeral groups (deleted after use)"
|
|
4556
|
+
});
|
|
4557
|
+
}
|
|
4558
|
+
if (f.clockRecovery) {
|
|
4559
|
+
addResource(out, {
|
|
4560
|
+
resourceType: "group",
|
|
4561
|
+
patternType: "prefixed",
|
|
4562
|
+
name: `${input.clientId}-clock-recovery-`,
|
|
4563
|
+
operations: ["READ", "DESCRIBE", "DELETE"],
|
|
4564
|
+
reason: "clockRecovery \u2014 timestamped ephemeral groups (deleted after use)"
|
|
4565
|
+
});
|
|
4566
|
+
}
|
|
4567
|
+
if (f.transactions) {
|
|
4568
|
+
addResource(out, {
|
|
4569
|
+
resourceType: "transactional-id",
|
|
4570
|
+
patternType: "literal",
|
|
4571
|
+
name: `${input.clientId}-tx`,
|
|
4572
|
+
operations: ["WRITE", "DESCRIBE"],
|
|
4573
|
+
reason: "transaction() \u2014 default transactionalId (override-aware: adjust if you set one)"
|
|
4574
|
+
});
|
|
4575
|
+
}
|
|
4576
|
+
if (f.autoCreateTopics) {
|
|
4577
|
+
addResource(out, {
|
|
4578
|
+
resourceType: "cluster",
|
|
4579
|
+
patternType: "literal",
|
|
4580
|
+
name: "kafka-cluster",
|
|
4581
|
+
operations: ["CREATE"],
|
|
4582
|
+
reason: "autoCreateTopics: true \u2014 not recommended in production"
|
|
4583
|
+
});
|
|
4584
|
+
}
|
|
4585
|
+
return [...out.values()];
|
|
4586
|
+
}
|
|
4587
|
+
function toKafkaAclCommands(resources, principal, bootstrapServer = "<bootstrap-server>") {
|
|
4588
|
+
return resources.map((r) => {
|
|
4589
|
+
const ops = r.operations.map((o) => `--operation ${o}`).join(" ");
|
|
4590
|
+
const resourceFlag = r.resourceType === "topic" ? `--topic '${r.name}'` : r.resourceType === "group" ? `--group '${r.name}'` : r.resourceType === "transactional-id" ? `--transactional-id '${r.name}'` : "--cluster";
|
|
4591
|
+
const pattern = r.patternType === "prefixed" ? " --resource-pattern-type prefixed" : "";
|
|
4592
|
+
return `kafka-acls.sh --bootstrap-server ${bootstrapServer} --add --allow-principal '${principal}' ${ops} ${resourceFlag}${pattern} # ${r.reason}`;
|
|
4593
|
+
});
|
|
4594
|
+
}
|
|
4595
|
+
var MSK_TOPIC_ACTIONS = {
|
|
4596
|
+
READ: ["kafka-cluster:ReadData", "kafka-cluster:DescribeTopic"],
|
|
4597
|
+
WRITE: ["kafka-cluster:WriteData", "kafka-cluster:DescribeTopic"],
|
|
4598
|
+
DESCRIBE: ["kafka-cluster:DescribeTopic"],
|
|
4599
|
+
CREATE: ["kafka-cluster:CreateTopic"],
|
|
4600
|
+
DELETE: ["kafka-cluster:DeleteTopic"]
|
|
4601
|
+
};
|
|
4602
|
+
var MSK_GROUP_ACTIONS = {
|
|
4603
|
+
READ: ["kafka-cluster:AlterGroup", "kafka-cluster:DescribeGroup"],
|
|
4604
|
+
DESCRIBE: ["kafka-cluster:DescribeGroup"],
|
|
4605
|
+
DELETE: ["kafka-cluster:DeleteGroup"]
|
|
4606
|
+
};
|
|
4607
|
+
var MSK_TX_ACTIONS = {
|
|
4608
|
+
WRITE: [
|
|
4609
|
+
"kafka-cluster:AlterTransactionalId",
|
|
4610
|
+
"kafka-cluster:DescribeTransactionalId"
|
|
4611
|
+
],
|
|
4612
|
+
DESCRIBE: ["kafka-cluster:DescribeTransactionalId"]
|
|
4613
|
+
};
|
|
4614
|
+
function toMskIamPolicy(resources, cluster) {
|
|
4615
|
+
const { region, accountId, clusterName, clusterUuid } = cluster;
|
|
4616
|
+
const arn = (type, name) => `arn:aws:kafka:${region}:${accountId}:${type}/${clusterName}/${clusterUuid}/${name}`;
|
|
4617
|
+
const statements = [
|
|
4618
|
+
{
|
|
4619
|
+
Sid: "Connect",
|
|
4620
|
+
Effect: "Allow",
|
|
4621
|
+
Action: ["kafka-cluster:Connect"],
|
|
4622
|
+
Resource: [
|
|
4623
|
+
`arn:aws:kafka:${region}:${accountId}:cluster/${clusterName}/${clusterUuid}`
|
|
4624
|
+
]
|
|
4625
|
+
}
|
|
4626
|
+
];
|
|
4627
|
+
let sid = 0;
|
|
4628
|
+
for (const r of resources) {
|
|
4629
|
+
const suffix = r.patternType === "prefixed" ? `${r.name}*` : r.name;
|
|
4630
|
+
let actions = [];
|
|
4631
|
+
let resource;
|
|
4632
|
+
if (r.resourceType === "topic") {
|
|
4633
|
+
actions = [...new Set(r.operations.flatMap((o) => MSK_TOPIC_ACTIONS[o] ?? []))];
|
|
4634
|
+
resource = arn("topic", suffix);
|
|
4635
|
+
} else if (r.resourceType === "group") {
|
|
4636
|
+
actions = [...new Set(r.operations.flatMap((o) => MSK_GROUP_ACTIONS[o] ?? []))];
|
|
4637
|
+
resource = arn("group", suffix);
|
|
4638
|
+
} else if (r.resourceType === "transactional-id") {
|
|
4639
|
+
actions = [...new Set(r.operations.flatMap((o) => MSK_TX_ACTIONS[o] ?? []))];
|
|
4640
|
+
resource = arn("transactional-id", suffix);
|
|
4641
|
+
} else {
|
|
4642
|
+
actions = ["kafka-cluster:CreateTopic"];
|
|
4643
|
+
resource = `arn:aws:kafka:${region}:${accountId}:topic/${clusterName}/${clusterUuid}/*`;
|
|
4644
|
+
}
|
|
4645
|
+
if (actions.length === 0 || !resource) continue;
|
|
4646
|
+
statements.push({
|
|
4647
|
+
Sid: `Acl${sid++}`,
|
|
4648
|
+
Effect: "Allow",
|
|
4649
|
+
Action: actions,
|
|
4650
|
+
Resource: [resource]
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
return { Version: "2012-10-17", Statement: statements };
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
// src/client/config/from-env.ts
|
|
4657
|
+
var TRUE_VALUES = /* @__PURE__ */ new Set(["true", "1", "yes"]);
|
|
4658
|
+
var FALSE_VALUES = /* @__PURE__ */ new Set(["false", "0", "no"]);
|
|
4659
|
+
function parseBool(name, raw) {
|
|
4660
|
+
const normalized = raw.trim().toLowerCase();
|
|
4661
|
+
if (TRUE_VALUES.has(normalized)) return true;
|
|
4662
|
+
if (FALSE_VALUES.has(normalized)) return false;
|
|
4663
|
+
throw new Error(
|
|
4664
|
+
`Invalid boolean for ${name}: "${raw}". Use one of true/false, 1/0, yes/no (case-insensitive).`
|
|
4665
|
+
);
|
|
4666
|
+
}
|
|
4667
|
+
function parseNum(name, raw) {
|
|
4668
|
+
const value = Number(raw.trim());
|
|
4669
|
+
if (Number.isNaN(value)) {
|
|
4670
|
+
throw new Error(`Invalid number for ${name}: "${raw}".`);
|
|
4671
|
+
}
|
|
4672
|
+
return value;
|
|
4673
|
+
}
|
|
4674
|
+
function parseList(raw) {
|
|
4675
|
+
return raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
4676
|
+
}
|
|
4677
|
+
function parseEnum(name, raw, allowed) {
|
|
4678
|
+
const value = raw.trim();
|
|
4679
|
+
if (!allowed.includes(value)) {
|
|
4680
|
+
throw new Error(
|
|
4681
|
+
`Invalid value for ${name}: "${raw}". Allowed: ${allowed.join(", ")}.`
|
|
4682
|
+
);
|
|
4683
|
+
}
|
|
4684
|
+
return value;
|
|
4685
|
+
}
|
|
4686
|
+
function readVar(env, key, apply) {
|
|
4687
|
+
const raw = env[key];
|
|
4688
|
+
if (raw === void 0 || raw.trim() === "") return;
|
|
4689
|
+
apply(raw);
|
|
4690
|
+
}
|
|
4691
|
+
function kafkaClientConfigFromEnv(env = process.env, prefix = "KAFKA_") {
|
|
4692
|
+
const options = {};
|
|
4693
|
+
const result = { options };
|
|
4694
|
+
readVar(env, `${prefix}CLIENT_ID`, (raw) => {
|
|
4695
|
+
result.clientId = raw.trim();
|
|
4696
|
+
});
|
|
4697
|
+
readVar(env, `${prefix}GROUP_ID`, (raw) => {
|
|
4698
|
+
result.groupId = raw.trim();
|
|
4699
|
+
});
|
|
4700
|
+
readVar(env, `${prefix}BROKERS`, (raw) => {
|
|
4701
|
+
result.brokers = parseList(raw);
|
|
4702
|
+
});
|
|
4703
|
+
readVar(env, `${prefix}AUTO_CREATE_TOPICS`, (raw) => {
|
|
4704
|
+
options.autoCreateTopics = parseBool(`${prefix}AUTO_CREATE_TOPICS`, raw);
|
|
4705
|
+
});
|
|
4706
|
+
readVar(env, `${prefix}STRICT_SCHEMAS`, (raw) => {
|
|
4707
|
+
options.strictSchemas = parseBool(`${prefix}STRICT_SCHEMAS`, raw);
|
|
4708
|
+
});
|
|
4709
|
+
readVar(env, `${prefix}NUM_PARTITIONS`, (raw) => {
|
|
4710
|
+
options.numPartitions = parseNum(`${prefix}NUM_PARTITIONS`, raw);
|
|
4711
|
+
});
|
|
4712
|
+
readVar(env, `${prefix}TRANSACTIONAL_ID`, (raw) => {
|
|
4713
|
+
options.transactionalId = raw.trim();
|
|
4714
|
+
});
|
|
4715
|
+
readVar(env, `${prefix}CLOCK_RECOVERY_TOPICS`, (raw) => {
|
|
4716
|
+
const topics = parseList(raw);
|
|
4717
|
+
if (topics.length === 0) return;
|
|
4718
|
+
options.clockRecovery = { topics };
|
|
4719
|
+
});
|
|
4720
|
+
readVar(env, `${prefix}CLOCK_RECOVERY_TIMEOUT_MS`, (raw) => {
|
|
4721
|
+
const timeoutMs = parseNum(`${prefix}CLOCK_RECOVERY_TIMEOUT_MS`, raw);
|
|
4722
|
+
if (options.clockRecovery) {
|
|
4723
|
+
options.clockRecovery.timeoutMs = timeoutMs;
|
|
4724
|
+
}
|
|
4725
|
+
});
|
|
4726
|
+
readVar(env, `${prefix}LAG_THROTTLE_MAX_LAG`, (raw) => {
|
|
4727
|
+
options.lagThrottle = {
|
|
4728
|
+
maxLag: parseNum(`${prefix}LAG_THROTTLE_MAX_LAG`, raw)
|
|
4729
|
+
};
|
|
4730
|
+
});
|
|
4731
|
+
readVar(env, `${prefix}LAG_THROTTLE_GROUP_ID`, (raw) => {
|
|
4732
|
+
if (options.lagThrottle) options.lagThrottle.groupId = raw.trim();
|
|
4733
|
+
});
|
|
4734
|
+
readVar(env, `${prefix}LAG_THROTTLE_POLL_INTERVAL_MS`, (raw) => {
|
|
4735
|
+
if (options.lagThrottle) {
|
|
4736
|
+
options.lagThrottle.pollIntervalMs = parseNum(
|
|
4737
|
+
`${prefix}LAG_THROTTLE_POLL_INTERVAL_MS`,
|
|
4738
|
+
raw
|
|
4739
|
+
);
|
|
4740
|
+
}
|
|
4741
|
+
});
|
|
4742
|
+
readVar(env, `${prefix}LAG_THROTTLE_MAX_WAIT_MS`, (raw) => {
|
|
4743
|
+
if (options.lagThrottle) {
|
|
4744
|
+
options.lagThrottle.maxWaitMs = parseNum(
|
|
4745
|
+
`${prefix}LAG_THROTTLE_MAX_WAIT_MS`,
|
|
4746
|
+
raw
|
|
4747
|
+
);
|
|
4748
|
+
}
|
|
4749
|
+
});
|
|
4750
|
+
const security = securityFromEnv(env, prefix);
|
|
4751
|
+
if (security) options.security = security;
|
|
4752
|
+
return result;
|
|
4753
|
+
}
|
|
4754
|
+
function securityFromEnv(env, prefix) {
|
|
4755
|
+
let ssl;
|
|
4756
|
+
let allowInsecure;
|
|
4757
|
+
let mechanism;
|
|
4758
|
+
let username;
|
|
4759
|
+
let password;
|
|
4760
|
+
readVar(env, `${prefix}SSL`, (raw) => {
|
|
4761
|
+
ssl = parseBool(`${prefix}SSL`, raw);
|
|
4762
|
+
});
|
|
4763
|
+
readVar(env, `${prefix}ALLOW_INSECURE`, (raw) => {
|
|
4764
|
+
allowInsecure = parseBool(`${prefix}ALLOW_INSECURE`, raw);
|
|
4765
|
+
});
|
|
4766
|
+
readVar(env, `${prefix}SASL_MECHANISM`, (raw) => {
|
|
4767
|
+
mechanism = parseEnum(`${prefix}SASL_MECHANISM`, raw, [
|
|
4768
|
+
"plain",
|
|
4769
|
+
"scram-sha-256",
|
|
4770
|
+
"scram-sha-512"
|
|
4771
|
+
]);
|
|
4772
|
+
});
|
|
4773
|
+
readVar(env, `${prefix}SASL_USERNAME`, (raw) => {
|
|
4774
|
+
username = raw.trim();
|
|
4775
|
+
});
|
|
4776
|
+
readVar(env, `${prefix}SASL_PASSWORD`, (raw) => {
|
|
4777
|
+
password = raw;
|
|
4778
|
+
});
|
|
4779
|
+
if (ssl === void 0 && allowInsecure === void 0 && mechanism === void 0 && username === void 0 && password === void 0) {
|
|
4780
|
+
return void 0;
|
|
4781
|
+
}
|
|
4782
|
+
const security = {};
|
|
4783
|
+
if (ssl !== void 0) security.ssl = ssl;
|
|
4784
|
+
if (allowInsecure !== void 0) security.allowInsecure = allowInsecure;
|
|
4785
|
+
if (mechanism !== void 0 || username !== void 0 || password !== void 0) {
|
|
4786
|
+
if (mechanism === void 0 || username === void 0 || password === void 0) {
|
|
4787
|
+
throw new Error(
|
|
4788
|
+
`Incomplete SASL configuration: ${prefix}SASL_MECHANISM, ${prefix}SASL_USERNAME, and ${prefix}SASL_PASSWORD must all be set together (oauthbearer must be configured in code).`
|
|
4789
|
+
);
|
|
4790
|
+
}
|
|
4791
|
+
const sasl = { mechanism, username, password };
|
|
4792
|
+
security.sasl = sasl;
|
|
4793
|
+
}
|
|
4794
|
+
return security;
|
|
4795
|
+
}
|
|
4796
|
+
function consumerOptionsFromEnv(env = process.env, prefix = "KAFKA_CONSUMER_") {
|
|
4797
|
+
const options = {};
|
|
4798
|
+
readVar(env, `${prefix}GROUP_ID`, (raw) => {
|
|
4799
|
+
options.groupId = raw.trim();
|
|
4800
|
+
});
|
|
4801
|
+
readVar(env, `${prefix}FROM_BEGINNING`, (raw) => {
|
|
4802
|
+
options.fromBeginning = parseBool(`${prefix}FROM_BEGINNING`, raw);
|
|
4803
|
+
});
|
|
4804
|
+
readVar(env, `${prefix}AUTO_COMMIT`, (raw) => {
|
|
4805
|
+
options.autoCommit = parseBool(`${prefix}AUTO_COMMIT`, raw);
|
|
4806
|
+
});
|
|
4807
|
+
readVar(env, `${prefix}DLQ`, (raw) => {
|
|
4808
|
+
options.dlq = parseBool(`${prefix}DLQ`, raw);
|
|
4809
|
+
});
|
|
4810
|
+
readVar(env, `${prefix}RETRY_MAX_RETRIES`, (raw) => {
|
|
4811
|
+
const retry = {
|
|
4812
|
+
maxRetries: parseNum(`${prefix}RETRY_MAX_RETRIES`, raw)
|
|
4813
|
+
};
|
|
4814
|
+
options.retry = retry;
|
|
4815
|
+
});
|
|
4816
|
+
readVar(env, `${prefix}RETRY_BACKOFF_MS`, (raw) => {
|
|
4817
|
+
if (options.retry) {
|
|
4818
|
+
options.retry.backoffMs = parseNum(`${prefix}RETRY_BACKOFF_MS`, raw);
|
|
4819
|
+
}
|
|
4820
|
+
});
|
|
4821
|
+
readVar(env, `${prefix}RETRY_MAX_BACKOFF_MS`, (raw) => {
|
|
4822
|
+
if (options.retry) {
|
|
4823
|
+
options.retry.maxBackoffMs = parseNum(`${prefix}RETRY_MAX_BACKOFF_MS`, raw);
|
|
4824
|
+
}
|
|
4825
|
+
});
|
|
4826
|
+
readVar(env, `${prefix}RETRY_TOPICS`, (raw) => {
|
|
4827
|
+
options.retryTopics = parseBool(`${prefix}RETRY_TOPICS`, raw);
|
|
4828
|
+
});
|
|
4829
|
+
readVar(env, `${prefix}RETRY_TOPIC_ASSIGNMENT_TIMEOUT_MS`, (raw) => {
|
|
4830
|
+
options.retryTopicAssignmentTimeoutMs = parseNum(
|
|
4831
|
+
`${prefix}RETRY_TOPIC_ASSIGNMENT_TIMEOUT_MS`,
|
|
4832
|
+
raw
|
|
4833
|
+
);
|
|
4834
|
+
});
|
|
4835
|
+
readVar(env, `${prefix}HANDLER_TIMEOUT_MS`, (raw) => {
|
|
4836
|
+
options.handlerTimeoutMs = parseNum(`${prefix}HANDLER_TIMEOUT_MS`, raw);
|
|
4837
|
+
});
|
|
4838
|
+
readVar(env, `${prefix}MESSAGE_TTL_MS`, (raw) => {
|
|
4839
|
+
options.messageTtlMs = parseNum(`${prefix}MESSAGE_TTL_MS`, raw);
|
|
4840
|
+
});
|
|
4841
|
+
readVar(env, `${prefix}DEDUPLICATION_STRATEGY`, (raw) => {
|
|
4842
|
+
const strategy = parseEnum(`${prefix}DEDUPLICATION_STRATEGY`, raw, [
|
|
4843
|
+
"drop",
|
|
4844
|
+
"dlq",
|
|
4845
|
+
"topic"
|
|
4846
|
+
]);
|
|
4847
|
+
const dedup = { strategy };
|
|
4848
|
+
options.deduplication = dedup;
|
|
4849
|
+
});
|
|
4850
|
+
readVar(env, `${prefix}DEDUPLICATION_TOPIC`, (raw) => {
|
|
4851
|
+
if (options.deduplication) {
|
|
4852
|
+
options.deduplication.duplicatesTopic = raw.trim();
|
|
4853
|
+
}
|
|
4854
|
+
});
|
|
4855
|
+
readVar(env, `${prefix}CIRCUIT_BREAKER_THRESHOLD`, (raw) => {
|
|
4856
|
+
const cb = {
|
|
4857
|
+
threshold: parseNum(`${prefix}CIRCUIT_BREAKER_THRESHOLD`, raw)
|
|
4858
|
+
};
|
|
4859
|
+
options.circuitBreaker = cb;
|
|
4860
|
+
});
|
|
4861
|
+
readVar(env, `${prefix}CIRCUIT_BREAKER_RECOVERY_MS`, (raw) => {
|
|
4862
|
+
if (options.circuitBreaker) {
|
|
4863
|
+
options.circuitBreaker.recoveryMs = parseNum(
|
|
4864
|
+
`${prefix}CIRCUIT_BREAKER_RECOVERY_MS`,
|
|
4865
|
+
raw
|
|
4866
|
+
);
|
|
4867
|
+
}
|
|
4868
|
+
});
|
|
4869
|
+
readVar(env, `${prefix}CIRCUIT_BREAKER_WINDOW_SIZE`, (raw) => {
|
|
4870
|
+
if (options.circuitBreaker) {
|
|
4871
|
+
options.circuitBreaker.windowSize = parseNum(
|
|
4872
|
+
`${prefix}CIRCUIT_BREAKER_WINDOW_SIZE`,
|
|
4873
|
+
raw
|
|
4874
|
+
);
|
|
4875
|
+
}
|
|
4876
|
+
});
|
|
4877
|
+
readVar(env, `${prefix}CIRCUIT_BREAKER_HALF_OPEN_SUCCESSES`, (raw) => {
|
|
4878
|
+
if (options.circuitBreaker) {
|
|
4879
|
+
options.circuitBreaker.halfOpenSuccesses = parseNum(
|
|
4880
|
+
`${prefix}CIRCUIT_BREAKER_HALF_OPEN_SUCCESSES`,
|
|
4881
|
+
raw
|
|
4882
|
+
);
|
|
4883
|
+
}
|
|
4884
|
+
});
|
|
4885
|
+
readVar(env, `${prefix}QUEUE_HIGH_WATER_MARK`, (raw) => {
|
|
4886
|
+
options.queueHighWaterMark = parseNum(`${prefix}QUEUE_HIGH_WATER_MARK`, raw);
|
|
4887
|
+
});
|
|
4888
|
+
readVar(env, `${prefix}PARTITION_ASSIGNER`, (raw) => {
|
|
4889
|
+
options.partitionAssigner = parseEnum(`${prefix}PARTITION_ASSIGNER`, raw, [
|
|
4890
|
+
"roundrobin",
|
|
4891
|
+
"range",
|
|
4892
|
+
"cooperative-sticky"
|
|
4893
|
+
]);
|
|
4894
|
+
});
|
|
4895
|
+
readVar(env, `${prefix}GROUP_INSTANCE_ID`, (raw) => {
|
|
4896
|
+
options.groupInstanceId = raw.trim();
|
|
4897
|
+
});
|
|
4898
|
+
readVar(env, `${prefix}SUBSCRIBE_RETRY_RETRIES`, (raw) => {
|
|
4899
|
+
const subscribeRetry = {
|
|
4900
|
+
retries: parseNum(`${prefix}SUBSCRIBE_RETRY_RETRIES`, raw)
|
|
4901
|
+
};
|
|
4902
|
+
options.subscribeRetry = subscribeRetry;
|
|
4903
|
+
});
|
|
4904
|
+
readVar(env, `${prefix}SUBSCRIBE_RETRY_DELAY_MS`, (raw) => {
|
|
4905
|
+
if (options.subscribeRetry) {
|
|
4906
|
+
options.subscribeRetry.backoffMs = parseNum(
|
|
4907
|
+
`${prefix}SUBSCRIBE_RETRY_DELAY_MS`,
|
|
4908
|
+
raw
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
});
|
|
4912
|
+
return options;
|
|
4913
|
+
}
|
|
4914
|
+
var NESTED_CONSUMER_KEYS = [
|
|
4915
|
+
"retry",
|
|
4916
|
+
"deduplication",
|
|
4917
|
+
"circuitBreaker",
|
|
4918
|
+
"subscribeRetry"
|
|
4919
|
+
];
|
|
4920
|
+
function isPlainObject(value) {
|
|
4921
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4922
|
+
}
|
|
4923
|
+
function mergeConsumerOptions(...layers) {
|
|
4924
|
+
const result = {};
|
|
4925
|
+
for (const layer of layers) {
|
|
4926
|
+
if (!layer) continue;
|
|
4927
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
4928
|
+
if (value === void 0) continue;
|
|
4929
|
+
if (NESTED_CONSUMER_KEYS.includes(key) && isPlainObject(value) && isPlainObject(result[key])) {
|
|
4930
|
+
result[key] = {
|
|
4931
|
+
...result[key],
|
|
4932
|
+
...value
|
|
4933
|
+
};
|
|
4934
|
+
} else {
|
|
4935
|
+
result[key] = value;
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
return result;
|
|
4940
|
+
}
|
|
3618
4941
|
|
|
3619
4942
|
// src/nest/kafka.module.ts
|
|
3620
4943
|
var import_common3 = require("@nestjs/common");
|
|
@@ -3663,11 +4986,14 @@ var SubscribeTo = (topics, options) => {
|
|
|
3663
4986
|
};
|
|
3664
4987
|
|
|
3665
4988
|
// src/nest/kafka.explorer.ts
|
|
4989
|
+
var wiredSubscriptions = /* @__PURE__ */ new WeakMap();
|
|
3666
4990
|
var KafkaExplorer = class {
|
|
3667
4991
|
constructor(discoveryService, moduleRef) {
|
|
3668
4992
|
this.discoveryService = discoveryService;
|
|
3669
4993
|
this.moduleRef = moduleRef;
|
|
3670
4994
|
}
|
|
4995
|
+
discoveryService;
|
|
4996
|
+
moduleRef;
|
|
3671
4997
|
logger = new import_common2.Logger(KafkaExplorer.name);
|
|
3672
4998
|
/**
|
|
3673
4999
|
* Scan all NestJS providers for `@SubscribeTo()` metadata and wire each decorated
|
|
@@ -3687,6 +5013,14 @@ var KafkaExplorer = class {
|
|
|
3687
5013
|
if (!metadata || metadata.length === 0) continue;
|
|
3688
5014
|
for (const entry of metadata) {
|
|
3689
5015
|
const token = getKafkaClientToken(entry.clientName);
|
|
5016
|
+
const entryKey = `${token}:${String(entry.methodName)}`;
|
|
5017
|
+
let wired = wiredSubscriptions.get(instance);
|
|
5018
|
+
if (!wired) {
|
|
5019
|
+
wired = /* @__PURE__ */ new Set();
|
|
5020
|
+
wiredSubscriptions.set(instance, wired);
|
|
5021
|
+
}
|
|
5022
|
+
if (wired.has(entryKey)) continue;
|
|
5023
|
+
wired.add(entryKey);
|
|
3690
5024
|
let client;
|
|
3691
5025
|
try {
|
|
3692
5026
|
client = this.moduleRef.get(token, { strict: false });
|
|
@@ -3776,6 +5110,12 @@ var KafkaModule = class {
|
|
|
3776
5110
|
instrumentation: options.instrumentation,
|
|
3777
5111
|
onMessageLost: options.onMessageLost,
|
|
3778
5112
|
onRebalance: options.onRebalance,
|
|
5113
|
+
transactionalId: options.transactionalId,
|
|
5114
|
+
clockRecovery: options.clockRecovery,
|
|
5115
|
+
lagThrottle: options.lagThrottle,
|
|
5116
|
+
onTtlExpired: options.onTtlExpired,
|
|
5117
|
+
transport: options.transport,
|
|
5118
|
+
security: options.security,
|
|
3779
5119
|
logger: new import_common3.Logger(`KafkaClient:${options.clientId}`)
|
|
3780
5120
|
}
|
|
3781
5121
|
);
|
|
@@ -3799,13 +5139,19 @@ KafkaHealthIndicator = __decorateClass([
|
|
|
3799
5139
|
], KafkaHealthIndicator);
|
|
3800
5140
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3801
5141
|
0 && (module.exports = {
|
|
5142
|
+
ConfluentTransport,
|
|
3802
5143
|
HEADER_CORRELATION_ID,
|
|
5144
|
+
HEADER_DELAYED_TARGET,
|
|
5145
|
+
HEADER_DELAYED_UNTIL,
|
|
3803
5146
|
HEADER_EVENT_ID,
|
|
3804
5147
|
HEADER_LAMPORT_CLOCK,
|
|
3805
5148
|
HEADER_SCHEMA_VERSION,
|
|
3806
5149
|
HEADER_TIMESTAMP,
|
|
3807
5150
|
HEADER_TRACEPARENT,
|
|
5151
|
+
InMemoryDedupStore,
|
|
5152
|
+
InMemoryOutboxStore,
|
|
3808
5153
|
InjectKafkaClient,
|
|
5154
|
+
JsonSerde,
|
|
3809
5155
|
KAFKA_CLIENT,
|
|
3810
5156
|
KAFKA_SUBSCRIBER_METADATA,
|
|
3811
5157
|
KafkaClient,
|
|
@@ -3815,13 +5161,27 @@ KafkaHealthIndicator = __decorateClass([
|
|
|
3815
5161
|
KafkaProcessingError,
|
|
3816
5162
|
KafkaRetryExhaustedError,
|
|
3817
5163
|
KafkaValidationError,
|
|
5164
|
+
SchemaRegistryClient,
|
|
3818
5165
|
SubscribeTo,
|
|
5166
|
+
awsMskIamProvider,
|
|
3819
5167
|
buildEnvelopeHeaders,
|
|
5168
|
+
consumerOptionsFromEnv,
|
|
3820
5169
|
decodeHeaders,
|
|
5170
|
+
describeRequiredAcls,
|
|
3821
5171
|
extractEnvelope,
|
|
5172
|
+
gcpAccessTokenProvider,
|
|
3822
5173
|
getEnvelopeContext,
|
|
3823
5174
|
getKafkaClientToken,
|
|
5175
|
+
kafkaClientConfigFromEnv,
|
|
5176
|
+
mergeConsumerOptions,
|
|
5177
|
+
registrySchema,
|
|
5178
|
+
resolveSecurityOptions,
|
|
3824
5179
|
runWithEnvelopeContext,
|
|
3825
|
-
|
|
5180
|
+
startOutboxRelay,
|
|
5181
|
+
toError,
|
|
5182
|
+
toKafkaAclCommands,
|
|
5183
|
+
toMskIamPolicy,
|
|
5184
|
+
topic,
|
|
5185
|
+
versionedSchema
|
|
3826
5186
|
});
|
|
3827
5187
|
//# sourceMappingURL=index.js.map
|