@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.
Files changed (180) hide show
  1. package/README.md +693 -8
  2. package/dist/chunk-OR7TPAAE.mjs +4760 -0
  3. package/dist/chunk-OR7TPAAE.mjs.map +1 -0
  4. package/dist/chunk-PQVBRDNV.mjs +149 -0
  5. package/dist/chunk-PQVBRDNV.mjs.map +1 -0
  6. package/dist/cli/dlq.d.ts +119 -0
  7. package/dist/cli/dlq.d.ts.map +1 -0
  8. package/dist/cli/index.d.ts +3 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/{chunk-SM4FZKAZ.mjs → cli/index.js} +1073 -309
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/cli/index.mjs +356 -0
  13. package/dist/cli/index.mjs.map +1 -0
  14. package/dist/client/config/from-env.d.ts +188 -0
  15. package/dist/client/config/from-env.d.ts.map +1 -0
  16. package/dist/client/config/index.d.ts +2 -0
  17. package/dist/client/config/index.d.ts.map +1 -0
  18. package/dist/client/errors.d.ts +67 -0
  19. package/dist/client/errors.d.ts.map +1 -0
  20. package/dist/client/kafka.client/admin/ops.d.ts +114 -0
  21. package/dist/client/kafka.client/admin/ops.d.ts.map +1 -0
  22. package/dist/client/kafka.client/consumer/features/delayed.d.ts +24 -0
  23. package/dist/client/kafka.client/consumer/features/delayed.d.ts.map +1 -0
  24. package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts +52 -0
  25. package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts.map +1 -0
  26. package/dist/client/kafka.client/consumer/features/routed.d.ts +4 -0
  27. package/dist/client/kafka.client/consumer/features/routed.d.ts.map +1 -0
  28. package/dist/client/kafka.client/consumer/features/snapshot.d.ts +10 -0
  29. package/dist/client/kafka.client/consumer/features/snapshot.d.ts.map +1 -0
  30. package/dist/client/kafka.client/consumer/features/window.d.ts +5 -0
  31. package/dist/client/kafka.client/consumer/features/window.d.ts.map +1 -0
  32. package/dist/client/kafka.client/consumer/handler.d.ts +163 -0
  33. package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -0
  34. package/dist/client/kafka.client/consumer/ops.d.ts +64 -0
  35. package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -0
  36. package/dist/client/kafka.client/consumer/pipeline.d.ts +168 -0
  37. package/dist/client/kafka.client/consumer/pipeline.d.ts.map +1 -0
  38. package/dist/client/kafka.client/consumer/queue.d.ts +37 -0
  39. package/dist/client/kafka.client/consumer/queue.d.ts.map +1 -0
  40. package/dist/client/kafka.client/consumer/retry-topic.d.ts +68 -0
  41. package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -0
  42. package/dist/client/kafka.client/consumer/setup.d.ts +66 -0
  43. package/dist/client/kafka.client/consumer/setup.d.ts.map +1 -0
  44. package/dist/client/kafka.client/consumer/start.d.ts +7 -0
  45. package/dist/client/kafka.client/consumer/start.d.ts.map +1 -0
  46. package/dist/client/kafka.client/consumer/stop.d.ts +19 -0
  47. package/dist/client/kafka.client/consumer/stop.d.ts.map +1 -0
  48. package/dist/client/kafka.client/consumer/subscribe-retry.d.ts +4 -0
  49. package/dist/client/kafka.client/consumer/subscribe-retry.d.ts.map +1 -0
  50. package/dist/client/kafka.client/context.d.ts +75 -0
  51. package/dist/client/kafka.client/context.d.ts.map +1 -0
  52. package/dist/client/kafka.client/index.d.ts +155 -0
  53. package/dist/client/kafka.client/index.d.ts.map +1 -0
  54. package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts +61 -0
  55. package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts.map +1 -0
  56. package/dist/client/kafka.client/infra/dedup.store.d.ts +28 -0
  57. package/dist/client/kafka.client/infra/dedup.store.d.ts.map +1 -0
  58. package/dist/client/kafka.client/infra/inflight.tracker.d.ts +22 -0
  59. package/dist/client/kafka.client/infra/inflight.tracker.d.ts.map +1 -0
  60. package/dist/client/kafka.client/infra/metrics.manager.d.ts +67 -0
  61. package/dist/client/kafka.client/infra/metrics.manager.d.ts.map +1 -0
  62. package/dist/client/kafka.client/producer/lifecycle.d.ts +41 -0
  63. package/dist/client/kafka.client/producer/lifecycle.d.ts.map +1 -0
  64. package/dist/client/kafka.client/producer/ops.d.ts +79 -0
  65. package/dist/client/kafka.client/producer/ops.d.ts.map +1 -0
  66. package/dist/client/kafka.client/producer/send.d.ts +21 -0
  67. package/dist/client/kafka.client/producer/send.d.ts.map +1 -0
  68. package/dist/client/kafka.client/validate-options.d.ts +11 -0
  69. package/dist/client/kafka.client/validate-options.d.ts.map +1 -0
  70. package/dist/client/message/envelope.d.ts +105 -0
  71. package/dist/client/message/envelope.d.ts.map +1 -0
  72. package/dist/client/message/schema-registry.d.ts +124 -0
  73. package/dist/client/message/schema-registry.d.ts.map +1 -0
  74. package/dist/client/message/serde.d.ts +68 -0
  75. package/dist/client/message/serde.d.ts.map +1 -0
  76. package/dist/client/message/topic.d.ts +159 -0
  77. package/dist/client/message/topic.d.ts.map +1 -0
  78. package/dist/client/message/versioned-schema.d.ts +53 -0
  79. package/dist/client/message/versioned-schema.d.ts.map +1 -0
  80. package/dist/client/outbox/index.d.ts +4 -0
  81. package/dist/client/outbox/index.d.ts.map +1 -0
  82. package/dist/client/outbox/outbox.relay.d.ts +90 -0
  83. package/dist/client/outbox/outbox.relay.d.ts.map +1 -0
  84. package/dist/client/outbox/outbox.store.d.ts +42 -0
  85. package/dist/client/outbox/outbox.store.d.ts.map +1 -0
  86. package/dist/client/outbox/outbox.types.d.ts +144 -0
  87. package/dist/client/outbox/outbox.types.d.ts.map +1 -0
  88. package/dist/client/security/acl.d.ts +108 -0
  89. package/dist/client/security/acl.d.ts.map +1 -0
  90. package/dist/client/security/index.d.ts +5 -0
  91. package/dist/client/security/index.d.ts.map +1 -0
  92. package/dist/client/security/providers.d.ts +88 -0
  93. package/dist/client/security/providers.d.ts.map +1 -0
  94. package/dist/client/security/resolve-security.d.ts +19 -0
  95. package/dist/client/security/resolve-security.d.ts.map +1 -0
  96. package/dist/client/security/security.types.d.ts +76 -0
  97. package/dist/client/security/security.types.d.ts.map +1 -0
  98. package/dist/client/transport/confluent.transport.d.ts +32 -0
  99. package/dist/client/transport/confluent.transport.d.ts.map +1 -0
  100. package/dist/client/transport/transport.interface.d.ts +221 -0
  101. package/dist/client/transport/transport.interface.d.ts.map +1 -0
  102. package/dist/client/types/admin.interface.d.ts +174 -0
  103. package/dist/client/types/admin.interface.d.ts.map +1 -0
  104. package/dist/client/types/admin.types.d.ts +140 -0
  105. package/dist/client/types/admin.types.d.ts.map +1 -0
  106. package/dist/client/types/client.d.ts +21 -0
  107. package/dist/client/types/client.d.ts.map +1 -0
  108. package/dist/client/types/common.d.ts +84 -0
  109. package/dist/client/types/common.d.ts.map +1 -0
  110. package/dist/client/types/config.types.d.ts +167 -0
  111. package/dist/client/types/config.types.d.ts.map +1 -0
  112. package/dist/client/types/consumer.interface.d.ts +115 -0
  113. package/dist/client/types/consumer.interface.d.ts.map +1 -0
  114. package/dist/{consumer.types-fFCag3VJ.d.mts → client/types/consumer.types.d.ts} +62 -383
  115. package/dist/client/types/consumer.types.d.ts.map +1 -0
  116. package/dist/client/types/dedup.types.d.ts +50 -0
  117. package/dist/client/types/dedup.types.d.ts.map +1 -0
  118. package/dist/client/types/lifecycle.interface.d.ts +72 -0
  119. package/dist/client/types/lifecycle.interface.d.ts.map +1 -0
  120. package/dist/client/types/producer.interface.d.ts +52 -0
  121. package/dist/client/types/producer.interface.d.ts.map +1 -0
  122. package/dist/client/types/producer.types.d.ts +90 -0
  123. package/dist/client/types/producer.types.d.ts.map +1 -0
  124. package/dist/client/types.d.ts +8 -0
  125. package/dist/client/types.d.ts.map +1 -0
  126. package/dist/core.d.ts +13 -314
  127. package/dist/core.d.ts.map +1 -0
  128. package/dist/core.js +1466 -123
  129. package/dist/core.js.map +1 -1
  130. package/dist/core.mjs +45 -3
  131. package/dist/index.d.ts +7 -128
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +1483 -123
  134. package/dist/index.js.map +1 -1
  135. package/dist/index.mjs +62 -3
  136. package/dist/index.mjs.map +1 -1
  137. package/dist/nest/kafka.constants.d.ts +5 -0
  138. package/dist/nest/kafka.constants.d.ts.map +1 -0
  139. package/dist/nest/kafka.decorator.d.ts +49 -0
  140. package/dist/nest/kafka.decorator.d.ts.map +1 -0
  141. package/dist/nest/kafka.explorer.d.ts +17 -0
  142. package/dist/nest/kafka.explorer.d.ts.map +1 -0
  143. package/dist/nest/kafka.health.d.ts +7 -0
  144. package/dist/nest/kafka.health.d.ts.map +1 -0
  145. package/dist/nest/kafka.module.d.ts +61 -0
  146. package/dist/nest/kafka.module.d.ts.map +1 -0
  147. package/dist/otel.d.ts +83 -5
  148. package/dist/otel.d.ts.map +1 -0
  149. package/dist/otel.js +100 -6
  150. package/dist/otel.js.map +1 -1
  151. package/dist/otel.mjs +98 -5
  152. package/dist/otel.mjs.map +1 -1
  153. package/dist/serde.d.ts +157 -0
  154. package/dist/serde.d.ts.map +1 -0
  155. package/dist/serde.js +308 -0
  156. package/dist/serde.js.map +1 -0
  157. package/dist/serde.mjs +158 -0
  158. package/dist/serde.mjs.map +1 -0
  159. package/dist/testing/client.mock.d.ts +47 -0
  160. package/dist/testing/client.mock.d.ts.map +1 -0
  161. package/dist/testing/index.d.ts +4 -0
  162. package/dist/testing/index.d.ts.map +1 -0
  163. package/dist/testing/test.container.d.ts +63 -0
  164. package/dist/testing/test.container.d.ts.map +1 -0
  165. package/dist/{testing.d.mts → testing/transport.fake.d.ts} +7 -111
  166. package/dist/testing/transport.fake.d.ts.map +1 -0
  167. package/dist/testing.d.ts +2 -318
  168. package/dist/testing.d.ts.map +1 -0
  169. package/dist/testing.js +26 -0
  170. package/dist/testing.js.map +1 -1
  171. package/dist/testing.mjs +26 -0
  172. package/dist/testing.mjs.map +1 -1
  173. package/package.json +40 -8
  174. package/dist/chunk-SM4FZKAZ.mjs.map +0 -1
  175. package/dist/client-1irhGEu0.d.mts +0 -751
  176. package/dist/client-BpFjkHhr.d.ts +0 -751
  177. package/dist/consumer.types-fFCag3VJ.d.ts +0 -958
  178. package/dist/core.d.mts +0 -314
  179. package/dist/index.d.mts +0 -128
  180. 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
- topic: () => topic
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/kafka.client/confluent-transport.ts
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
- await this.producer.connect();
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.fetchTopicOffsetsByTime(topic2, timestamp);
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
- this.kafka = new KafkaClass({
185
- kafkaJS: { clientId, brokers, logLevel: KafkaLogLevel.ERROR }
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: JSON.stringify(
392
- await validateMessage(topicOrDesc, m.value, deps, sendCtx)
393
- ),
394
- key: m.key ?? null,
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?.offset ?? "-1" };
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
- leader: p.leader ?? 0,
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 value = message.value.toString();
1156
- const shouldProcess = !options.filter || options.filter(headers, value);
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, fire all `onDlq` instrumentation hooks,
1215
- * and notify the circuit breaker of a failure (when `gid` is provided).
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, gid) {
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
- if (gid) this.deps.onCircuitFailure(envelope, gid);
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
- return fn().finally(() => {
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 raw = message.value.toString();
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
- raw,
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
- [raw],
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
- raw,
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
- if (!ctx.dedupStates.has(groupId))
2214
- ctx.dedupStates.set(groupId, /* @__PURE__ */ new Map());
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, gid),
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
- const lastProcessedClock = dedup.state.get(stateKey) ?? -1;
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
- dedup.state.set(stateKey, incomingClock);
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 raw = message.value.toString();
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
- raw,
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
- message.value.toString(),
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, message.value.toString(), deps, {
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: [message.value.toString()],
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
- raw,
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, message.value.toString(), deps, {
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(message.value.toString());
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
- await handler(envelopes, { trigger, windowStart, windowEnd: Date.now() });
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").catch((err) => {
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
- if (flushTimer !== null) {
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/snapshot.ts
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 transport = options?.transport ?? new ConfluentTransport(clientId, brokers);
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
- topic
5180
+ startOutboxRelay,
5181
+ toError,
5182
+ toKafkaAclCommands,
5183
+ toMskIamPolicy,
5184
+ topic,
5185
+ versionedSchema
3826
5186
  });
3827
5187
  //# sourceMappingURL=index.js.map