@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
@@ -0,0 +1,4760 @@
1
+ import {
2
+ JsonSerde
3
+ } from "./chunk-PQVBRDNV.mjs";
4
+
5
+ // src/client/transport/confluent.transport.ts
6
+ import { KafkaJS } from "@confluentinc/kafka-javascript";
7
+ var { Kafka: KafkaClass, logLevel: KafkaLogLevel, PartitionAssigners } = KafkaJS;
8
+ var ConfluentTransaction = class {
9
+ constructor(tx) {
10
+ this.tx = tx;
11
+ }
12
+ tx;
13
+ async send(record) {
14
+ await this.tx.send(record);
15
+ }
16
+ async sendOffsets(options) {
17
+ const nativeConsumer = options.consumer.getNative();
18
+ await this.tx.sendOffsets({
19
+ consumer: nativeConsumer,
20
+ topics: options.topics
21
+ });
22
+ }
23
+ async commit() {
24
+ await this.tx.commit();
25
+ }
26
+ async abort() {
27
+ await this.tx.abort();
28
+ }
29
+ };
30
+ var ConfluentProducer = class {
31
+ constructor(producer) {
32
+ this.producer = producer;
33
+ }
34
+ producer;
35
+ connectPromise;
36
+ async connect() {
37
+ this.connectPromise ??= this.producer.connect().catch((err) => {
38
+ this.connectPromise = void 0;
39
+ throw err;
40
+ });
41
+ return this.connectPromise;
42
+ }
43
+ async disconnect() {
44
+ this.connectPromise = void 0;
45
+ await this.producer.disconnect();
46
+ }
47
+ async send(record) {
48
+ await this.producer.send(record);
49
+ }
50
+ async transaction() {
51
+ const tx = await this.producer.transaction();
52
+ return new ConfluentTransaction(tx);
53
+ }
54
+ };
55
+ var ConfluentConsumer = class {
56
+ constructor(consumer) {
57
+ this.consumer = consumer;
58
+ }
59
+ consumer;
60
+ /** Returns the underlying KafkaJS.Consumer — used by ConfluentTransaction.sendOffsets. */
61
+ getNative() {
62
+ return this.consumer;
63
+ }
64
+ async connect() {
65
+ await this.consumer.connect();
66
+ }
67
+ async disconnect() {
68
+ await this.consumer.disconnect();
69
+ }
70
+ async subscribe(options) {
71
+ await this.consumer.subscribe(options);
72
+ }
73
+ async run(config) {
74
+ await this.consumer.run(config);
75
+ }
76
+ pause(assignments) {
77
+ this.consumer.pause(assignments);
78
+ }
79
+ resume(assignments) {
80
+ this.consumer.resume(assignments);
81
+ }
82
+ seek(options) {
83
+ this.consumer.seek(options);
84
+ }
85
+ assignment() {
86
+ return this.consumer.assignment();
87
+ }
88
+ async commitOffsets(offsets) {
89
+ await this.consumer.commitOffsets(offsets);
90
+ }
91
+ async stop() {
92
+ await this.consumer.stop?.();
93
+ }
94
+ };
95
+ var ConfluentAdmin = class {
96
+ constructor(admin) {
97
+ this.admin = admin;
98
+ }
99
+ admin;
100
+ async connect() {
101
+ await this.admin.connect();
102
+ }
103
+ async disconnect() {
104
+ await this.admin.disconnect();
105
+ }
106
+ async createTopics(options) {
107
+ await this.admin.createTopics(options);
108
+ }
109
+ async fetchTopicOffsets(topic2) {
110
+ return this.admin.fetchTopicOffsets(topic2);
111
+ }
112
+ async fetchTopicOffsetsByTimestamp(topic2, timestamp) {
113
+ return this.admin.fetchTopicOffsetsByTimestamp(topic2, timestamp);
114
+ }
115
+ async fetchOffsets(options) {
116
+ return this.admin.fetchOffsets(options);
117
+ }
118
+ async setOffsets(options) {
119
+ await this.admin.setOffsets(options);
120
+ }
121
+ async listTopics() {
122
+ return this.admin.listTopics();
123
+ }
124
+ async listGroups() {
125
+ return this.admin.listGroups();
126
+ }
127
+ async fetchTopicMetadata(options) {
128
+ return this.admin.fetchTopicMetadata(options);
129
+ }
130
+ async deleteGroups(groupIds) {
131
+ await this.admin.deleteGroups(groupIds);
132
+ }
133
+ async deleteTopicRecords(options) {
134
+ await this.admin.deleteTopicRecords(options);
135
+ }
136
+ };
137
+ var ConfluentTransport = class {
138
+ kafka;
139
+ constructor(clientId, brokers, security) {
140
+ const kafkaJS = { clientId, brokers, logLevel: KafkaLogLevel.ERROR };
141
+ if (security?.ssl !== void 0) kafkaJS.ssl = security.ssl;
142
+ if (security?.sasl) {
143
+ if (security.sasl.mechanism === "oauthbearer") {
144
+ const provider = security.sasl.oauthBearerProvider;
145
+ kafkaJS.sasl = {
146
+ mechanism: "oauthbearer",
147
+ oauthBearerProvider: async () => {
148
+ const token = await provider();
149
+ return {
150
+ value: token.value,
151
+ principal: token.principal ?? "kafka-client",
152
+ lifetime: token.lifetimeMs ?? Date.now() + 15 * 6e4,
153
+ ...token.extensions && { extensions: token.extensions }
154
+ };
155
+ }
156
+ };
157
+ } else {
158
+ kafkaJS.sasl = security.sasl;
159
+ }
160
+ }
161
+ this.kafka = new KafkaClass({ kafkaJS });
162
+ }
163
+ producer(options) {
164
+ const native = this.kafka.producer({
165
+ kafkaJS: {
166
+ acks: -1,
167
+ ...options?.idempotent !== void 0 && {
168
+ idempotent: options.idempotent
169
+ },
170
+ ...options?.transactionalId !== void 0 && {
171
+ transactionalId: options.transactionalId,
172
+ maxInFlightRequests: 1
173
+ }
174
+ }
175
+ });
176
+ return new ConfluentProducer(native);
177
+ }
178
+ consumer(options) {
179
+ const assigner = options.partitionAssigner === "roundrobin" ? PartitionAssigners.roundRobin : options.partitionAssigner === "range" ? PartitionAssigners.range : PartitionAssigners.cooperativeSticky;
180
+ const config = {
181
+ kafkaJS: {
182
+ groupId: options.groupId,
183
+ fromBeginning: options.fromBeginning,
184
+ autoCommit: options.autoCommit,
185
+ partitionAssigners: [assigner]
186
+ }
187
+ };
188
+ if (options.groupInstanceId) {
189
+ config["group.instance.id"] = options.groupInstanceId;
190
+ }
191
+ if (options.onRebalance) {
192
+ const cb = options.onRebalance;
193
+ config.rebalance_cb = (err, assignment) => {
194
+ const type = err.code === -175 ? "assign" : "revoke";
195
+ cb(
196
+ type,
197
+ assignment.map((p) => ({ topic: p.topic, partition: p.partition }))
198
+ );
199
+ };
200
+ }
201
+ return new ConfluentConsumer(this.kafka.consumer(config));
202
+ }
203
+ admin() {
204
+ return new ConfluentAdmin(this.kafka.admin());
205
+ }
206
+ };
207
+
208
+ // src/client/kafka.client/infra/dedup.store.ts
209
+ var InMemoryDedupStore = class {
210
+ constructor(states) {
211
+ this.states = states;
212
+ }
213
+ states;
214
+ getLastClock(groupId, topicPartition) {
215
+ return this.states.get(groupId)?.get(topicPartition);
216
+ }
217
+ setLastClock(groupId, topicPartition, clock) {
218
+ let group = this.states.get(groupId);
219
+ if (!group) {
220
+ group = /* @__PURE__ */ new Map();
221
+ this.states.set(groupId, group);
222
+ }
223
+ group.set(topicPartition, clock);
224
+ }
225
+ };
226
+
227
+ // src/client/message/envelope.ts
228
+ import { AsyncLocalStorage } from "async_hooks";
229
+ import { randomUUID } from "crypto";
230
+ var HEADER_EVENT_ID = "x-event-id";
231
+ var HEADER_CORRELATION_ID = "x-correlation-id";
232
+ var HEADER_TIMESTAMP = "x-timestamp";
233
+ var HEADER_SCHEMA_VERSION = "x-schema-version";
234
+ var HEADER_TRACEPARENT = "traceparent";
235
+ var HEADER_LAMPORT_CLOCK = "x-lamport-clock";
236
+ var HEADER_DELAYED_UNTIL = "x-delayed-until";
237
+ var HEADER_DELAYED_TARGET = "x-delayed-target";
238
+ var envelopeStorage = new AsyncLocalStorage();
239
+ function getEnvelopeContext() {
240
+ return envelopeStorage.getStore();
241
+ }
242
+ function runWithEnvelopeContext(ctx, fn) {
243
+ return envelopeStorage.run(ctx, fn);
244
+ }
245
+ function buildEnvelopeHeaders(options = {}) {
246
+ const ctx = getEnvelopeContext();
247
+ const correlationId = options.correlationId ?? ctx?.correlationId ?? randomUUID();
248
+ const eventId = options.eventId ?? randomUUID();
249
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
250
+ const schemaVersion = String(options.schemaVersion ?? 1);
251
+ const envelope = {
252
+ [HEADER_EVENT_ID]: eventId,
253
+ [HEADER_CORRELATION_ID]: correlationId,
254
+ [HEADER_TIMESTAMP]: timestamp,
255
+ [HEADER_SCHEMA_VERSION]: schemaVersion
256
+ };
257
+ if (ctx?.traceparent) {
258
+ envelope[HEADER_TRACEPARENT] = ctx.traceparent;
259
+ }
260
+ return { ...envelope, ...options.headers };
261
+ }
262
+ function decodeHeaders(raw) {
263
+ if (!raw) return {};
264
+ const result = {};
265
+ for (const [key, value] of Object.entries(raw)) {
266
+ if (value === void 0) continue;
267
+ if (Array.isArray(value)) {
268
+ const items = value.map((v) => Buffer.isBuffer(v) ? v.toString() : v);
269
+ result[key] = items[items.length - 1] ?? "";
270
+ } else {
271
+ result[key] = Buffer.isBuffer(value) ? value.toString() : value;
272
+ }
273
+ }
274
+ return result;
275
+ }
276
+ function extractEnvelope(payload, headers, topic2, partition, offset) {
277
+ return {
278
+ payload,
279
+ topic: topic2,
280
+ partition,
281
+ offset,
282
+ eventId: headers[HEADER_EVENT_ID] ?? randomUUID(),
283
+ correlationId: headers[HEADER_CORRELATION_ID] ?? randomUUID(),
284
+ timestamp: headers[HEADER_TIMESTAMP] ?? (/* @__PURE__ */ new Date()).toISOString(),
285
+ schemaVersion: Number(headers[HEADER_SCHEMA_VERSION] ?? 1),
286
+ traceparent: headers[HEADER_TRACEPARENT],
287
+ headers
288
+ };
289
+ }
290
+
291
+ // src/client/errors.ts
292
+ function toError(error) {
293
+ return error instanceof Error ? error : new Error(String(error));
294
+ }
295
+ var KafkaProcessingError = class extends Error {
296
+ constructor(message, topic2, originalMessage, options) {
297
+ super(message, options);
298
+ this.topic = topic2;
299
+ this.originalMessage = originalMessage;
300
+ this.name = "KafkaProcessingError";
301
+ if (options?.cause) this.cause = options.cause;
302
+ }
303
+ topic;
304
+ originalMessage;
305
+ };
306
+ var KafkaValidationError = class extends Error {
307
+ constructor(topic2, originalMessage, options) {
308
+ super(`Schema validation failed for topic "${topic2}"`, options);
309
+ this.topic = topic2;
310
+ this.originalMessage = originalMessage;
311
+ this.name = "KafkaValidationError";
312
+ if (options?.cause) this.cause = options.cause;
313
+ }
314
+ topic;
315
+ originalMessage;
316
+ };
317
+ var KafkaRetryExhaustedError = class extends KafkaProcessingError {
318
+ constructor(topic2, originalMessage, attempts, options) {
319
+ super(
320
+ `Message processing failed after ${attempts} attempts on topic "${topic2}"`,
321
+ topic2,
322
+ originalMessage,
323
+ options
324
+ );
325
+ this.attempts = attempts;
326
+ this.name = "KafkaRetryExhaustedError";
327
+ }
328
+ attempts;
329
+ };
330
+
331
+ // src/client/kafka.client/producer/ops.ts
332
+ function resolveSerde(topicOrDesc, clientSerde) {
333
+ return topicOrDesc?.__serde ?? clientSerde;
334
+ }
335
+ function resolveTopicName(topicOrDescriptor) {
336
+ if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
337
+ if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
338
+ return topicOrDescriptor.__topic;
339
+ }
340
+ return String(topicOrDescriptor);
341
+ }
342
+ function registerSchema(topicOrDesc, schemaRegistry, logger) {
343
+ if (topicOrDesc?.__schema) {
344
+ const topic2 = resolveTopicName(topicOrDesc);
345
+ const existing = schemaRegistry.get(topic2);
346
+ if (existing && existing !== topicOrDesc.__schema) {
347
+ logger?.warn(
348
+ `Schema conflict for topic "${topic2}": a different schema is already registered. Using the new schema \u2014 ensure consistent schemas to avoid silent validation mismatches.`
349
+ );
350
+ }
351
+ schemaRegistry.set(topic2, topicOrDesc.__schema);
352
+ }
353
+ }
354
+ async function validateMessage(topicOrDesc, message, deps, ctx) {
355
+ const topicName = resolveTopicName(topicOrDesc);
356
+ if (topicOrDesc?.__schema) {
357
+ try {
358
+ return await topicOrDesc.__schema.parse(message, ctx);
359
+ } catch (error) {
360
+ throw new KafkaValidationError(topicName, message, {
361
+ cause: error instanceof Error ? error : new Error(String(error))
362
+ });
363
+ }
364
+ }
365
+ if (deps.strictSchemasEnabled && typeof topicOrDesc === "string") {
366
+ const schema = deps.schemaRegistry.get(topicOrDesc);
367
+ if (schema) {
368
+ try {
369
+ return await schema.parse(message, ctx);
370
+ } catch (error) {
371
+ throw new KafkaValidationError(topicName, message, {
372
+ cause: error instanceof Error ? error : new Error(String(error))
373
+ });
374
+ }
375
+ }
376
+ }
377
+ return message;
378
+ }
379
+ async function buildSendPayload(topicOrDesc, messages, deps, compression) {
380
+ const topic2 = resolveTopicName(topicOrDesc);
381
+ const serde = resolveSerde(topicOrDesc, deps.serde);
382
+ const builtMessages = await Promise.all(
383
+ messages.map(async (m) => {
384
+ const envelopeHeaders = buildEnvelopeHeaders({
385
+ correlationId: m.correlationId,
386
+ schemaVersion: m.schemaVersion,
387
+ eventId: m.eventId,
388
+ headers: m.headers
389
+ });
390
+ if (deps.nextLamportClock) {
391
+ envelopeHeaders[HEADER_LAMPORT_CLOCK] = String(deps.nextLamportClock());
392
+ }
393
+ for (const inst of deps.instrumentation) {
394
+ inst.beforeSend?.(topic2, envelopeHeaders);
395
+ }
396
+ const sendCtx = {
397
+ topic: topic2,
398
+ headers: envelopeHeaders,
399
+ version: m.schemaVersion ?? 1
400
+ };
401
+ const validated = await validateMessage(topicOrDesc, m.value, deps, sendCtx);
402
+ return {
403
+ value: await serde.serialize(validated, {
404
+ topic: topic2,
405
+ headers: envelopeHeaders,
406
+ isKey: false
407
+ }),
408
+ // Explicit key wins; otherwise fall back to the descriptor's .key()
409
+ // extractor (runs on the original, pre-validation payload).
410
+ key: m.key ?? topicOrDesc?.__key?.(m.value) ?? null,
411
+ headers: envelopeHeaders
412
+ };
413
+ })
414
+ );
415
+ return { topic: topic2, messages: builtMessages, ...compression && { compression } };
416
+ }
417
+
418
+ // src/client/kafka.client/consumer/ops.ts
419
+ function getOrCreateConsumer(groupId, fromBeginning, autoCommit, deps, partitionAssigner, onFirstAssignment, groupInstanceId) {
420
+ const { consumers, consumerCreationOptions, transport, onRebalance, logger } = deps;
421
+ if (consumers.has(groupId)) {
422
+ const prev = consumerCreationOptions.get(groupId);
423
+ if (prev.fromBeginning !== fromBeginning || prev.autoCommit !== autoCommit) {
424
+ logger.warn(
425
+ `Consumer group "${groupId}" already exists with options (fromBeginning: ${prev.fromBeginning}, autoCommit: ${prev.autoCommit}) \u2014 new options (fromBeginning: ${fromBeginning}, autoCommit: ${autoCommit}) ignored. Use a different groupId to apply different options.`
426
+ );
427
+ }
428
+ onFirstAssignment?.();
429
+ return consumers.get(groupId);
430
+ }
431
+ consumerCreationOptions.set(groupId, { fromBeginning, autoCommit });
432
+ const SETTLE_MS = fromBeginning ? 0 : 500;
433
+ let hasAssignment = false;
434
+ let settleTimer;
435
+ const scheduleSettle = () => {
436
+ if (!hasAssignment) return;
437
+ if (settleTimer) clearTimeout(settleTimer);
438
+ settleTimer = setTimeout(() => {
439
+ settleTimer = void 0;
440
+ onFirstAssignment?.();
441
+ }, SETTLE_MS);
442
+ };
443
+ const fireOnAssignment = () => {
444
+ hasAssignment = true;
445
+ scheduleSettle();
446
+ };
447
+ const consumer = transport.consumer({
448
+ groupId,
449
+ fromBeginning,
450
+ autoCommit,
451
+ partitionAssigner: partitionAssigner ?? "cooperative-sticky",
452
+ groupInstanceId,
453
+ onRebalance: (type, assignments) => {
454
+ if (type === "assign") fireOnAssignment();
455
+ else if (type === "revoke") scheduleSettle();
456
+ if (onRebalance) {
457
+ try {
458
+ onRebalance(type, assignments);
459
+ } catch (e) {
460
+ logger.warn(`onRebalance callback threw: ${e.message}`);
461
+ }
462
+ }
463
+ }
464
+ });
465
+ consumers.set(groupId, consumer);
466
+ return consumer;
467
+ }
468
+ function buildSchemaMap(topics, schemaRegistry, optionSchemas, logger) {
469
+ const schemaMap = /* @__PURE__ */ new Map();
470
+ const registerChecked = (name, schema) => {
471
+ const existing = schemaRegistry.get(name);
472
+ if (existing && existing !== schema) {
473
+ logger?.warn(
474
+ `Schema conflict for topic "${name}": a different schema is already registered. Using the new schema \u2014 ensure consistent schemas to avoid silent validation mismatches.`
475
+ );
476
+ }
477
+ schemaMap.set(name, schema);
478
+ schemaRegistry.set(name, schema);
479
+ };
480
+ for (const t of topics) {
481
+ if (t?.__schema) {
482
+ registerChecked(resolveTopicName(t), t.__schema);
483
+ }
484
+ }
485
+ if (optionSchemas) {
486
+ for (const [k, v] of optionSchemas) {
487
+ registerChecked(k, v);
488
+ }
489
+ }
490
+ return schemaMap;
491
+ }
492
+ function buildSerdeMap(topics) {
493
+ let serdeMap;
494
+ for (const t of topics) {
495
+ if (t?.__serde) {
496
+ (serdeMap ??= /* @__PURE__ */ new Map()).set(resolveTopicName(t), t.__serde);
497
+ }
498
+ }
499
+ return serdeMap;
500
+ }
501
+
502
+ // src/client/kafka.client/admin/ops.ts
503
+ var AdminOps = class {
504
+ constructor(deps) {
505
+ this.deps = deps;
506
+ }
507
+ deps;
508
+ isConnected = false;
509
+ /** Underlying admin client — used by index.ts for topic validation. */
510
+ get admin() {
511
+ return this.deps.admin;
512
+ }
513
+ /** Whether the admin client is currently connected. */
514
+ get connected() {
515
+ return this.isConnected;
516
+ }
517
+ /**
518
+ * Connect the admin client if not already connected.
519
+ * The flag is only set to `true` after a successful connect — if `admin.connect()`
520
+ * throws the flag remains `false` so the next call will retry the connection.
521
+ */
522
+ async ensureConnected() {
523
+ if (this.isConnected) return;
524
+ try {
525
+ await this.deps.admin.connect();
526
+ this.isConnected = true;
527
+ } catch (err) {
528
+ this.isConnected = false;
529
+ throw err;
530
+ }
531
+ }
532
+ /** Disconnect admin if connected. Resets the connected flag. */
533
+ async disconnect() {
534
+ if (!this.isConnected) return;
535
+ await this.deps.admin.disconnect();
536
+ this.isConnected = false;
537
+ }
538
+ async resetOffsets(groupId, topic2, position) {
539
+ const gid = groupId ?? this.deps.defaultGroupId;
540
+ if (this.deps.runningConsumers.has(gid)) {
541
+ throw new Error(
542
+ `resetOffsets: consumer group "${gid}" is still running. Call stopConsumer("${gid}") before resetting offsets.`
543
+ );
544
+ }
545
+ await this.ensureConnected();
546
+ const partitionOffsets = await this.deps.admin.fetchTopicOffsets(topic2);
547
+ const partitions = partitionOffsets.map(({ partition, low, high }) => ({
548
+ partition,
549
+ offset: position === "earliest" ? low : high
550
+ }));
551
+ await this.deps.admin.setOffsets({ groupId: gid, topic: topic2, partitions });
552
+ this.deps.logger.log(
553
+ `Offsets reset to ${position} for group "${gid}" on topic "${topic2}"`
554
+ );
555
+ }
556
+ /**
557
+ * Seek specific topic-partition pairs to explicit offsets for a stopped consumer group.
558
+ * Throws if the group is still running — call `stopConsumer(groupId)` first.
559
+ * Assignments are grouped by topic and committed via `admin.setOffsets`.
560
+ */
561
+ async seekToOffset(groupId, assignments) {
562
+ const gid = groupId ?? this.deps.defaultGroupId;
563
+ if (this.deps.runningConsumers.has(gid)) {
564
+ throw new Error(
565
+ `seekToOffset: consumer group "${gid}" is still running. Call stopConsumer("${gid}") before seeking offsets.`
566
+ );
567
+ }
568
+ await this.ensureConnected();
569
+ const byTopic = /* @__PURE__ */ new Map();
570
+ for (const { topic: topic2, partition, offset } of assignments) {
571
+ const list = byTopic.get(topic2) ?? [];
572
+ list.push({ partition, offset });
573
+ byTopic.set(topic2, list);
574
+ }
575
+ for (const [topic2, partitions] of byTopic) {
576
+ await this.deps.admin.setOffsets({ groupId: gid, topic: topic2, partitions });
577
+ this.deps.logger.log(
578
+ `Offsets set for group "${gid}" on "${topic2}": ${JSON.stringify(partitions)}`
579
+ );
580
+ }
581
+ }
582
+ /**
583
+ * Seek specific topic-partition pairs to the offset nearest to a given timestamp
584
+ * (in milliseconds) for a stopped consumer group.
585
+ * Throws if the group is still running — call `stopConsumer(groupId)` first.
586
+ * Assignments are grouped by topic and committed via `admin.setOffsets`.
587
+ * If no offset exists at the requested timestamp (e.g. empty partition or
588
+ * future timestamp), the partition falls back to `-1` (end of topic — new messages only).
589
+ */
590
+ async seekToTimestamp(groupId, assignments) {
591
+ const gid = groupId ?? this.deps.defaultGroupId;
592
+ if (this.deps.runningConsumers.has(gid)) {
593
+ throw new Error(
594
+ `seekToTimestamp: consumer group "${gid}" is still running. Call stopConsumer("${gid}") before seeking offsets.`
595
+ );
596
+ }
597
+ await this.ensureConnected();
598
+ const byTopic = /* @__PURE__ */ new Map();
599
+ for (const { topic: topic2, partition, timestamp } of assignments) {
600
+ const list = byTopic.get(topic2) ?? [];
601
+ list.push({ partition, timestamp });
602
+ byTopic.set(topic2, list);
603
+ }
604
+ for (const [topic2, parts] of byTopic) {
605
+ const offsets = await Promise.all(
606
+ parts.map(async ({ partition, timestamp }) => {
607
+ const results = await this.deps.admin.fetchTopicOffsetsByTimestamp(
608
+ topic2,
609
+ timestamp
610
+ );
611
+ const found = results.find(
612
+ (r) => r.partition === partition
613
+ );
614
+ if (found) return { partition, offset: found.offset };
615
+ const topicOffsets = await this.deps.admin.fetchTopicOffsets(topic2);
616
+ const po = topicOffsets.find((o) => o.partition === partition);
617
+ return { partition, offset: po?.high ?? "0" };
618
+ })
619
+ );
620
+ await this.deps.admin.setOffsets({ groupId: gid, topic: topic2, partitions: offsets });
621
+ this.deps.logger.log(
622
+ `Offsets set by timestamp for group "${gid}" on "${topic2}": ${JSON.stringify(offsets)}`
623
+ );
624
+ }
625
+ }
626
+ /**
627
+ * Query consumer group lag per partition.
628
+ * Lag = broker high-watermark − last committed offset.
629
+ * A committed offset of -1 (nothing committed yet) counts as full lag.
630
+ *
631
+ * Returns an empty array when the consumer group has never committed any
632
+ * offsets (freshly created group, `autoCommit: false` with no manual commits,
633
+ * or group not yet assigned). This is a Kafka protocol limitation:
634
+ * `fetchOffsets` only returns data for topic-partitions that have at least one
635
+ * committed offset. Use `checkStatus()` to verify broker connectivity in that case.
636
+ */
637
+ async getConsumerLag(groupId) {
638
+ const gid = groupId ?? this.deps.defaultGroupId;
639
+ await this.ensureConnected();
640
+ const committedByTopic = await this.deps.admin.fetchOffsets({ groupId: gid });
641
+ const brokerOffsetsAll = await Promise.all(
642
+ committedByTopic.map(({ topic: topic2 }) => this.deps.admin.fetchTopicOffsets(topic2))
643
+ );
644
+ const result = [];
645
+ for (let i = 0; i < committedByTopic.length; i++) {
646
+ const { topic: topic2, partitions } = committedByTopic[i];
647
+ const brokerOffsets = brokerOffsetsAll[i];
648
+ for (const { partition, offset } of partitions) {
649
+ const broker = brokerOffsets.find((o) => o.partition === partition);
650
+ if (!broker) continue;
651
+ const committed = parseInt(offset, 10);
652
+ const high = parseInt(broker.high, 10);
653
+ const lag = committed === -1 ? high : Math.max(0, high - committed);
654
+ result.push({ topic: topic2, partition, lag });
655
+ }
656
+ }
657
+ return result;
658
+ }
659
+ /** Check broker connectivity. Never throws — returns a discriminated union. */
660
+ async checkStatus() {
661
+ try {
662
+ await this.ensureConnected();
663
+ const topics = await this.deps.admin.listTopics();
664
+ return { status: "up", clientId: this.deps.clientId, topics };
665
+ } catch (error) {
666
+ return {
667
+ status: "down",
668
+ clientId: this.deps.clientId,
669
+ error: error instanceof Error ? error.message : String(error)
670
+ };
671
+ }
672
+ }
673
+ /**
674
+ * List all consumer groups known to the broker.
675
+ * Useful for monitoring which groups are active and their current state.
676
+ */
677
+ async listConsumerGroups() {
678
+ await this.ensureConnected();
679
+ const result = await this.deps.admin.listGroups();
680
+ return result.groups.map((g) => ({
681
+ groupId: g.groupId,
682
+ state: g.state ?? "Unknown"
683
+ }));
684
+ }
685
+ /**
686
+ * Describe topics — returns partition layout, leader, replicas, and ISR.
687
+ * @param topics Topic names to describe. Omit to describe all topics.
688
+ */
689
+ async describeTopics(topics) {
690
+ await this.ensureConnected();
691
+ const result = await this.deps.admin.fetchTopicMetadata(
692
+ topics ? { topics } : void 0
693
+ );
694
+ return result.topics.map((t) => ({
695
+ name: t.name,
696
+ partitions: t.partitions.map((p) => ({
697
+ partition: p.partitionId ?? p.partition ?? 0,
698
+ // -1 is Kafka's own "no leader" sentinel; 0 is a valid broker id
699
+ leader: p.leader ?? -1,
700
+ replicas: (p.replicas ?? []).map(
701
+ (r) => typeof r === "number" ? r : r.nodeId
702
+ ),
703
+ isr: (p.isr ?? []).map(
704
+ (r) => typeof r === "number" ? r : r.nodeId
705
+ )
706
+ }))
707
+ }));
708
+ }
709
+ /**
710
+ * Delete consumer groups from the broker.
711
+ * Groups must be empty (no active members) before deletion.
712
+ * Silently skips unknown group IDs.
713
+ * @param groupIds Consumer group IDs to delete.
714
+ */
715
+ async deleteGroups(groupIds) {
716
+ if (groupIds.length === 0) return;
717
+ await this.ensureConnected();
718
+ await this.deps.admin.deleteGroups(groupIds);
719
+ }
720
+ /**
721
+ * Delete records from a topic up to (but not including) the given offsets.
722
+ * All messages with offsets **before** the given offset are deleted.
723
+ */
724
+ async deleteRecords(topic2, partitions) {
725
+ await this.ensureConnected();
726
+ await this.deps.admin.deleteTopicRecords({ topic: topic2, partitions });
727
+ }
728
+ /**
729
+ * When `retryTopics: true` and `autoCreateTopics: false`, verify that every
730
+ * `<topic>.retry.<level>` topic already exists. Throws a clear error at startup
731
+ * rather than silently discovering missing topics on the first handler failure.
732
+ */
733
+ async validateRetryTopicsExist(topicNames, maxRetries) {
734
+ await this.ensureConnected();
735
+ const existing = new Set(await this.deps.admin.listTopics());
736
+ const missing = [];
737
+ for (const t of topicNames) {
738
+ for (let level = 1; level <= maxRetries; level++) {
739
+ const retryTopic = `${t}.retry.${level}`;
740
+ if (!existing.has(retryTopic)) missing.push(retryTopic);
741
+ }
742
+ }
743
+ if (missing.length > 0) {
744
+ throw new Error(
745
+ `retryTopics: true but the following retry topics do not exist: ${missing.join(", ")}. Create them manually or set autoCreateTopics: true.`
746
+ );
747
+ }
748
+ }
749
+ /**
750
+ * When `autoCreateTopics` is disabled, verify that `<topic>.dlq` exists for every
751
+ * consumed topic. Throws a clear error at startup rather than silently discovering
752
+ * missing DLQ topics on the first handler failure.
753
+ */
754
+ async validateDlqTopicsExist(topicNames) {
755
+ await this.ensureConnected();
756
+ const existing = new Set(await this.deps.admin.listTopics());
757
+ const missing = topicNames.filter((t) => !existing.has(`${t}.dlq`)).map((t) => `${t}.dlq`);
758
+ if (missing.length > 0) {
759
+ throw new Error(
760
+ `dlq: true but the following DLQ topics do not exist: ${missing.join(", ")}. Create them manually or set autoCreateTopics: true.`
761
+ );
762
+ }
763
+ }
764
+ /**
765
+ * When `deduplication.strategy: 'topic'` and `autoCreateTopics: false`, verify
766
+ * that every `<topic>.duplicates` destination topic already exists. Throws a
767
+ * clear error at startup rather than silently dropping duplicates on first hit.
768
+ */
769
+ async validateDuplicatesTopicsExist(topicNames, customDestination) {
770
+ await this.ensureConnected();
771
+ const existing = new Set(await this.deps.admin.listTopics());
772
+ const toCheck = customDestination ? [customDestination] : topicNames.map((t) => `${t}.duplicates`);
773
+ const missing = toCheck.filter((t) => !existing.has(t));
774
+ if (missing.length > 0) {
775
+ throw new Error(
776
+ `deduplication.strategy: 'topic' but the following duplicate-routing topics do not exist: ${missing.join(", ")}. Create them manually or set autoCreateTopics: true.`
777
+ );
778
+ }
779
+ }
780
+ };
781
+
782
+ // src/client/kafka.client/consumer/pipeline.ts
783
+ function sleep(ms) {
784
+ return new Promise((resolve) => setTimeout(resolve, ms));
785
+ }
786
+ async function validateWithSchema(message, raw, topic2, schemaMap, interceptors, dlq, deps) {
787
+ const schema = schemaMap.get(topic2);
788
+ if (!schema) return message;
789
+ const ctx = {
790
+ topic: topic2,
791
+ headers: deps.originalHeaders ?? {},
792
+ version: Number(deps.originalHeaders?.["x-schema-version"] ?? 1)
793
+ };
794
+ try {
795
+ return await schema.parse(message, ctx);
796
+ } catch (error) {
797
+ const err = toError(error);
798
+ const validationError = new KafkaValidationError(topic2, message, {
799
+ cause: err
800
+ });
801
+ deps.logger.error(
802
+ `Schema validation failed for topic ${topic2}:`,
803
+ err.message
804
+ );
805
+ if (dlq) {
806
+ await sendToDlq(topic2, raw, deps, {
807
+ error: validationError,
808
+ attempt: 0,
809
+ originalHeaders: deps.originalHeaders
810
+ });
811
+ } else {
812
+ await deps.onMessageLost?.({
813
+ topic: topic2,
814
+ error: validationError,
815
+ attempt: 0,
816
+ headers: deps.originalHeaders ?? {}
817
+ });
818
+ }
819
+ const errorEnvelope = extractEnvelope(
820
+ message,
821
+ deps.originalHeaders ?? {},
822
+ topic2,
823
+ -1,
824
+ ""
825
+ );
826
+ for (const inst of deps.instrumentation ?? []) {
827
+ inst.onConsumeError?.(errorEnvelope, validationError);
828
+ }
829
+ for (const interceptor of interceptors) {
830
+ await interceptor.onError?.(errorEnvelope, validationError);
831
+ }
832
+ return null;
833
+ }
834
+ }
835
+ function buildDlqPayload(topic2, rawMessage, meta) {
836
+ const dlqTopic = `${topic2}.dlq`;
837
+ const headers = {
838
+ ...meta?.originalHeaders ?? {},
839
+ "x-dlq-original-topic": topic2,
840
+ "x-dlq-failed-at": (/* @__PURE__ */ new Date()).toISOString(),
841
+ "x-dlq-error-message": meta?.error.message ?? "unknown",
842
+ "x-dlq-error-stack": meta?.error.stack?.slice(0, 2e3) ?? "",
843
+ "x-dlq-attempt-count": String(meta?.attempt ?? 0)
844
+ };
845
+ return { topic: dlqTopic, messages: [{ value: rawMessage, headers }] };
846
+ }
847
+ async function sendToDlq(topic2, rawMessage, deps, meta) {
848
+ const payload = buildDlqPayload(topic2, rawMessage, meta);
849
+ try {
850
+ await deps.producer.send(payload);
851
+ deps.logger.warn(`Message sent to DLQ: ${payload.topic}`);
852
+ } catch (error) {
853
+ const err = toError(error);
854
+ deps.logger.error(
855
+ `Failed to send message to DLQ ${payload.topic}:`,
856
+ err.stack
857
+ );
858
+ await deps.onMessageLost?.({
859
+ topic: topic2,
860
+ error: err,
861
+ attempt: meta?.attempt ?? 0,
862
+ headers: meta?.originalHeaders ?? {}
863
+ });
864
+ }
865
+ }
866
+ var RETRY_HEADER_ATTEMPT = "x-retry-attempt";
867
+ var RETRY_HEADER_AFTER = "x-retry-after";
868
+ var RETRY_HEADER_MAX_RETRIES = "x-retry-max-retries";
869
+ var RETRY_HEADER_ORIGINAL_TOPIC = "x-retry-original-topic";
870
+ function buildRetryTopicPayload(originalTopic, rawMessages, attempt, maxRetries, delayMs, originalHeaders) {
871
+ const retryTopic = `${originalTopic}.retry.${attempt}`;
872
+ const STRIP = /* @__PURE__ */ new Set([
873
+ RETRY_HEADER_ATTEMPT,
874
+ RETRY_HEADER_AFTER,
875
+ RETRY_HEADER_MAX_RETRIES,
876
+ RETRY_HEADER_ORIGINAL_TOPIC
877
+ ]);
878
+ function buildHeaders(hdr) {
879
+ const userHeaders = Object.fromEntries(
880
+ Object.entries(hdr).filter(([k]) => !STRIP.has(k))
881
+ );
882
+ return {
883
+ ...userHeaders,
884
+ [RETRY_HEADER_ATTEMPT]: String(attempt),
885
+ [RETRY_HEADER_AFTER]: String(Date.now() + delayMs),
886
+ [RETRY_HEADER_MAX_RETRIES]: String(maxRetries),
887
+ [RETRY_HEADER_ORIGINAL_TOPIC]: originalTopic
888
+ };
889
+ }
890
+ return {
891
+ topic: retryTopic,
892
+ messages: rawMessages.map((value, i) => ({
893
+ value,
894
+ headers: buildHeaders(
895
+ Array.isArray(originalHeaders) ? originalHeaders[i] ?? {} : originalHeaders
896
+ )
897
+ }))
898
+ };
899
+ }
900
+ async function sendToRetryTopic(originalTopic, rawMessages, attempt, maxRetries, delayMs, originalHeaders, deps) {
901
+ const payload = buildRetryTopicPayload(
902
+ originalTopic,
903
+ rawMessages,
904
+ attempt,
905
+ maxRetries,
906
+ delayMs,
907
+ originalHeaders
908
+ );
909
+ try {
910
+ await deps.producer.send(payload);
911
+ deps.logger.warn(
912
+ `Message queued in retry topic ${payload.topic} (attempt ${attempt}/${maxRetries})`
913
+ );
914
+ } catch (error) {
915
+ const err = toError(error);
916
+ deps.logger.error(
917
+ `Failed to send message to retry topic ${payload.topic}:`,
918
+ err.stack
919
+ );
920
+ await deps.onMessageLost?.({
921
+ topic: originalTopic,
922
+ error: err,
923
+ attempt,
924
+ headers: Array.isArray(originalHeaders) ? originalHeaders[0] ?? {} : originalHeaders
925
+ });
926
+ }
927
+ }
928
+ function buildDuplicateTopicPayload(sourceTopic, rawMessage, destinationTopic, meta) {
929
+ const headers = {
930
+ ...meta?.originalHeaders ?? {},
931
+ "x-duplicate-original-topic": sourceTopic,
932
+ "x-duplicate-detected-at": (/* @__PURE__ */ new Date()).toISOString(),
933
+ "x-duplicate-reason": "lamport-clock-duplicate",
934
+ "x-duplicate-incoming-clock": String(meta?.incomingClock ?? 0),
935
+ "x-duplicate-last-processed-clock": String(meta?.lastProcessedClock ?? 0)
936
+ };
937
+ return {
938
+ topic: destinationTopic,
939
+ messages: [{ value: rawMessage, headers }]
940
+ };
941
+ }
942
+ async function sendToDuplicatesTopic(sourceTopic, rawMessage, destinationTopic, deps, meta) {
943
+ const payload = buildDuplicateTopicPayload(
944
+ sourceTopic,
945
+ rawMessage,
946
+ destinationTopic,
947
+ meta
948
+ );
949
+ try {
950
+ await deps.producer.send(payload);
951
+ deps.logger.warn(`Duplicate message forwarded to ${destinationTopic}`);
952
+ } catch (error) {
953
+ deps.logger.error(
954
+ `Failed to forward duplicate to ${destinationTopic}:`,
955
+ toError(error).stack
956
+ );
957
+ }
958
+ }
959
+ async function broadcastToInterceptors(envelopes, interceptors, cb) {
960
+ for (const env of envelopes) {
961
+ for (const interceptor of interceptors) {
962
+ await cb(interceptor, env);
963
+ }
964
+ }
965
+ }
966
+ async function runHandlerWithPipeline(fn, envelopes, interceptors, instrumentation) {
967
+ const cleanups = [];
968
+ const wraps = [];
969
+ try {
970
+ for (const env of envelopes) {
971
+ for (const inst of instrumentation) {
972
+ const result = inst.beforeConsume?.(env);
973
+ if (typeof result === "function") {
974
+ cleanups.push(result);
975
+ } else if (result) {
976
+ if (result.cleanup) cleanups.push(result.cleanup);
977
+ if (result.wrap) wraps.push(result.wrap);
978
+ }
979
+ }
980
+ }
981
+ for (const env of envelopes) {
982
+ for (const interceptor of interceptors) {
983
+ await interceptor.before?.(env);
984
+ }
985
+ }
986
+ let runFn = fn;
987
+ for (let i = wraps.length - 1; i >= 0; i--) {
988
+ const wrap = wraps[i];
989
+ const inner = runFn;
990
+ runFn = () => wrap(inner);
991
+ }
992
+ await runFn();
993
+ for (const env of envelopes) {
994
+ for (const interceptor of interceptors) {
995
+ await interceptor.after?.(env);
996
+ }
997
+ }
998
+ for (const cleanup of cleanups) cleanup();
999
+ return null;
1000
+ } catch (error) {
1001
+ const err = toError(error);
1002
+ for (const env of envelopes) {
1003
+ for (const inst of instrumentation) {
1004
+ inst.onConsumeError?.(env, err);
1005
+ }
1006
+ }
1007
+ for (const cleanup of cleanups) cleanup();
1008
+ return err;
1009
+ }
1010
+ }
1011
+ async function notifyInterceptorsOnError(envelopes, interceptors, error) {
1012
+ await broadcastToInterceptors(
1013
+ envelopes,
1014
+ interceptors,
1015
+ (i, env) => i.onError?.(env, error)
1016
+ );
1017
+ }
1018
+ async function executeWithRetry(fn, ctx, deps) {
1019
+ const {
1020
+ envelope,
1021
+ rawMessages,
1022
+ interceptors,
1023
+ dlq,
1024
+ retry,
1025
+ isBatch,
1026
+ retryTopics
1027
+ } = ctx;
1028
+ const maxAttempts = retryTopics ? 1 : retry ? retry.maxRetries + 1 : 1;
1029
+ const backoffMs = retry?.backoffMs ?? 1e3;
1030
+ const maxBackoffMs = retry?.maxBackoffMs ?? 3e4;
1031
+ const envelopes = Array.isArray(envelope) ? envelope : [envelope];
1032
+ const topic2 = envelopes[0]?.topic ?? "unknown";
1033
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1034
+ const error = await runHandlerWithPipeline(
1035
+ fn,
1036
+ envelopes,
1037
+ interceptors,
1038
+ deps.instrumentation
1039
+ );
1040
+ if (!error) {
1041
+ if (deps.eosCommitOnSuccess) {
1042
+ try {
1043
+ await deps.eosCommitOnSuccess();
1044
+ } catch (commitErr) {
1045
+ deps.logger.error(
1046
+ `EOS offset commit failed after successful handler \u2014 message will be redelivered:`,
1047
+ toError(commitErr).stack
1048
+ );
1049
+ return;
1050
+ }
1051
+ }
1052
+ for (const env of envelopes) deps.onMessage?.(env);
1053
+ return;
1054
+ }
1055
+ deps.onFailure?.(envelopes[0]);
1056
+ const isLastAttempt = attempt === maxAttempts;
1057
+ const reportedError = isLastAttempt && maxAttempts > 1 ? new KafkaRetryExhaustedError(
1058
+ topic2,
1059
+ envelopes.map((e) => e.payload),
1060
+ maxAttempts,
1061
+ { cause: error }
1062
+ ) : error;
1063
+ await notifyInterceptorsOnError(envelopes, interceptors, reportedError);
1064
+ deps.logger.error(
1065
+ `Error processing ${isBatch ? "batch" : "message"} from topic ${topic2} (attempt ${attempt}/${maxAttempts}):`,
1066
+ error.stack
1067
+ );
1068
+ if (retryTopics && retry) {
1069
+ const cap = Math.min(backoffMs, maxBackoffMs);
1070
+ const delay = Math.floor(Math.random() * cap);
1071
+ if (deps.eosRouteToRetry) {
1072
+ try {
1073
+ await deps.eosRouteToRetry(rawMessages, envelopes, delay);
1074
+ deps.onRetry?.(envelopes[0], 1, retry.maxRetries);
1075
+ } catch (txErr) {
1076
+ deps.logger.error(
1077
+ `EOS routing to retry topic failed \u2014 message will be redelivered:`,
1078
+ toError(txErr).stack
1079
+ );
1080
+ }
1081
+ } else {
1082
+ await sendToRetryTopic(
1083
+ topic2,
1084
+ rawMessages,
1085
+ 1,
1086
+ retry.maxRetries,
1087
+ delay,
1088
+ isBatch ? envelopes.map((e) => e.headers) : envelopes[0]?.headers ?? {},
1089
+ deps
1090
+ );
1091
+ deps.onRetry?.(envelopes[0], 1, retry.maxRetries);
1092
+ }
1093
+ } else if (isLastAttempt) {
1094
+ if (dlq) {
1095
+ for (let i = 0; i < rawMessages.length; i++) {
1096
+ await sendToDlq(topic2, rawMessages[i], deps, {
1097
+ error,
1098
+ attempt,
1099
+ originalHeaders: envelopes[i]?.headers
1100
+ });
1101
+ deps.onDlq?.(envelopes[i] ?? envelopes[0], "handler-error");
1102
+ }
1103
+ } else {
1104
+ await deps.onMessageLost?.({
1105
+ topic: topic2,
1106
+ error,
1107
+ attempt,
1108
+ headers: envelopes[0]?.headers ?? {}
1109
+ });
1110
+ }
1111
+ } else {
1112
+ const cap = Math.min(backoffMs * 2 ** (attempt - 1), maxBackoffMs);
1113
+ deps.onRetry?.(envelopes[0], attempt, maxAttempts - 1);
1114
+ await sleep(Math.floor(Math.random() * cap));
1115
+ }
1116
+ }
1117
+ }
1118
+
1119
+ // src/client/kafka.client/consumer/subscribe-retry.ts
1120
+ async function subscribeWithRetry(consumer, topics, logger, retryOpts) {
1121
+ const maxAttempts = retryOpts?.retries ?? 5;
1122
+ const backoffMs = retryOpts?.backoffMs ?? 5e3;
1123
+ const displayTopics = topics.map((t) => t instanceof RegExp ? t.toString() : t).join(", ");
1124
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1125
+ try {
1126
+ await consumer.subscribe({ topics });
1127
+ return;
1128
+ } catch (error) {
1129
+ if (attempt === maxAttempts) throw error;
1130
+ const msg = toError(error).message;
1131
+ const delay = Math.floor(Math.random() * backoffMs);
1132
+ logger.warn(
1133
+ `Failed to subscribe to [${displayTopics}] (attempt ${attempt}/${maxAttempts}): ${msg}. Retrying in ${delay}ms...`
1134
+ );
1135
+ await sleep(delay);
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ // src/client/kafka.client/consumer/features/dlq-replay.ts
1141
+ async function replayDlqTopic(topic2, deps, options = {}) {
1142
+ if (topic2.endsWith(".dlq")) {
1143
+ throw new Error(
1144
+ `replayDlq: pass the ORIGINAL topic name \u2014 "${topic2}" already ends in ".dlq" (the ".dlq" suffix is appended internally, so this would read "${topic2}.dlq")`
1145
+ );
1146
+ }
1147
+ const dlqTopic = `${topic2}.dlq`;
1148
+ const partitionOffsets = await deps.fetchTopicOffsets(dlqTopic);
1149
+ const activePartitions = partitionOffsets.filter(
1150
+ (p) => Number.parseInt(p.high, 10) > Number.parseInt(p.low, 10)
1151
+ );
1152
+ if (activePartitions.length === 0) {
1153
+ deps.logger.log(`replayDlq: "${dlqTopic}" is empty \u2014 nothing to replay`);
1154
+ return { replayed: 0, skipped: 0 };
1155
+ }
1156
+ const highWatermarks = new Map(
1157
+ activePartitions.map(({ partition, high }) => [partition, Number.parseInt(high, 10)])
1158
+ );
1159
+ const processedOffsets = /* @__PURE__ */ new Map();
1160
+ let replayed = 0;
1161
+ let skipped = 0;
1162
+ const fromBeginning = options.fromBeginning ?? true;
1163
+ const groupId = fromBeginning ? `${dlqTopic}-replay-${Date.now()}` : `${dlqTopic}-replay`;
1164
+ await new Promise((resolve, reject) => {
1165
+ const consumer = deps.createConsumer(groupId, fromBeginning);
1166
+ const cleanup = () => deps.cleanupConsumer(consumer, groupId, fromBeginning);
1167
+ consumer.connect().then(() => subscribeWithRetry(consumer, [dlqTopic], deps.logger)).then(
1168
+ () => consumer.run({
1169
+ eachMessage: async ({ partition, message }) => {
1170
+ if (!message.value) return;
1171
+ const offset = Number.parseInt(message.offset, 10);
1172
+ processedOffsets.set(partition, offset);
1173
+ const headers = decodeHeaders(message.headers);
1174
+ const targetTopic = options.targetTopic ?? headers["x-dlq-original-topic"];
1175
+ const originalHeaders = Object.fromEntries(
1176
+ Object.entries(headers).filter(([k]) => !deps.dlqHeaderKeys.has(k))
1177
+ );
1178
+ const bytes = message.value;
1179
+ const shouldProcess = !options.filter || options.filter(headers, bytes.toString("utf8"));
1180
+ if (!targetTopic || !shouldProcess) {
1181
+ skipped++;
1182
+ } else if (options.dryRun) {
1183
+ deps.logger.log(`[DLQ replay dry-run] Would replay to "${targetTopic}"`);
1184
+ replayed++;
1185
+ } else {
1186
+ await deps.send(targetTopic, [{ value: bytes, headers: originalHeaders }]);
1187
+ replayed++;
1188
+ }
1189
+ const allDone = Array.from(highWatermarks.entries()).every(
1190
+ ([p, hwm]) => (processedOffsets.get(p) ?? -1) >= hwm - 1
1191
+ );
1192
+ if (allDone) {
1193
+ cleanup();
1194
+ resolve();
1195
+ }
1196
+ }
1197
+ })
1198
+ ).catch((err) => {
1199
+ cleanup();
1200
+ reject(err);
1201
+ });
1202
+ });
1203
+ deps.logger.log(`replayDlq: replayed ${replayed}, skipped ${skipped} from "${dlqTopic}"`);
1204
+ return { replayed, skipped };
1205
+ }
1206
+
1207
+ // src/client/kafka.client/infra/metrics.manager.ts
1208
+ var MetricsManager = class {
1209
+ constructor(deps) {
1210
+ this.deps = deps;
1211
+ }
1212
+ deps;
1213
+ topicMetrics = /* @__PURE__ */ new Map();
1214
+ metricsFor(topic2) {
1215
+ let m = this.topicMetrics.get(topic2);
1216
+ if (!m) {
1217
+ m = { processedCount: 0, retryCount: 0, dlqCount: 0, dedupCount: 0 };
1218
+ this.topicMetrics.set(topic2, m);
1219
+ }
1220
+ return m;
1221
+ }
1222
+ /** Fire `afterSend` instrumentation hooks for each message in a batch. */
1223
+ notifyAfterSend(topic2, count) {
1224
+ for (let i = 0; i < count; i++)
1225
+ for (const inst of this.deps.instrumentation) inst.afterSend?.(topic2);
1226
+ }
1227
+ /**
1228
+ * Increment the retry counter for the envelope's topic and fire all `onRetry` instrumentation hooks.
1229
+ * @param envelope The message envelope being retried.
1230
+ * @param attempt Current retry attempt number (1-based).
1231
+ * @param maxRetries Maximum number of retries configured for this consumer.
1232
+ */
1233
+ notifyRetry(envelope, attempt, maxRetries) {
1234
+ this.metricsFor(envelope.topic).retryCount++;
1235
+ for (const inst of this.deps.instrumentation) inst.onRetry?.(envelope, attempt, maxRetries);
1236
+ }
1237
+ /**
1238
+ * Increment the DLQ counter for the envelope's topic and fire all `onDlq` instrumentation hooks.
1239
+ * Circuit breaker failures are recorded separately via `notifyFailure` at the
1240
+ * handler-error boundary — dead-lettering itself is not a circuit event.
1241
+ * @param envelope The message envelope being sent to the DLQ.
1242
+ * @param reason The reason the message is being dead-lettered.
1243
+ */
1244
+ notifyDlq(envelope, reason) {
1245
+ this.metricsFor(envelope.topic).dlqCount++;
1246
+ for (const inst of this.deps.instrumentation) inst.onDlq?.(envelope, reason);
1247
+ }
1248
+ /**
1249
+ * Notify the circuit breaker of a handler failure. Fired on every failed
1250
+ * handler attempt (in-process retries and retry-topic levels included),
1251
+ * independent of whether the message is ultimately dead-lettered.
1252
+ * @param envelope The message envelope whose handler failed.
1253
+ * @param gid Consumer group ID — used to drive circuit breaker state.
1254
+ */
1255
+ notifyFailure(envelope, gid) {
1256
+ this.deps.onCircuitFailure(envelope, gid);
1257
+ }
1258
+ /**
1259
+ * Increment the deduplication counter for the envelope's topic and fire all `onDuplicate` hooks.
1260
+ * @param envelope The duplicate message envelope.
1261
+ * @param strategy The deduplication strategy applied (`"drop"`, `"dlq"`, or `"topic"`).
1262
+ */
1263
+ notifyDuplicate(envelope, strategy) {
1264
+ this.metricsFor(envelope.topic).dedupCount++;
1265
+ for (const inst of this.deps.instrumentation) inst.onDuplicate?.(envelope, strategy);
1266
+ }
1267
+ /**
1268
+ * Increment the processed counter for the envelope's topic, fire all `onMessage` hooks,
1269
+ * and notify the circuit breaker of a success (when `gid` is provided).
1270
+ * @param envelope The successfully processed message envelope.
1271
+ * @param gid Consumer group ID — used to drive circuit breaker state.
1272
+ */
1273
+ notifyMessage(envelope, gid) {
1274
+ this.metricsFor(envelope.topic).processedCount++;
1275
+ for (const inst of this.deps.instrumentation) inst.onMessage?.(envelope);
1276
+ if (gid) this.deps.onCircuitSuccess(envelope, gid);
1277
+ }
1278
+ /**
1279
+ * Return a snapshot of event counters.
1280
+ * @param topic When provided, returns counters for that topic only; otherwise aggregates all topics.
1281
+ * @returns Read-only `KafkaMetrics` snapshot. Returns zero-valued counters if the topic has no events.
1282
+ */
1283
+ getMetrics(topic2) {
1284
+ if (topic2 !== void 0) {
1285
+ const m = this.topicMetrics.get(topic2);
1286
+ return m ? { ...m } : { processedCount: 0, retryCount: 0, dlqCount: 0, dedupCount: 0 };
1287
+ }
1288
+ const agg = { processedCount: 0, retryCount: 0, dlqCount: 0, dedupCount: 0 };
1289
+ for (const m of this.topicMetrics.values()) {
1290
+ agg.processedCount += m.processedCount;
1291
+ agg.retryCount += m.retryCount;
1292
+ agg.dlqCount += m.dlqCount;
1293
+ agg.dedupCount += m.dedupCount;
1294
+ }
1295
+ return agg;
1296
+ }
1297
+ /**
1298
+ * Reset event counters to zero.
1299
+ * @param topic When provided, clears counters for that topic only; otherwise clears all topics.
1300
+ */
1301
+ resetMetrics(topic2) {
1302
+ if (topic2 !== void 0) {
1303
+ this.topicMetrics.delete(topic2);
1304
+ return;
1305
+ }
1306
+ this.topicMetrics.clear();
1307
+ }
1308
+ };
1309
+
1310
+ // src/client/kafka.client/infra/inflight.tracker.ts
1311
+ var InFlightTracker = class {
1312
+ constructor(warn) {
1313
+ this.warn = warn;
1314
+ }
1315
+ warn;
1316
+ inFlightTotal = 0;
1317
+ drainResolvers = [];
1318
+ /**
1319
+ * Wrap an async handler so its lifetime is counted against the in-flight total.
1320
+ * Resolvers registered with `waitForDrain` are notified when the count reaches zero.
1321
+ * @param fn The async function to track.
1322
+ * @returns The same promise returned by `fn`.
1323
+ */
1324
+ track(fn) {
1325
+ this.inFlightTotal++;
1326
+ const done = () => {
1327
+ this.inFlightTotal--;
1328
+ if (this.inFlightTotal === 0) this.drainResolvers.splice(0).forEach((r) => r());
1329
+ };
1330
+ try {
1331
+ return fn().finally(done);
1332
+ } catch (err) {
1333
+ done();
1334
+ throw err;
1335
+ }
1336
+ }
1337
+ /**
1338
+ * Resolve when all tracked handlers have completed, or after `timeoutMs` elapses.
1339
+ * Logs a warning (via the injected `warn` callback) if the timeout is hit before draining.
1340
+ * Returns immediately if there are no in-flight handlers.
1341
+ * @param timeoutMs Maximum time to wait in milliseconds before resolving anyway.
1342
+ */
1343
+ waitForDrain(timeoutMs) {
1344
+ if (this.inFlightTotal === 0) return Promise.resolve();
1345
+ return new Promise((resolve) => {
1346
+ let handle;
1347
+ const onDrain = () => {
1348
+ clearTimeout(handle);
1349
+ resolve();
1350
+ };
1351
+ this.drainResolvers.push(onDrain);
1352
+ handle = setTimeout(() => {
1353
+ const idx = this.drainResolvers.indexOf(onDrain);
1354
+ if (idx !== -1) this.drainResolvers.splice(idx, 1);
1355
+ this.warn(
1356
+ `Drain timed out after ${timeoutMs}ms \u2014 ${this.inFlightTotal} handler(s) still in flight`
1357
+ );
1358
+ resolve();
1359
+ }, timeoutMs);
1360
+ });
1361
+ }
1362
+ };
1363
+
1364
+ // src/client/kafka.client/infra/circuit-breaker.manager.ts
1365
+ var CircuitBreakerManager = class {
1366
+ constructor(deps) {
1367
+ this.deps = deps;
1368
+ }
1369
+ deps;
1370
+ states = /* @__PURE__ */ new Map();
1371
+ configs = /* @__PURE__ */ new Map();
1372
+ /**
1373
+ * Register or update circuit breaker configuration for a consumer group.
1374
+ * Must be called before the group starts consuming for the config to take effect.
1375
+ * @param gid Consumer group ID.
1376
+ * @param options Circuit breaker thresholds and timing configuration.
1377
+ */
1378
+ setConfig(gid, options) {
1379
+ this.configs.set(gid, options);
1380
+ }
1381
+ /**
1382
+ * Returns a snapshot of the circuit breaker state for a given topic-partition.
1383
+ * Returns `undefined` when no state exists for the key.
1384
+ */
1385
+ getState(topic2, partition, gid) {
1386
+ const state = this.states.get(`${gid}:${topic2}:${partition}`);
1387
+ if (!state) return void 0;
1388
+ return {
1389
+ status: state.status,
1390
+ failures: state.window.filter((v) => !v).length,
1391
+ windowSize: state.window.length
1392
+ };
1393
+ }
1394
+ /**
1395
+ * Record a failure for the given envelope and group.
1396
+ * Drives the CLOSED → OPEN and HALF-OPEN → OPEN transitions.
1397
+ */
1398
+ onFailure(envelope, gid) {
1399
+ const cfg = this.configs.get(gid);
1400
+ if (!cfg) return;
1401
+ const threshold = cfg.threshold ?? 5;
1402
+ const recoveryMs = cfg.recoveryMs ?? 3e4;
1403
+ const stateKey = `${gid}:${envelope.topic}:${envelope.partition}`;
1404
+ let state = this.states.get(stateKey);
1405
+ if (!state) {
1406
+ state = { status: "closed", window: [], successes: 0 };
1407
+ this.states.set(stateKey, state);
1408
+ }
1409
+ if (state.status === "open") return;
1410
+ const openCircuit = () => {
1411
+ state.status = "open";
1412
+ state.window = [];
1413
+ state.successes = 0;
1414
+ clearTimeout(state.timer);
1415
+ for (const inst of this.deps.instrumentation)
1416
+ inst.onCircuitOpen?.(envelope.topic, envelope.partition);
1417
+ this.deps.pauseConsumer(gid, [{ topic: envelope.topic, partitions: [envelope.partition] }]);
1418
+ state.timer = setTimeout(() => {
1419
+ state.status = "half-open";
1420
+ state.successes = 0;
1421
+ this.deps.logger.log(
1422
+ `[CircuitBreaker] HALF-OPEN \u2014 group="${gid}" topic="${envelope.topic}" partition=${envelope.partition}`
1423
+ );
1424
+ for (const inst of this.deps.instrumentation)
1425
+ inst.onCircuitHalfOpen?.(envelope.topic, envelope.partition);
1426
+ this.deps.resumeConsumer(gid, [{ topic: envelope.topic, partitions: [envelope.partition] }]);
1427
+ }, recoveryMs);
1428
+ };
1429
+ if (state.status === "half-open") {
1430
+ clearTimeout(state.timer);
1431
+ this.deps.logger.warn(
1432
+ `[CircuitBreaker] OPEN (half-open failure) \u2014 group="${gid}" topic="${envelope.topic}" partition=${envelope.partition}`
1433
+ );
1434
+ openCircuit();
1435
+ return;
1436
+ }
1437
+ const windowSize = cfg.windowSize ?? Math.max(threshold * 2, 10);
1438
+ state.window = [...state.window, false];
1439
+ if (state.window.length > windowSize) {
1440
+ state.window = state.window.slice(state.window.length - windowSize);
1441
+ }
1442
+ const failures = state.window.filter((v) => !v).length;
1443
+ if (failures >= threshold) {
1444
+ this.deps.logger.warn(
1445
+ `[CircuitBreaker] OPEN \u2014 group="${gid}" topic="${envelope.topic}" partition=${envelope.partition} (${failures}/${state.window.length} failures, threshold=${threshold})`
1446
+ );
1447
+ openCircuit();
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Record a success for the given envelope and group.
1452
+ * Drives the HALF-OPEN → CLOSED transition and updates the success window.
1453
+ */
1454
+ onSuccess(envelope, gid) {
1455
+ const cfg = this.configs.get(gid);
1456
+ if (!cfg) return;
1457
+ const stateKey = `${gid}:${envelope.topic}:${envelope.partition}`;
1458
+ const state = this.states.get(stateKey);
1459
+ if (!state) return;
1460
+ const halfOpenSuccesses = cfg.halfOpenSuccesses ?? 1;
1461
+ if (state.status === "half-open") {
1462
+ state.successes++;
1463
+ if (state.successes >= halfOpenSuccesses) {
1464
+ clearTimeout(state.timer);
1465
+ state.timer = void 0;
1466
+ state.status = "closed";
1467
+ state.window = [];
1468
+ state.successes = 0;
1469
+ this.deps.logger.log(
1470
+ `[CircuitBreaker] CLOSED \u2014 group="${gid}" topic="${envelope.topic}" partition=${envelope.partition}`
1471
+ );
1472
+ for (const inst of this.deps.instrumentation)
1473
+ inst.onCircuitClose?.(envelope.topic, envelope.partition);
1474
+ }
1475
+ } else if (state.status === "closed") {
1476
+ const threshold = cfg.threshold ?? 5;
1477
+ const windowSize = cfg.windowSize ?? Math.max(threshold * 2, 10);
1478
+ state.window = [...state.window, true];
1479
+ if (state.window.length > windowSize) {
1480
+ state.window = state.window.slice(state.window.length - windowSize);
1481
+ }
1482
+ }
1483
+ }
1484
+ /**
1485
+ * Remove all circuit state and config for the given group.
1486
+ * Called when a consumer is stopped via `stopConsumer(groupId)`.
1487
+ */
1488
+ removeGroup(gid) {
1489
+ for (const key of [...this.states.keys()]) {
1490
+ if (key.startsWith(`${gid}:`)) {
1491
+ clearTimeout(this.states.get(key).timer);
1492
+ this.states.delete(key);
1493
+ }
1494
+ }
1495
+ this.configs.delete(gid);
1496
+ }
1497
+ /** Clear all circuit state and config. Called on `disconnect()`. */
1498
+ clear() {
1499
+ for (const state of this.states.values()) clearTimeout(state.timer);
1500
+ this.states.clear();
1501
+ this.configs.clear();
1502
+ }
1503
+ };
1504
+
1505
+ // src/client/kafka.client/consumer/queue.ts
1506
+ var AsyncQueue = class {
1507
+ constructor(highWaterMark = Infinity, onFull = () => {
1508
+ }, onDrained = () => {
1509
+ }) {
1510
+ this.highWaterMark = highWaterMark;
1511
+ this.onFull = onFull;
1512
+ this.onDrained = onDrained;
1513
+ }
1514
+ highWaterMark;
1515
+ onFull;
1516
+ onDrained;
1517
+ items = [];
1518
+ waiting = [];
1519
+ closed = false;
1520
+ error;
1521
+ paused = false;
1522
+ /**
1523
+ * Enqueue an item. If a consumer is already awaiting the next item, delivers it immediately.
1524
+ * When the internal buffer reaches `highWaterMark`, calls `onFull` to signal backpressure.
1525
+ * @param item The value to enqueue.
1526
+ */
1527
+ push(item) {
1528
+ if (this.closed) return;
1529
+ if (this.waiting.length > 0) {
1530
+ this.waiting.shift().resolve({ value: item, done: false });
1531
+ } else {
1532
+ this.items.push(item);
1533
+ if (!this.paused && this.items.length >= this.highWaterMark) {
1534
+ this.paused = true;
1535
+ this.onFull();
1536
+ }
1537
+ }
1538
+ }
1539
+ /**
1540
+ * Terminate the queue with an error. All pending and future `next()` calls will reject.
1541
+ * @param err The error to propagate to all waiting consumers.
1542
+ */
1543
+ fail(err) {
1544
+ this.closed = true;
1545
+ this.error = err;
1546
+ for (const { reject } of this.waiting.splice(0)) reject(err);
1547
+ }
1548
+ /**
1549
+ * Signal end-of-stream. All pending `next()` calls resolve with `{ done: true }`,
1550
+ * and future `next()` calls resolve immediately with `{ done: true }`.
1551
+ */
1552
+ close() {
1553
+ this.closed = true;
1554
+ for (const { resolve } of this.waiting.splice(0))
1555
+ resolve({ value: void 0, done: true });
1556
+ }
1557
+ /**
1558
+ * Pull the next item from the queue, conforming to the `AsyncIterator` protocol.
1559
+ * Rejects if the queue has been failed. Resolves with `{ done: true }` if it has been closed.
1560
+ * Suspends (returns a pending Promise) when the queue is empty and not yet closed.
1561
+ * When items drain below `highWaterMark / 2`, calls `onDrained` to resume backpressure.
1562
+ * @returns A Promise resolving to an `IteratorResult<V>`.
1563
+ */
1564
+ next() {
1565
+ if (this.error) return Promise.reject(this.error);
1566
+ if (this.items.length > 0) {
1567
+ const value = this.items.shift();
1568
+ if (this.paused && this.items.length <= Math.floor(this.highWaterMark / 2)) {
1569
+ this.paused = false;
1570
+ this.onDrained();
1571
+ }
1572
+ return Promise.resolve({ value, done: false });
1573
+ }
1574
+ if (this.closed) return Promise.resolve({ value: void 0, done: true });
1575
+ return new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
1576
+ }
1577
+ };
1578
+
1579
+ // src/client/kafka.client/validate-options.ts
1580
+ function validateClientOptions(clientId, groupId, brokers, options) {
1581
+ const problems = [];
1582
+ if (typeof clientId !== "string" || clientId.trim() === "") {
1583
+ problems.push("clientId must be a non-empty string");
1584
+ }
1585
+ if (typeof groupId !== "string" || groupId.trim() === "") {
1586
+ problems.push("groupId must be a non-empty string");
1587
+ }
1588
+ if (!Array.isArray(brokers) || brokers.length === 0 && !options?.transport) {
1589
+ problems.push("brokers must be a non-empty array of broker addresses");
1590
+ } else if (brokers.some((b) => typeof b !== "string" || b.trim() === "")) {
1591
+ problems.push("brokers must not contain empty entries");
1592
+ }
1593
+ if (options) {
1594
+ const {
1595
+ numPartitions,
1596
+ transactionalId,
1597
+ clockRecovery,
1598
+ lagThrottle
1599
+ } = options;
1600
+ if (numPartitions !== void 0 && (!Number.isInteger(numPartitions) || numPartitions < 1)) {
1601
+ problems.push(
1602
+ `numPartitions must be a positive integer (got ${numPartitions})`
1603
+ );
1604
+ }
1605
+ if (transactionalId !== void 0 && transactionalId.trim() === "") {
1606
+ problems.push("transactionalId must be a non-empty string when set");
1607
+ }
1608
+ if (clockRecovery) {
1609
+ if (!Array.isArray(clockRecovery.topics)) {
1610
+ problems.push("clockRecovery.topics must be an array of topic names");
1611
+ }
1612
+ if (clockRecovery.timeoutMs !== void 0 && !(clockRecovery.timeoutMs > 0)) {
1613
+ problems.push(
1614
+ `clockRecovery.timeoutMs must be > 0 (got ${clockRecovery.timeoutMs})`
1615
+ );
1616
+ }
1617
+ }
1618
+ if (lagThrottle) {
1619
+ if (!(lagThrottle.maxLag >= 0)) {
1620
+ problems.push(`lagThrottle.maxLag must be >= 0 (got ${lagThrottle.maxLag})`);
1621
+ }
1622
+ if (lagThrottle.pollIntervalMs !== void 0 && !(lagThrottle.pollIntervalMs > 0)) {
1623
+ problems.push(
1624
+ `lagThrottle.pollIntervalMs must be > 0 (got ${lagThrottle.pollIntervalMs})`
1625
+ );
1626
+ }
1627
+ if (lagThrottle.maxWaitMs !== void 0 && !(lagThrottle.maxWaitMs >= 0)) {
1628
+ problems.push(
1629
+ `lagThrottle.maxWaitMs must be >= 0 (got ${lagThrottle.maxWaitMs})`
1630
+ );
1631
+ }
1632
+ }
1633
+ }
1634
+ if (problems.length > 0) {
1635
+ throw new Error(
1636
+ `KafkaClient: invalid configuration:
1637
+ - ${problems.join("\n- ")}`
1638
+ );
1639
+ }
1640
+ }
1641
+
1642
+ // src/client/security/resolve-security.ts
1643
+ var LOCAL_HOST_PATTERNS = [
1644
+ /^localhost(:\d+)?$/i,
1645
+ /^127\.\d+\.\d+\.\d+(:\d+)?$/,
1646
+ /^\[?::1\]?(:\d+)?$/,
1647
+ /^0\.0\.0\.0(:\d+)?$/,
1648
+ /^host\.docker\.internal(:\d+)?$/i
1649
+ ];
1650
+ function isLocalBroker(broker) {
1651
+ return LOCAL_HOST_PATTERNS.some((re) => re.test(broker.trim()));
1652
+ }
1653
+ function resolveSecurityOptions(security, brokers, logger) {
1654
+ const hasRemoteBroker = brokers.some((b) => !isLocalBroker(b));
1655
+ if (!security?.sasl && security?.ssl !== true) {
1656
+ if (hasRemoteBroker && !security?.allowInsecure) {
1657
+ logger.warn(
1658
+ "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."
1659
+ );
1660
+ }
1661
+ return security;
1662
+ }
1663
+ if (security.sasl && security.ssl === void 0) {
1664
+ return { ...security, ssl: true };
1665
+ }
1666
+ if (security.sasl && security.ssl === false) {
1667
+ logger.warn(
1668
+ "SASL credentials are configured with `ssl: false` \u2014 credentials will be sent over plaintext. This is only safe on fully trusted networks."
1669
+ );
1670
+ }
1671
+ return security;
1672
+ }
1673
+
1674
+ // src/client/kafka.client/producer/lifecycle.ts
1675
+ var _activeTransactionalIds = /* @__PURE__ */ new Set();
1676
+ async function ensureTopic(ctx, topic2) {
1677
+ if (!ctx.autoCreateTopicsEnabled || ctx.ensuredTopics.has(topic2)) return;
1678
+ let p = ctx.ensureTopicPromises.get(topic2);
1679
+ if (!p) {
1680
+ p = (async () => {
1681
+ await ctx.adminOps.ensureConnected();
1682
+ await ctx.adminOps.admin.createTopics({
1683
+ topics: [{ topic: topic2, numPartitions: ctx.numPartitions }]
1684
+ });
1685
+ ctx.ensuredTopics.add(topic2);
1686
+ })().finally(() => ctx.ensureTopicPromises.delete(topic2));
1687
+ ctx.ensureTopicPromises.set(topic2, p);
1688
+ }
1689
+ await p;
1690
+ }
1691
+ async function createRetryTxProducer(ctx, transactionalId) {
1692
+ if (_activeTransactionalIds.has(transactionalId)) {
1693
+ ctx.logger.warn(
1694
+ `transactionalId "${transactionalId}" is already in use by another KafkaClient in this process. Kafka will fence one of the producers. Set a unique \`transactionalId\` (or distinct \`clientId\`) per instance.`
1695
+ );
1696
+ }
1697
+ const p = ctx.transport.producer({
1698
+ idempotent: true,
1699
+ transactionalId
1700
+ });
1701
+ await p.connect();
1702
+ _activeTransactionalIds.add(transactionalId);
1703
+ ctx.retryTxProducers.set(transactionalId, p);
1704
+ return p;
1705
+ }
1706
+ async function connectProducerImpl(ctx) {
1707
+ await ctx.producer.connect();
1708
+ await recoverLamportClockImpl(ctx, ctx.clockRecoveryTopics);
1709
+ if (ctx.lagThrottleOpts) startLagThrottlePoller(ctx);
1710
+ ctx.logger.log("Producer connected");
1711
+ }
1712
+ async function disconnectImpl(ctx, drainTimeoutMs = 3e4) {
1713
+ if (ctx._lagThrottleTimer) {
1714
+ clearInterval(ctx._lagThrottleTimer);
1715
+ ctx._lagThrottleTimer = void 0;
1716
+ }
1717
+ await ctx.inFlight.waitForDrain(drainTimeoutMs);
1718
+ const tasks = [ctx.producer.disconnect()];
1719
+ if (ctx.txProducer) {
1720
+ tasks.push(ctx.txProducer.disconnect());
1721
+ _activeTransactionalIds.delete(ctx.txId);
1722
+ ctx.txProducer = void 0;
1723
+ ctx.txProducerInitPromise = void 0;
1724
+ }
1725
+ for (const txId of ctx.retryTxProducers.keys())
1726
+ _activeTransactionalIds.delete(txId);
1727
+ for (const p of ctx.retryTxProducers.values()) tasks.push(p.disconnect());
1728
+ ctx.retryTxProducers.clear();
1729
+ for (const consumer of ctx.consumers.values())
1730
+ tasks.push(consumer.disconnect());
1731
+ tasks.push(ctx.adminOps.disconnect());
1732
+ await Promise.allSettled(tasks);
1733
+ ctx.consumers.clear();
1734
+ ctx.runningConsumers.clear();
1735
+ ctx.consumerCreationOptions.clear();
1736
+ ctx.companionGroupIds.clear();
1737
+ ctx.circuitBreaker.clear();
1738
+ ctx.logger.log("All connections closed");
1739
+ }
1740
+ function startLagThrottlePoller(ctx) {
1741
+ const opts = ctx.lagThrottleOpts;
1742
+ const { maxLag, pollIntervalMs = 5e3 } = opts;
1743
+ const groupId = opts.groupId;
1744
+ const poll = async () => {
1745
+ try {
1746
+ const lags = await ctx.adminOps.getConsumerLag(groupId);
1747
+ const total = lags.reduce((sum, e) => sum + e.lag, 0);
1748
+ if (total > maxLag && !ctx._lagThrottled) {
1749
+ ctx._lagThrottled = true;
1750
+ ctx.logger.warn(
1751
+ `lagThrottle: lag ${total} > ${maxLag} \u2014 producer sends will be delayed`
1752
+ );
1753
+ } else if (total <= maxLag && ctx._lagThrottled) {
1754
+ ctx._lagThrottled = false;
1755
+ ctx.logger.log(
1756
+ `lagThrottle: lag ${total} \u2264 ${maxLag} \u2014 producer sends resumed`
1757
+ );
1758
+ }
1759
+ } catch {
1760
+ }
1761
+ };
1762
+ ctx._lagThrottleTimer = setInterval(() => {
1763
+ void poll();
1764
+ }, pollIntervalMs);
1765
+ ctx._lagThrottleTimer.unref?.();
1766
+ }
1767
+ async function recoverLamportClockImpl(ctx, topics) {
1768
+ if (topics.length === 0) return;
1769
+ ctx.logger.log(
1770
+ `Clock recovery: scanning ${topics.length} topic(s) for Lamport clock...`
1771
+ );
1772
+ await ctx.adminOps.ensureConnected();
1773
+ const partitionsToRead = [];
1774
+ for (const t of topics) {
1775
+ let offsets;
1776
+ try {
1777
+ offsets = await ctx.adminOps.admin.fetchTopicOffsets(t);
1778
+ } catch {
1779
+ ctx.logger.warn(
1780
+ `Clock recovery: could not fetch offsets for "${t}", skipping`
1781
+ );
1782
+ continue;
1783
+ }
1784
+ for (const { partition, high, low } of offsets) {
1785
+ if (Number.parseInt(high, 10) > Number.parseInt(low, 10)) {
1786
+ partitionsToRead.push({
1787
+ topic: t,
1788
+ partition,
1789
+ lastOffset: String(Number.parseInt(high, 10) - 1)
1790
+ });
1791
+ }
1792
+ }
1793
+ }
1794
+ if (partitionsToRead.length === 0) {
1795
+ ctx.logger.log(
1796
+ "Clock recovery: all topics empty \u2014 keeping Lamport clock at 0"
1797
+ );
1798
+ return;
1799
+ }
1800
+ const recoveryGroupId = `${ctx.clientId}-clock-recovery-${Date.now()}`;
1801
+ let maxClock = -1;
1802
+ await new Promise((resolve, reject) => {
1803
+ const consumer = ctx.transport.consumer({ groupId: recoveryGroupId });
1804
+ const remaining = new Set(
1805
+ partitionsToRead.map((p) => `${p.topic}:${p.partition}`)
1806
+ );
1807
+ let settled = false;
1808
+ const cleanup = () => {
1809
+ consumer.disconnect().catch(() => {
1810
+ }).finally(() => {
1811
+ ctx.adminOps.deleteGroups([recoveryGroupId]).catch(() => {
1812
+ });
1813
+ });
1814
+ };
1815
+ const timeoutTimer = setTimeout(() => {
1816
+ if (settled) return;
1817
+ settled = true;
1818
+ ctx.logger.warn(
1819
+ `Clock recovery: timed out after ${ctx.clockRecoveryTimeoutMs} ms with ${remaining.size} partition(s) unread \u2014 proceeding with partial result`
1820
+ );
1821
+ cleanup();
1822
+ resolve();
1823
+ }, ctx.clockRecoveryTimeoutMs);
1824
+ timeoutTimer.unref?.();
1825
+ consumer.connect().then(async () => {
1826
+ const uniqueTopics = [
1827
+ ...new Set(partitionsToRead.map((p) => p.topic))
1828
+ ];
1829
+ await consumer.subscribe({ topics: uniqueTopics });
1830
+ for (const { topic: t, partition, lastOffset } of partitionsToRead) {
1831
+ consumer.seek({ topic: t, partition, offset: lastOffset });
1832
+ }
1833
+ }).then(
1834
+ () => consumer.run({
1835
+ eachMessage: async ({ topic: t, partition, message }) => {
1836
+ const key = `${t}:${partition}`;
1837
+ if (!remaining.has(key)) return;
1838
+ remaining.delete(key);
1839
+ const clockHeader = message.headers?.[HEADER_LAMPORT_CLOCK];
1840
+ if (clockHeader !== void 0) {
1841
+ const raw = Buffer.isBuffer(clockHeader) ? clockHeader.toString() : String(clockHeader);
1842
+ const clock = Number(raw);
1843
+ if (!Number.isNaN(clock) && clock > maxClock) maxClock = clock;
1844
+ }
1845
+ if (remaining.size === 0 && !settled) {
1846
+ settled = true;
1847
+ clearTimeout(timeoutTimer);
1848
+ cleanup();
1849
+ resolve();
1850
+ }
1851
+ }
1852
+ })
1853
+ ).catch((err) => {
1854
+ if (settled) return;
1855
+ settled = true;
1856
+ clearTimeout(timeoutTimer);
1857
+ cleanup();
1858
+ reject(err);
1859
+ });
1860
+ });
1861
+ if (maxClock >= 0) {
1862
+ ctx._lamportClock = maxClock;
1863
+ ctx.logger.log(
1864
+ `Clock recovery: Lamport clock restored \u2014 next clock will be ${maxClock + 1}`
1865
+ );
1866
+ } else {
1867
+ ctx.logger.log(
1868
+ "Clock recovery: no x-lamport-clock headers found \u2014 keeping clock at 0"
1869
+ );
1870
+ }
1871
+ }
1872
+ function wrapWithTimeoutWarning(logger, fn, timeoutMs, topic2) {
1873
+ let timer;
1874
+ const promise = fn().finally(() => {
1875
+ if (timer !== void 0) clearTimeout(timer);
1876
+ });
1877
+ timer = setTimeout(() => {
1878
+ logger.warn(
1879
+ `Handler for topic "${topic2}" has not resolved after ${timeoutMs}ms \u2014 possible stuck handler`
1880
+ );
1881
+ }, timeoutMs);
1882
+ return promise;
1883
+ }
1884
+
1885
+ // src/client/kafka.client/producer/send.ts
1886
+ async function preparePayload(ctx, topicOrDesc, messages, compression) {
1887
+ registerSchema(topicOrDesc, ctx.schemaRegistry, ctx.logger);
1888
+ const payload = await buildSendPayload(
1889
+ topicOrDesc,
1890
+ messages,
1891
+ ctx.producerOpsDeps,
1892
+ compression
1893
+ );
1894
+ await ensureTopic(ctx, payload.topic);
1895
+ return payload;
1896
+ }
1897
+ async function redirectToDelayed(ctx, payload, deliverAfterMs) {
1898
+ const until = String(Date.now() + deliverAfterMs);
1899
+ for (const m of payload.messages) {
1900
+ m.headers[HEADER_DELAYED_UNTIL] = until;
1901
+ m.headers[HEADER_DELAYED_TARGET] = payload.topic;
1902
+ }
1903
+ payload.topic = `${payload.topic}.delayed`;
1904
+ await ensureTopic(ctx, payload.topic);
1905
+ }
1906
+ async function sendMessageImpl(ctx, topicOrDesc, message, options = {}) {
1907
+ await waitIfThrottled(ctx);
1908
+ const payload = await preparePayload(
1909
+ ctx,
1910
+ topicOrDesc,
1911
+ [
1912
+ {
1913
+ value: message,
1914
+ key: options.key,
1915
+ headers: options.headers,
1916
+ correlationId: options.correlationId,
1917
+ schemaVersion: options.schemaVersion,
1918
+ eventId: options.eventId
1919
+ }
1920
+ ],
1921
+ options.compression
1922
+ );
1923
+ if (options.deliverAfterMs && options.deliverAfterMs > 0) {
1924
+ await redirectToDelayed(ctx, payload, options.deliverAfterMs);
1925
+ }
1926
+ await ctx.producer.send(payload);
1927
+ ctx.metrics.notifyAfterSend(payload.topic, payload.messages.length);
1928
+ }
1929
+ async function sendBatchImpl(ctx, topicOrDesc, messages, options) {
1930
+ await waitIfThrottled(ctx);
1931
+ const payload = await preparePayload(
1932
+ ctx,
1933
+ topicOrDesc,
1934
+ messages,
1935
+ options?.compression
1936
+ );
1937
+ if (options?.deliverAfterMs && options.deliverAfterMs > 0) {
1938
+ await redirectToDelayed(ctx, payload, options.deliverAfterMs);
1939
+ }
1940
+ await ctx.producer.send(payload);
1941
+ ctx.metrics.notifyAfterSend(payload.topic, payload.messages.length);
1942
+ }
1943
+ async function sendTombstoneImpl(ctx, topic2, key, headers) {
1944
+ await waitIfThrottled(ctx);
1945
+ const hdrs = { ...headers };
1946
+ for (const inst of ctx.instrumentation) inst.beforeSend?.(topic2, hdrs);
1947
+ await ensureTopic(ctx, topic2);
1948
+ await ctx.producer.send({
1949
+ topic: topic2,
1950
+ messages: [{ value: null, key, headers: hdrs }]
1951
+ });
1952
+ for (const inst of ctx.instrumentation) inst.afterSend?.(topic2);
1953
+ }
1954
+ async function transactionImpl(ctx, fn) {
1955
+ if (!ctx.txProducerInitPromise) {
1956
+ if (_activeTransactionalIds.has(ctx.txId)) {
1957
+ ctx.logger.warn(
1958
+ `transactionalId "${ctx.txId}" is already in use by another KafkaClient in this process. Kafka will fence one of the producers. Set a unique \`transactionalId\` (or distinct \`clientId\`) per instance.`
1959
+ );
1960
+ }
1961
+ const initPromise = (async () => {
1962
+ const p = ctx.transport.producer({
1963
+ idempotent: true,
1964
+ transactionalId: ctx.txId
1965
+ });
1966
+ await p.connect();
1967
+ _activeTransactionalIds.add(ctx.txId);
1968
+ return p;
1969
+ })();
1970
+ ctx.txProducerInitPromise = initPromise.catch((err) => {
1971
+ ctx.txProducerInitPromise = void 0;
1972
+ throw err;
1973
+ });
1974
+ }
1975
+ ctx.txProducer = await ctx.txProducerInitPromise;
1976
+ const prev = ctx._txChain;
1977
+ let release;
1978
+ ctx._txChain = new Promise((r) => release = r);
1979
+ await prev;
1980
+ try {
1981
+ await runTransaction(ctx, fn);
1982
+ } finally {
1983
+ release();
1984
+ }
1985
+ }
1986
+ async function runTransaction(ctx, fn) {
1987
+ const tx = await ctx.txProducer.transaction();
1988
+ try {
1989
+ const txCtx = {
1990
+ send: async (topicOrDesc, message, sendOpts = {}) => {
1991
+ const payload = await preparePayload(ctx, topicOrDesc, [
1992
+ {
1993
+ value: message,
1994
+ key: sendOpts.key,
1995
+ headers: sendOpts.headers,
1996
+ correlationId: sendOpts.correlationId,
1997
+ schemaVersion: sendOpts.schemaVersion,
1998
+ eventId: sendOpts.eventId
1999
+ }
2000
+ ]);
2001
+ await tx.send(payload);
2002
+ ctx.metrics.notifyAfterSend(payload.topic, payload.messages.length);
2003
+ },
2004
+ sendBatch: async (topicOrDesc, messages, batchOpts) => {
2005
+ const payload = await preparePayload(
2006
+ ctx,
2007
+ topicOrDesc,
2008
+ messages,
2009
+ batchOpts?.compression
2010
+ );
2011
+ await tx.send(payload);
2012
+ ctx.metrics.notifyAfterSend(payload.topic, payload.messages.length);
2013
+ }
2014
+ };
2015
+ await fn(txCtx);
2016
+ await tx.commit();
2017
+ } catch (error) {
2018
+ try {
2019
+ await tx.abort();
2020
+ } catch (abortError) {
2021
+ ctx.logger.error(
2022
+ "Failed to abort transaction:",
2023
+ (abortError instanceof Error ? abortError : new Error(String(abortError))).message
2024
+ );
2025
+ }
2026
+ throw error;
2027
+ }
2028
+ }
2029
+ async function waitIfThrottled(ctx) {
2030
+ if (!ctx._lagThrottled) return;
2031
+ const maxWait = ctx.lagThrottleOpts?.maxWaitMs ?? 3e4;
2032
+ const start = Date.now();
2033
+ while (ctx._lagThrottled) {
2034
+ if (Date.now() - start >= maxWait) {
2035
+ ctx.logger.warn(
2036
+ `lagThrottle: maxWaitMs (${maxWait} ms) exceeded \u2014 sending anyway`
2037
+ );
2038
+ return;
2039
+ }
2040
+ await new Promise((r) => setTimeout(r, 100));
2041
+ }
2042
+ }
2043
+
2044
+ // src/client/kafka.client/consumer/retry-topic.ts
2045
+ async function waitForPartitionAssignment(consumer, topics, logger, timeoutMs = 1e4) {
2046
+ const topicSet = new Set(topics);
2047
+ const deadline = Date.now() + timeoutMs;
2048
+ while (Date.now() < deadline) {
2049
+ try {
2050
+ const assigned = consumer.assignment();
2051
+ if (assigned.some((a) => topicSet.has(a.topic))) return;
2052
+ } catch {
2053
+ }
2054
+ await sleep(200);
2055
+ }
2056
+ logger.warn(
2057
+ `Retry consumer did not receive partition assignments for [${topics.join(", ")}] within ${timeoutMs}ms`
2058
+ );
2059
+ }
2060
+ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopics, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
2061
+ const {
2062
+ logger,
2063
+ producer,
2064
+ instrumentation,
2065
+ onMessageLost,
2066
+ onRetry,
2067
+ onDlq,
2068
+ onMessage,
2069
+ ensureTopic: ensureTopic2,
2070
+ getOrCreateConsumer: getOrCreateConsumer2,
2071
+ runningConsumers,
2072
+ createRetryTxProducer: createRetryTxProducer2
2073
+ } = deps;
2074
+ const backoffMs = retry.backoffMs ?? 1e3;
2075
+ const maxBackoffMs = retry.maxBackoffMs ?? 3e4;
2076
+ const pipelineDeps = { logger, producer, instrumentation, onMessageLost };
2077
+ for (const lt of levelTopics) {
2078
+ await ensureTopic2(lt);
2079
+ }
2080
+ const levelTxProducer = await createRetryTxProducer2(`${levelGroupId}-tx`);
2081
+ const consumer = getOrCreateConsumer2(levelGroupId, false, false);
2082
+ await consumer.connect();
2083
+ await subscribeWithRetry(consumer, levelTopics, logger);
2084
+ await consumer.run({
2085
+ eachMessage: async ({ topic: levelTopic, partition, message }) => {
2086
+ const nextOffset = {
2087
+ topic: levelTopic,
2088
+ partition,
2089
+ offset: (parseInt(message.offset, 10) + 1).toString()
2090
+ };
2091
+ if (!message.value) {
2092
+ await consumer.commitOffsets([nextOffset]);
2093
+ return;
2094
+ }
2095
+ const headers = decodeHeaders(message.headers);
2096
+ const retryAfter = parseInt(
2097
+ headers[RETRY_HEADER_AFTER] ?? "0",
2098
+ 10
2099
+ );
2100
+ const remaining = retryAfter - Date.now();
2101
+ if (remaining > 0) {
2102
+ consumer.pause([{ topic: levelTopic, partitions: [partition] }]);
2103
+ await sleep(remaining);
2104
+ consumer.resume([{ topic: levelTopic, partitions: [partition] }]);
2105
+ }
2106
+ const rawBytes = message.value;
2107
+ const currentMaxRetries = parseInt(
2108
+ headers[RETRY_HEADER_MAX_RETRIES] ?? String(retry.maxRetries),
2109
+ 10
2110
+ );
2111
+ const originalTopic = headers[RETRY_HEADER_ORIGINAL_TOPIC] ?? levelTopic.replace(/\.retry\.\d+$/, "");
2112
+ const serde = serdeMap?.get(originalTopic) ?? deps.serde;
2113
+ let parsed;
2114
+ try {
2115
+ parsed = await serde.deserialize(rawBytes, {
2116
+ topic: originalTopic,
2117
+ headers,
2118
+ isKey: false
2119
+ });
2120
+ } catch (err) {
2121
+ logger.error(
2122
+ `Failed to deserialize retry message from topic ${levelTopic}:`,
2123
+ toError(err).stack
2124
+ );
2125
+ await consumer.commitOffsets([nextOffset]);
2126
+ return;
2127
+ }
2128
+ if (parsed === null) {
2129
+ await consumer.commitOffsets([nextOffset]);
2130
+ return;
2131
+ }
2132
+ const validated = await validateWithSchema(
2133
+ parsed,
2134
+ rawBytes,
2135
+ originalTopic,
2136
+ schemaMap,
2137
+ interceptors,
2138
+ dlq,
2139
+ { ...pipelineDeps, originalHeaders: headers }
2140
+ );
2141
+ if (validated === null) {
2142
+ await consumer.commitOffsets([nextOffset]);
2143
+ return;
2144
+ }
2145
+ const envelope = extractEnvelope(
2146
+ validated,
2147
+ headers,
2148
+ originalTopic,
2149
+ partition,
2150
+ message.offset
2151
+ );
2152
+ const error = await runHandlerWithPipeline(
2153
+ () => runWithEnvelopeContext(
2154
+ {
2155
+ correlationId: envelope.correlationId,
2156
+ traceparent: envelope.traceparent
2157
+ },
2158
+ () => handleMessage(envelope)
2159
+ ),
2160
+ [envelope],
2161
+ interceptors,
2162
+ instrumentation
2163
+ );
2164
+ if (!error) {
2165
+ onMessage?.(envelope);
2166
+ await consumer.commitOffsets([nextOffset]);
2167
+ return;
2168
+ }
2169
+ deps.onFailure?.(envelope);
2170
+ const exhausted = level >= currentMaxRetries;
2171
+ const reportedError = exhausted && currentMaxRetries > 1 ? new KafkaRetryExhaustedError(
2172
+ originalTopic,
2173
+ [envelope.payload],
2174
+ currentMaxRetries,
2175
+ { cause: error }
2176
+ ) : error;
2177
+ await notifyInterceptorsOnError([envelope], interceptors, reportedError);
2178
+ logger.error(
2179
+ `Retry consumer error for ${originalTopic} (level ${level}/${currentMaxRetries}):`,
2180
+ error.stack
2181
+ );
2182
+ if (!exhausted) {
2183
+ const nextLevel = level + 1;
2184
+ const cap = Math.min(backoffMs * 2 ** level, maxBackoffMs);
2185
+ const delay = Math.floor(Math.random() * cap);
2186
+ const { topic: rtTopic, messages: rtMsgs } = buildRetryTopicPayload(
2187
+ originalTopic,
2188
+ [rawBytes],
2189
+ nextLevel,
2190
+ currentMaxRetries,
2191
+ delay,
2192
+ headers
2193
+ );
2194
+ const tx = await levelTxProducer.transaction();
2195
+ try {
2196
+ await tx.send({ topic: rtTopic, messages: rtMsgs });
2197
+ await tx.sendOffsets({
2198
+ consumer,
2199
+ topics: [
2200
+ {
2201
+ topic: nextOffset.topic,
2202
+ partitions: [
2203
+ {
2204
+ partition: nextOffset.partition,
2205
+ offset: nextOffset.offset
2206
+ }
2207
+ ]
2208
+ }
2209
+ ]
2210
+ });
2211
+ await tx.commit();
2212
+ logger.warn(
2213
+ `Message routed to ${rtTopic} (EOS, level ${nextLevel}/${currentMaxRetries})`
2214
+ );
2215
+ onRetry?.(envelope, nextLevel, currentMaxRetries);
2216
+ } catch (txErr) {
2217
+ try {
2218
+ await tx.abort();
2219
+ } catch {
2220
+ }
2221
+ logger.error(
2222
+ `EOS routing to ${rtTopic} failed \u2014 message will be redelivered:`,
2223
+ toError(txErr).stack
2224
+ );
2225
+ return;
2226
+ }
2227
+ } else if (dlq) {
2228
+ const { topic: dTopic, messages: dMsgs } = buildDlqPayload(
2229
+ originalTopic,
2230
+ rawBytes,
2231
+ {
2232
+ error,
2233
+ // +1 to account for the main consumer's initial attempt before routing.
2234
+ attempt: level + 1,
2235
+ originalHeaders: headers
2236
+ }
2237
+ );
2238
+ const tx = await levelTxProducer.transaction();
2239
+ try {
2240
+ await tx.send({ topic: dTopic, messages: dMsgs });
2241
+ await tx.sendOffsets({
2242
+ consumer,
2243
+ topics: [
2244
+ {
2245
+ topic: nextOffset.topic,
2246
+ partitions: [
2247
+ {
2248
+ partition: nextOffset.partition,
2249
+ offset: nextOffset.offset
2250
+ }
2251
+ ]
2252
+ }
2253
+ ]
2254
+ });
2255
+ await tx.commit();
2256
+ logger.warn(`Message sent to DLQ: ${dTopic} (EOS)`);
2257
+ onDlq?.(envelope, "handler-error");
2258
+ } catch (txErr) {
2259
+ try {
2260
+ await tx.abort();
2261
+ } catch {
2262
+ }
2263
+ logger.error(
2264
+ `EOS DLQ routing to ${dTopic} failed \u2014 message will be redelivered:`,
2265
+ toError(txErr).stack
2266
+ );
2267
+ return;
2268
+ }
2269
+ } else {
2270
+ await onMessageLost?.({
2271
+ topic: originalTopic,
2272
+ error,
2273
+ attempt: level,
2274
+ headers
2275
+ });
2276
+ await consumer.commitOffsets([nextOffset]);
2277
+ }
2278
+ }
2279
+ });
2280
+ runningConsumers.set(levelGroupId, "eachMessage");
2281
+ await waitForPartitionAssignment(
2282
+ consumer,
2283
+ levelTopics,
2284
+ logger,
2285
+ assignmentTimeoutMs
2286
+ );
2287
+ deps.onLevelStarted?.(levelGroupId);
2288
+ logger.log(
2289
+ `Retry level ${level}/${retry.maxRetries} consumer started for: ${originalTopics.join(", ")} (group: ${levelGroupId})`
2290
+ );
2291
+ }
2292
+ async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
2293
+ const levelGroupIds = new Array(retry.maxRetries);
2294
+ await Promise.all(
2295
+ Array.from({ length: retry.maxRetries }, async (_, i) => {
2296
+ const level = i + 1;
2297
+ const levelTopics = originalTopics.map((t) => `${t}.retry.${level}`);
2298
+ const levelGroupId = `${originalGroupId}-retry.${level}`;
2299
+ await startLevelConsumer(
2300
+ level,
2301
+ levelTopics,
2302
+ levelGroupId,
2303
+ originalTopics,
2304
+ handleMessage,
2305
+ retry,
2306
+ dlq,
2307
+ interceptors,
2308
+ schemaMap,
2309
+ deps,
2310
+ assignmentTimeoutMs,
2311
+ serdeMap
2312
+ );
2313
+ levelGroupIds[i] = levelGroupId;
2314
+ })
2315
+ );
2316
+ return levelGroupIds;
2317
+ }
2318
+
2319
+ // src/client/kafka.client/consumer/setup.ts
2320
+ function validateTopicConsumerOpts(topics, options) {
2321
+ if (options.retryTopics && !options.retry) {
2322
+ throw new Error(
2323
+ "retryTopics requires retry to be configured \u2014 set retry.maxRetries to enable the retry topic chain"
2324
+ );
2325
+ }
2326
+ if (options.retryTopics && topics.some((t) => t instanceof RegExp)) {
2327
+ throw new Error(
2328
+ "retryTopics is incompatible with regex topic patterns \u2014 retry topics require a fixed topic name to build the retry chain."
2329
+ );
2330
+ }
2331
+ }
2332
+ async function ensureConsumerTopics(ctx, topicNames, dlq, deduplication) {
2333
+ for (const t of topicNames) await ensureTopic(ctx, t);
2334
+ if (dlq) {
2335
+ for (const t of topicNames) await ensureTopic(ctx, `${t}.dlq`);
2336
+ if (!ctx.autoCreateTopicsEnabled && topicNames.length > 0) {
2337
+ await ctx.adminOps.validateDlqTopicsExist(topicNames);
2338
+ }
2339
+ }
2340
+ if (deduplication?.strategy === "topic") {
2341
+ const dest = deduplication.duplicatesTopic;
2342
+ if (ctx.autoCreateTopicsEnabled) {
2343
+ for (const t of topicNames)
2344
+ await ensureTopic(ctx, dest ?? `${t}.duplicates`);
2345
+ } else if (topicNames.length > 0) {
2346
+ await ctx.adminOps.validateDuplicatesTopicsExist(topicNames, dest);
2347
+ }
2348
+ }
2349
+ }
2350
+ async function setupConsumer(ctx, topics, mode, options) {
2351
+ const {
2352
+ groupId: optGroupId,
2353
+ fromBeginning = false,
2354
+ retry,
2355
+ dlq = false,
2356
+ interceptors = [],
2357
+ schemas: optionSchemas
2358
+ } = options;
2359
+ const stringTopics = topics.filter((t) => !(t instanceof RegExp));
2360
+ const regexTopics = topics.filter((t) => t instanceof RegExp);
2361
+ const hasRegex = regexTopics.length > 0;
2362
+ const gid = optGroupId || ctx.defaultGroupId;
2363
+ const existingMode = ctx.runningConsumers.get(gid);
2364
+ const oppositeMode = mode === "eachMessage" ? "eachBatch" : "eachMessage";
2365
+ if (existingMode === oppositeMode) {
2366
+ throw new Error(
2367
+ `Cannot use ${mode} on consumer group "${gid}" \u2014 it is already running with ${oppositeMode}. Use a different groupId for this consumer.`
2368
+ );
2369
+ }
2370
+ if (existingMode === mode) {
2371
+ const callerName = mode === "eachMessage" ? "startConsumer" : "startBatchConsumer";
2372
+ throw new Error(
2373
+ `${callerName}("${gid}") called twice \u2014 this group is already consuming. Call stopConsumer("${gid}") first or pass a different groupId.`
2374
+ );
2375
+ }
2376
+ let resolveReady;
2377
+ const readyPromise = new Promise((resolve) => {
2378
+ resolveReady = resolve;
2379
+ });
2380
+ const consumer = getOrCreateConsumer(
2381
+ gid,
2382
+ fromBeginning,
2383
+ options.autoCommit ?? true,
2384
+ ctx.consumerOpsDeps,
2385
+ options.partitionAssigner,
2386
+ resolveReady,
2387
+ options.groupInstanceId
2388
+ );
2389
+ const schemaMap = buildSchemaMap(
2390
+ stringTopics,
2391
+ ctx.schemaRegistry,
2392
+ optionSchemas,
2393
+ ctx.logger
2394
+ );
2395
+ const serdeMap = buildSerdeMap(stringTopics);
2396
+ const topicNames = stringTopics.map((t) => resolveTopicName(t));
2397
+ const subscribeTopics = [...topicNames, ...regexTopics];
2398
+ await ensureConsumerTopics(ctx, topicNames, dlq, options.deduplication);
2399
+ await consumer.connect();
2400
+ if (dlq || options.retryTopics || options.deduplication) {
2401
+ await ctx.producer.connect();
2402
+ }
2403
+ await subscribeWithRetry(
2404
+ consumer,
2405
+ subscribeTopics,
2406
+ ctx.logger,
2407
+ options.subscribeRetry
2408
+ );
2409
+ const displayTopics = subscribeTopics.map((t) => t instanceof RegExp ? t.toString() : t).join(", ");
2410
+ ctx.logger.log(
2411
+ `${mode === "eachBatch" ? "Batch consumer" : "Consumer"} subscribed to topics: ${displayTopics}`
2412
+ );
2413
+ return { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, hasRegex, readyPromise };
2414
+ }
2415
+ function resolveDeduplicationContext(ctx, groupId, options) {
2416
+ if (!options) return void 0;
2417
+ const store = options.store ?? new InMemoryDedupStore(ctx.dedupStates);
2418
+ return { options, store, groupId };
2419
+ }
2420
+ function messageDepsFor(ctx, gid, options) {
2421
+ const notifyRetry = ctx.metrics.notifyRetry.bind(ctx.metrics);
2422
+ return {
2423
+ logger: ctx.logger,
2424
+ producer: ctx.producer,
2425
+ serde: ctx.serde,
2426
+ instrumentation: ctx.instrumentation,
2427
+ onMessageLost: options?.onMessageLost ?? ctx.onMessageLost,
2428
+ onTtlExpired: ctx.onTtlExpired,
2429
+ onRetry: options?.onRetry ? (envelope, attempt, max) => {
2430
+ notifyRetry(envelope, attempt, max);
2431
+ return options.onRetry(envelope, attempt, max);
2432
+ } : notifyRetry,
2433
+ onDlq: (envelope, reason) => ctx.metrics.notifyDlq(envelope, reason),
2434
+ onDuplicate: ctx.metrics.notifyDuplicate.bind(ctx.metrics),
2435
+ onMessage: (envelope) => ctx.metrics.notifyMessage(envelope, gid),
2436
+ onFailure: (envelope) => ctx.metrics.notifyFailure(envelope, gid)
2437
+ };
2438
+ }
2439
+ function buildRetryTopicDeps(ctx) {
2440
+ return {
2441
+ logger: ctx.logger,
2442
+ producer: ctx.producer,
2443
+ serde: ctx.serde,
2444
+ instrumentation: ctx.instrumentation,
2445
+ onMessageLost: ctx.onMessageLost,
2446
+ onRetry: ctx.metrics.notifyRetry.bind(ctx.metrics),
2447
+ onDlq: ctx.metrics.notifyDlq.bind(ctx.metrics),
2448
+ onMessage: ctx.metrics.notifyMessage.bind(ctx.metrics),
2449
+ ensureTopic: (t) => ensureTopic(ctx, t),
2450
+ getOrCreateConsumer: (gid, fb, ac) => getOrCreateConsumer(gid, fb, ac, ctx.consumerOpsDeps),
2451
+ runningConsumers: ctx.runningConsumers,
2452
+ createRetryTxProducer: (txId) => createRetryTxProducer(ctx, txId)
2453
+ };
2454
+ }
2455
+ async function makeEosMainContext(ctx, gid, consumer, options) {
2456
+ if (!options.retryTopics || !options.retry) return void 0;
2457
+ const txProducer = await createRetryTxProducer(ctx, `${gid}-main-tx`);
2458
+ return { txProducer, consumer };
2459
+ }
2460
+ async function launchRetryChain(ctx, gid, topicNames, handleMessage, opts) {
2461
+ const { retry, dlq, interceptors, schemaMap, serdeMap, assignmentTimeoutMs } = opts;
2462
+ if (!ctx.autoCreateTopicsEnabled) {
2463
+ await ctx.adminOps.validateRetryTopicsExist(topicNames, retry.maxRetries);
2464
+ }
2465
+ ctx.companionGroupIds.set(gid, []);
2466
+ await startRetryTopicConsumers(
2467
+ topicNames,
2468
+ gid,
2469
+ handleMessage,
2470
+ retry,
2471
+ dlq,
2472
+ interceptors,
2473
+ schemaMap,
2474
+ {
2475
+ ...ctx.retryTopicDeps,
2476
+ // Bind circuit breaker events to the MAIN consumer group so failures and
2477
+ // successes inside the retry chain drive the same breaker as the main
2478
+ // consumer (the retry chain has no breaker config of its own).
2479
+ onFailure: (envelope) => ctx.metrics.notifyFailure(envelope, gid),
2480
+ onMessage: (envelope) => ctx.metrics.notifyMessage(envelope, gid),
2481
+ onLevelStarted: (levelGroupId) => {
2482
+ ctx.companionGroupIds.get(gid).push(levelGroupId);
2483
+ }
2484
+ },
2485
+ assignmentTimeoutMs,
2486
+ serdeMap
2487
+ );
2488
+ }
2489
+
2490
+ // src/client/kafka.client/consumer/handler.ts
2491
+ async function applyDeduplication(envelope, raw, dedup, dlq, deps) {
2492
+ const clockRaw = envelope.headers[HEADER_LAMPORT_CLOCK];
2493
+ if (clockRaw === void 0) return false;
2494
+ const incomingClock = Number(clockRaw);
2495
+ if (Number.isNaN(incomingClock)) return false;
2496
+ const stateKey = `${envelope.topic}:${envelope.partition}`;
2497
+ let lastProcessedClock;
2498
+ try {
2499
+ lastProcessedClock = await dedup.store.getLastClock(dedup.groupId, stateKey) ?? -1;
2500
+ } catch (err) {
2501
+ deps.logger.error(
2502
+ `Dedup store getLastClock failed on ${envelope.topic}[${envelope.partition}] \u2014 treating message as not a duplicate (fail-open): ${err.message}`
2503
+ );
2504
+ return false;
2505
+ }
2506
+ if (incomingClock <= lastProcessedClock) {
2507
+ const meta = {
2508
+ incomingClock,
2509
+ lastProcessedClock,
2510
+ originalHeaders: envelope.headers
2511
+ };
2512
+ const strategy = dedup.options.strategy ?? "drop";
2513
+ deps.logger.warn(
2514
+ `Duplicate message on ${envelope.topic}[${envelope.partition}]: clock=${incomingClock} <= last=${lastProcessedClock} \u2014 strategy=${strategy}`
2515
+ );
2516
+ deps.onDuplicate?.(envelope, strategy);
2517
+ if (strategy === "dlq" && dlq) {
2518
+ const augmentedHeaders = {
2519
+ ...envelope.headers,
2520
+ "x-dlq-reason": "lamport-clock-duplicate",
2521
+ "x-dlq-duplicate-incoming-clock": String(incomingClock),
2522
+ "x-dlq-duplicate-last-processed-clock": String(lastProcessedClock)
2523
+ };
2524
+ await sendToDlq(envelope.topic, raw, deps, {
2525
+ error: new Error("Lamport Clock duplicate detected"),
2526
+ attempt: 0,
2527
+ originalHeaders: augmentedHeaders
2528
+ });
2529
+ } else if (strategy === "topic") {
2530
+ const destination = dedup.options.duplicatesTopic ?? `${envelope.topic}.duplicates`;
2531
+ await sendToDuplicatesTopic(envelope.topic, raw, destination, deps, meta);
2532
+ }
2533
+ return true;
2534
+ }
2535
+ try {
2536
+ await dedup.store.setLastClock(dedup.groupId, stateKey, incomingClock);
2537
+ } catch (err) {
2538
+ deps.logger.error(
2539
+ `Dedup store setLastClock failed on ${envelope.topic}[${envelope.partition}] \u2014 processing message anyway (fail-open): ${err.message}`
2540
+ );
2541
+ }
2542
+ return false;
2543
+ }
2544
+ async function parseSingleMessage(message, topic2, partition, schemaMap, interceptors, dlq, deps, serdeMap) {
2545
+ if (!message.value) {
2546
+ deps.logger.warn(`Received empty message from topic ${topic2}`);
2547
+ return null;
2548
+ }
2549
+ const bytes = message.value;
2550
+ const headers = decodeHeaders(message.headers);
2551
+ const serde = serdeMap?.get(topic2) ?? deps.serde;
2552
+ let parsed;
2553
+ try {
2554
+ parsed = await serde.deserialize(bytes, { topic: topic2, headers, isKey: false });
2555
+ } catch (error) {
2556
+ deps.logger.error(
2557
+ `Failed to deserialize message from topic ${topic2}:`,
2558
+ toError(error).stack
2559
+ );
2560
+ return null;
2561
+ }
2562
+ if (parsed === null) return null;
2563
+ const validated = await validateWithSchema(
2564
+ parsed,
2565
+ // Forward the ORIGINAL bytes to DLQ on validation failure (binary-safe).
2566
+ bytes,
2567
+ topic2,
2568
+ schemaMap,
2569
+ interceptors,
2570
+ dlq,
2571
+ { ...deps, originalHeaders: headers }
2572
+ );
2573
+ if (validated === null) return null;
2574
+ return extractEnvelope(validated, headers, topic2, partition, message.offset);
2575
+ }
2576
+ async function handleEachMessage(payload, opts, deps) {
2577
+ const { topic: topic2, partition, message } = payload;
2578
+ const {
2579
+ schemaMap,
2580
+ serdeMap,
2581
+ handleMessage,
2582
+ interceptors,
2583
+ dlq,
2584
+ retry,
2585
+ retryTopics,
2586
+ timeoutMs,
2587
+ wrapWithTimeout
2588
+ } = opts;
2589
+ const rawBytes = message.value;
2590
+ const eos = opts.eosMainContext;
2591
+ const nextOffsetStr = (parseInt(message.offset, 10) + 1).toString();
2592
+ const commitOffset = eos ? async () => {
2593
+ await eos.consumer.commitOffsets([
2594
+ { topic: topic2, partition, offset: nextOffsetStr }
2595
+ ]);
2596
+ } : void 0;
2597
+ const eosRouteToRetry = eos && retry ? async (rawMsgs, envelopes, delay) => {
2598
+ const { topic: rtTopic, messages: rtMsgs } = buildRetryTopicPayload(
2599
+ topic2,
2600
+ rawMsgs,
2601
+ 1,
2602
+ retry.maxRetries,
2603
+ delay,
2604
+ envelopes[0]?.headers ?? {}
2605
+ );
2606
+ const tx = await eos.txProducer.transaction();
2607
+ try {
2608
+ await tx.send({ topic: rtTopic, messages: rtMsgs });
2609
+ await tx.sendOffsets({
2610
+ consumer: eos.consumer,
2611
+ topics: [
2612
+ {
2613
+ topic: topic2,
2614
+ partitions: [{ partition, offset: nextOffsetStr }]
2615
+ }
2616
+ ]
2617
+ });
2618
+ await tx.commit();
2619
+ } catch (txErr) {
2620
+ try {
2621
+ await tx.abort();
2622
+ } catch {
2623
+ }
2624
+ throw txErr;
2625
+ }
2626
+ } : void 0;
2627
+ const envelope = await parseSingleMessage(
2628
+ message,
2629
+ topic2,
2630
+ partition,
2631
+ schemaMap,
2632
+ interceptors,
2633
+ dlq,
2634
+ deps,
2635
+ serdeMap
2636
+ );
2637
+ if (envelope === null) {
2638
+ await commitOffset?.();
2639
+ return;
2640
+ }
2641
+ if (opts.deduplication) {
2642
+ const isDuplicate = await applyDeduplication(
2643
+ envelope,
2644
+ rawBytes,
2645
+ opts.deduplication,
2646
+ dlq,
2647
+ deps
2648
+ );
2649
+ if (isDuplicate) {
2650
+ await commitOffset?.();
2651
+ return;
2652
+ }
2653
+ }
2654
+ if (opts.messageTtlMs !== void 0) {
2655
+ const ageMs = Date.now() - new Date(envelope.timestamp).getTime();
2656
+ if (ageMs > opts.messageTtlMs) {
2657
+ deps.logger.warn(
2658
+ `[KafkaClient] TTL expired on ${topic2}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
2659
+ );
2660
+ if (dlq) {
2661
+ await sendToDlq(topic2, rawBytes, deps, {
2662
+ error: new Error(`Message TTL expired: age ${ageMs}ms`),
2663
+ attempt: 0,
2664
+ originalHeaders: envelope.headers
2665
+ });
2666
+ deps.onDlq?.(envelope, "ttl-expired");
2667
+ } else {
2668
+ const ttlHandler = opts.onTtlExpired ?? deps.onTtlExpired;
2669
+ await ttlHandler?.({
2670
+ topic: topic2,
2671
+ ageMs,
2672
+ messageTtlMs: opts.messageTtlMs,
2673
+ headers: envelope.headers
2674
+ });
2675
+ }
2676
+ await commitOffset?.();
2677
+ return;
2678
+ }
2679
+ }
2680
+ await executeWithRetry(
2681
+ () => {
2682
+ const fn = () => runWithEnvelopeContext(
2683
+ {
2684
+ correlationId: envelope.correlationId,
2685
+ traceparent: envelope.traceparent
2686
+ },
2687
+ () => handleMessage(envelope)
2688
+ );
2689
+ return timeoutMs ? wrapWithTimeout(fn, timeoutMs, topic2) : fn();
2690
+ },
2691
+ {
2692
+ envelope,
2693
+ rawMessages: [rawBytes],
2694
+ interceptors,
2695
+ dlq,
2696
+ retry,
2697
+ retryTopics
2698
+ },
2699
+ { ...deps, eosRouteToRetry, eosCommitOnSuccess: commitOffset }
2700
+ );
2701
+ }
2702
+ async function handleEachBatch(payload, opts, deps) {
2703
+ const { batch, heartbeat, resolveOffset, commitOffsetsIfNecessary } = payload;
2704
+ const {
2705
+ schemaMap,
2706
+ serdeMap,
2707
+ handleBatch,
2708
+ interceptors,
2709
+ dlq,
2710
+ retry,
2711
+ retryTopics,
2712
+ timeoutMs,
2713
+ wrapWithTimeout
2714
+ } = opts;
2715
+ const eos = opts.eosMainContext;
2716
+ const lastRawOffset = batch.messages.length > 0 ? batch.messages[batch.messages.length - 1].offset : void 0;
2717
+ const batchNextOffsetStr = lastRawOffset ? (parseInt(lastRawOffset, 10) + 1).toString() : void 0;
2718
+ const commitBatchOffset = eos && batchNextOffsetStr ? async () => {
2719
+ await eos.consumer.commitOffsets([
2720
+ {
2721
+ topic: batch.topic,
2722
+ partition: batch.partition,
2723
+ offset: batchNextOffsetStr
2724
+ }
2725
+ ]);
2726
+ } : void 0;
2727
+ const eosRouteToRetry = eos && retry && batchNextOffsetStr ? async (rawMsgs, envelopes2, delay) => {
2728
+ const { topic: rtTopic, messages: rtMsgs } = buildRetryTopicPayload(
2729
+ batch.topic,
2730
+ rawMsgs,
2731
+ 1,
2732
+ retry.maxRetries,
2733
+ delay,
2734
+ envelopes2.map((e) => e.headers)
2735
+ );
2736
+ const tx = await eos.txProducer.transaction();
2737
+ try {
2738
+ await tx.send({ topic: rtTopic, messages: rtMsgs });
2739
+ await tx.sendOffsets({
2740
+ consumer: eos.consumer,
2741
+ topics: [
2742
+ {
2743
+ topic: batch.topic,
2744
+ partitions: [
2745
+ { partition: batch.partition, offset: batchNextOffsetStr }
2746
+ ]
2747
+ }
2748
+ ]
2749
+ });
2750
+ await tx.commit();
2751
+ } catch (txErr) {
2752
+ try {
2753
+ await tx.abort();
2754
+ } catch {
2755
+ }
2756
+ throw txErr;
2757
+ }
2758
+ } : void 0;
2759
+ const envelopes = [];
2760
+ const rawMessages = [];
2761
+ for (const message of batch.messages) {
2762
+ const rawBytes = message.value;
2763
+ const envelope = await parseSingleMessage(
2764
+ message,
2765
+ batch.topic,
2766
+ batch.partition,
2767
+ schemaMap,
2768
+ interceptors,
2769
+ dlq,
2770
+ deps,
2771
+ serdeMap
2772
+ );
2773
+ if (envelope === null) continue;
2774
+ if (opts.deduplication) {
2775
+ const isDuplicate = await applyDeduplication(
2776
+ envelope,
2777
+ rawBytes,
2778
+ opts.deduplication,
2779
+ dlq,
2780
+ deps
2781
+ );
2782
+ if (isDuplicate) continue;
2783
+ }
2784
+ if (opts.messageTtlMs !== void 0) {
2785
+ const ageMs = Date.now() - new Date(envelope.timestamp).getTime();
2786
+ if (ageMs > opts.messageTtlMs) {
2787
+ deps.logger.warn(
2788
+ `[KafkaClient] TTL expired on ${batch.topic}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
2789
+ );
2790
+ if (dlq) {
2791
+ await sendToDlq(batch.topic, rawBytes, deps, {
2792
+ error: new Error(`Message TTL expired: age ${ageMs}ms`),
2793
+ attempt: 0,
2794
+ originalHeaders: envelope.headers
2795
+ });
2796
+ deps.onDlq?.(envelope, "ttl-expired");
2797
+ } else {
2798
+ const ttlHandler = opts.onTtlExpired ?? deps.onTtlExpired;
2799
+ await ttlHandler?.({
2800
+ topic: batch.topic,
2801
+ ageMs,
2802
+ messageTtlMs: opts.messageTtlMs,
2803
+ headers: envelope.headers
2804
+ });
2805
+ }
2806
+ continue;
2807
+ }
2808
+ }
2809
+ envelopes.push(envelope);
2810
+ rawMessages.push(rawBytes);
2811
+ }
2812
+ if (envelopes.length === 0) {
2813
+ await commitBatchOffset?.();
2814
+ return;
2815
+ }
2816
+ const meta = {
2817
+ partition: batch.partition,
2818
+ highWatermark: batch.highWatermark,
2819
+ heartbeat,
2820
+ resolveOffset,
2821
+ commitOffsetsIfNecessary
2822
+ };
2823
+ await executeWithRetry(
2824
+ () => {
2825
+ const fn = () => handleBatch(envelopes, meta);
2826
+ return timeoutMs ? wrapWithTimeout(fn, timeoutMs, batch.topic) : fn();
2827
+ },
2828
+ {
2829
+ envelope: envelopes,
2830
+ rawMessages,
2831
+ interceptors,
2832
+ dlq,
2833
+ retry,
2834
+ isBatch: true,
2835
+ retryTopics
2836
+ },
2837
+ { ...deps, eosRouteToRetry, eosCommitOnSuccess: commitBatchOffset }
2838
+ );
2839
+ }
2840
+
2841
+ // src/client/kafka.client/consumer/stop.ts
2842
+ async function stopConsumerImpl(ctx, groupId) {
2843
+ if (groupId === void 0) {
2844
+ const tasks = [
2845
+ ...Array.from(ctx.consumers.values()).map(
2846
+ (c) => c.disconnect().catch(() => {
2847
+ })
2848
+ ),
2849
+ ...Array.from(ctx.retryTxProducers.values()).map(
2850
+ (p) => p.disconnect().catch(() => {
2851
+ })
2852
+ )
2853
+ ];
2854
+ await Promise.allSettled(tasks);
2855
+ ctx.consumers.clear();
2856
+ ctx.runningConsumers.clear();
2857
+ ctx.consumerCreationOptions.clear();
2858
+ ctx.companionGroupIds.clear();
2859
+ ctx.retryTxProducers.clear();
2860
+ ctx.dedupStates.clear();
2861
+ ctx.circuitBreaker.clear();
2862
+ ctx.logger.log("All consumers disconnected");
2863
+ return;
2864
+ }
2865
+ const consumer = ctx.consumers.get(groupId);
2866
+ if (!consumer) {
2867
+ ctx.logger.warn(
2868
+ `stopConsumer: no active consumer for group "${groupId}"`
2869
+ );
2870
+ return;
2871
+ }
2872
+ await consumer.disconnect().catch(
2873
+ (e) => ctx.logger.warn(
2874
+ `Error disconnecting consumer "${groupId}":`,
2875
+ toError(e).message
2876
+ )
2877
+ );
2878
+ ctx.consumers.delete(groupId);
2879
+ ctx.runningConsumers.delete(groupId);
2880
+ ctx.consumerCreationOptions.delete(groupId);
2881
+ ctx.dedupStates.delete(groupId);
2882
+ ctx.circuitBreaker.removeGroup(groupId);
2883
+ ctx.logger.log(`Consumer disconnected: group "${groupId}"`);
2884
+ const mainTxId = `${groupId}-main-tx`;
2885
+ const mainTxProducer = ctx.retryTxProducers.get(mainTxId);
2886
+ if (mainTxProducer) {
2887
+ await mainTxProducer.disconnect().catch(
2888
+ (e) => ctx.logger.warn(
2889
+ `Error disconnecting main tx producer "${mainTxId}":`,
2890
+ toError(e).message
2891
+ )
2892
+ );
2893
+ _activeTransactionalIds.delete(mainTxId);
2894
+ ctx.retryTxProducers.delete(mainTxId);
2895
+ }
2896
+ const companions = ctx.companionGroupIds.get(groupId) ?? [];
2897
+ for (const cGroupId of companions) {
2898
+ const cConsumer = ctx.consumers.get(cGroupId);
2899
+ if (cConsumer) {
2900
+ await cConsumer.disconnect().catch(
2901
+ (e) => ctx.logger.warn(
2902
+ `Error disconnecting retry consumer "${cGroupId}":`,
2903
+ toError(e).message
2904
+ )
2905
+ );
2906
+ ctx.consumers.delete(cGroupId);
2907
+ ctx.runningConsumers.delete(cGroupId);
2908
+ ctx.consumerCreationOptions.delete(cGroupId);
2909
+ ctx.logger.log(`Retry consumer disconnected: group "${cGroupId}"`);
2910
+ }
2911
+ const txId = `${cGroupId}-tx`;
2912
+ const txProducer = ctx.retryTxProducers.get(txId);
2913
+ if (txProducer) {
2914
+ await txProducer.disconnect().catch(
2915
+ (e) => ctx.logger.warn(
2916
+ `Error disconnecting retry tx producer "${txId}":`,
2917
+ toError(e).message
2918
+ )
2919
+ );
2920
+ _activeTransactionalIds.delete(txId);
2921
+ ctx.retryTxProducers.delete(txId);
2922
+ }
2923
+ }
2924
+ ctx.companionGroupIds.delete(groupId);
2925
+ }
2926
+ function pauseConsumerImpl(ctx, groupId, assignments) {
2927
+ const gid = groupId ?? ctx.defaultGroupId;
2928
+ const consumer = ctx.consumers.get(gid);
2929
+ if (!consumer) {
2930
+ ctx.logger.warn(`pauseConsumer: no active consumer for group "${gid}"`);
2931
+ return;
2932
+ }
2933
+ consumer.pause(
2934
+ assignments.flatMap(
2935
+ ({ topic: topic2, partitions }) => partitions.map((p) => ({ topic: topic2, partitions: [p] }))
2936
+ )
2937
+ );
2938
+ }
2939
+ function resumeConsumerImpl(ctx, groupId, assignments) {
2940
+ const gid = groupId ?? ctx.defaultGroupId;
2941
+ const consumer = ctx.consumers.get(gid);
2942
+ if (!consumer) {
2943
+ ctx.logger.warn(`resumeConsumer: no active consumer for group "${gid}"`);
2944
+ return;
2945
+ }
2946
+ consumer.resume(
2947
+ assignments.flatMap(
2948
+ ({ topic: topic2, partitions }) => partitions.map((p) => ({ topic: topic2, partitions: [p] }))
2949
+ )
2950
+ );
2951
+ }
2952
+ function pauseTopicAllPartitions(ctx, gid, topic2) {
2953
+ const consumer = ctx.consumers.get(gid);
2954
+ if (!consumer) return;
2955
+ const assignment = consumer.assignment();
2956
+ const partitions = assignment.filter((a) => a.topic === topic2).map((a) => a.partition);
2957
+ if (partitions.length > 0)
2958
+ consumer.pause(partitions.map((p) => ({ topic: topic2, partitions: [p] })));
2959
+ }
2960
+ function resumeTopicAllPartitions(ctx, gid, topic2) {
2961
+ const consumer = ctx.consumers.get(gid);
2962
+ if (!consumer) return;
2963
+ const assignment = consumer.assignment();
2964
+ const partitions = assignment.filter((a) => a.topic === topic2).map((a) => a.partition);
2965
+ if (partitions.length > 0)
2966
+ consumer.resume(partitions.map((p) => ({ topic: topic2, partitions: [p] })));
2967
+ }
2968
+
2969
+ // src/client/kafka.client/consumer/start.ts
2970
+ async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
2971
+ validateTopicConsumerOpts(topics, options);
2972
+ const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
2973
+ const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachMessage", setupOptions);
2974
+ if (options.circuitBreaker)
2975
+ ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
2976
+ const deps = messageDepsFor(ctx, gid, options);
2977
+ const eosMainContext = await makeEosMainContext(ctx, gid, consumer, options);
2978
+ await consumer.run({
2979
+ eachMessage: (payload) => ctx.inFlight.track(
2980
+ () => handleEachMessage(
2981
+ payload,
2982
+ {
2983
+ schemaMap,
2984
+ serdeMap,
2985
+ handleMessage,
2986
+ interceptors,
2987
+ dlq,
2988
+ retry,
2989
+ retryTopics: options.retryTopics,
2990
+ timeoutMs: options.handlerTimeoutMs,
2991
+ wrapWithTimeout: (fn, ms, topic2) => wrapWithTimeoutWarning(ctx.logger, fn, ms, topic2),
2992
+ deduplication: resolveDeduplicationContext(
2993
+ ctx,
2994
+ gid,
2995
+ options.deduplication
2996
+ ),
2997
+ messageTtlMs: options.messageTtlMs,
2998
+ onTtlExpired: options.onTtlExpired,
2999
+ eosMainContext
3000
+ },
3001
+ deps
3002
+ )
3003
+ )
3004
+ });
3005
+ ctx.runningConsumers.set(gid, "eachMessage");
3006
+ if (options.retryTopics && retry) {
3007
+ await launchRetryChain(ctx, gid, topicNames, handleMessage, {
3008
+ retry,
3009
+ dlq,
3010
+ interceptors,
3011
+ schemaMap,
3012
+ serdeMap,
3013
+ assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
3014
+ });
3015
+ }
3016
+ return { groupId: gid, stop: () => stopConsumerByGid(ctx, gid), ready: () => readyPromise };
3017
+ }
3018
+ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
3019
+ validateTopicConsumerOpts(topics, options);
3020
+ if (!options.retryTopics && options.autoCommit !== false) {
3021
+ ctx.logger.debug?.(
3022
+ `startBatchConsumer: autoCommit is enabled (default true). If your handler calls resolveOffset() or commitOffsetsIfNecessary(), set autoCommit: false to avoid offset conflicts.`
3023
+ );
3024
+ }
3025
+ const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
3026
+ const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachBatch", setupOptions);
3027
+ if (options.circuitBreaker)
3028
+ ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
3029
+ const deps = messageDepsFor(ctx, gid, options);
3030
+ const eosMainContext = await makeEosMainContext(ctx, gid, consumer, options);
3031
+ await consumer.run({
3032
+ eachBatch: (payload) => ctx.inFlight.track(
3033
+ () => handleEachBatch(
3034
+ payload,
3035
+ {
3036
+ schemaMap,
3037
+ serdeMap,
3038
+ handleBatch,
3039
+ interceptors,
3040
+ dlq,
3041
+ retry,
3042
+ retryTopics: options.retryTopics,
3043
+ timeoutMs: options.handlerTimeoutMs,
3044
+ wrapWithTimeout: (fn, ms, topic2) => wrapWithTimeoutWarning(ctx.logger, fn, ms, topic2),
3045
+ deduplication: resolveDeduplicationContext(
3046
+ ctx,
3047
+ gid,
3048
+ options.deduplication
3049
+ ),
3050
+ messageTtlMs: options.messageTtlMs,
3051
+ onTtlExpired: options.onTtlExpired,
3052
+ eosMainContext
3053
+ },
3054
+ deps
3055
+ )
3056
+ )
3057
+ });
3058
+ ctx.runningConsumers.set(gid, "eachBatch");
3059
+ if (options.retryTopics && retry) {
3060
+ const handleMessageForRetry = (env) => handleBatch([env], {
3061
+ partition: env.partition,
3062
+ highWatermark: null,
3063
+ heartbeat: async () => {
3064
+ },
3065
+ resolveOffset: () => {
3066
+ },
3067
+ commitOffsetsIfNecessary: async () => {
3068
+ }
3069
+ });
3070
+ await launchRetryChain(ctx, gid, topicNames, handleMessageForRetry, {
3071
+ retry,
3072
+ dlq,
3073
+ interceptors,
3074
+ schemaMap,
3075
+ serdeMap,
3076
+ assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
3077
+ });
3078
+ }
3079
+ return { groupId: gid, stop: () => stopConsumerByGid(ctx, gid), ready: () => readyPromise };
3080
+ }
3081
+ async function startTransactionalConsumerImpl(ctx, topics, handler, options = {}) {
3082
+ if (options.retryTopics) {
3083
+ throw new Error(
3084
+ "startTransactionalConsumer: retryTopics is not supported. EOS is already guaranteed by the transaction \u2014 redelivery on failure is handled automatically."
3085
+ );
3086
+ }
3087
+ const setupOptions = { ...options, autoCommit: false };
3088
+ const { consumer, schemaMap, serdeMap, gid, readyPromise } = await setupConsumer(
3089
+ ctx,
3090
+ topics,
3091
+ "eachMessage",
3092
+ setupOptions
3093
+ );
3094
+ const txProducer = await createRetryTxProducer(ctx, `${gid}-txc`);
3095
+ const deps = messageDepsFor(ctx, gid);
3096
+ await consumer.run({
3097
+ eachMessage: ({ topic: topic2, partition, message }) => ctx.inFlight.track(async () => {
3098
+ const envelope = await parseSingleMessage(
3099
+ message,
3100
+ topic2,
3101
+ partition,
3102
+ schemaMap,
3103
+ options.interceptors ?? [],
3104
+ false,
3105
+ deps,
3106
+ serdeMap
3107
+ );
3108
+ const nextOffset = String(Number.parseInt(message.offset, 10) + 1);
3109
+ if (envelope === null) {
3110
+ await consumer.commitOffsets([
3111
+ { topic: topic2, partition, offset: nextOffset }
3112
+ ]);
3113
+ return;
3114
+ }
3115
+ const tx = await txProducer.transaction();
3116
+ const txCtx = {
3117
+ send: async (t, msg, sendOpts) => {
3118
+ const payload = await preparePayload(ctx, t, [
3119
+ {
3120
+ value: msg,
3121
+ key: sendOpts?.key,
3122
+ headers: sendOpts?.headers,
3123
+ correlationId: sendOpts?.correlationId,
3124
+ schemaVersion: sendOpts?.schemaVersion,
3125
+ eventId: sendOpts?.eventId
3126
+ }
3127
+ ]);
3128
+ await tx.send(payload);
3129
+ },
3130
+ sendBatch: async (t, msgs, batchOpts) => {
3131
+ const payload = await preparePayload(
3132
+ ctx,
3133
+ t,
3134
+ msgs,
3135
+ batchOpts?.compression
3136
+ );
3137
+ await tx.send(payload);
3138
+ }
3139
+ };
3140
+ try {
3141
+ await runWithEnvelopeContext(
3142
+ {
3143
+ correlationId: envelope.correlationId,
3144
+ traceparent: envelope.traceparent
3145
+ },
3146
+ () => handler(envelope, txCtx)
3147
+ );
3148
+ await tx.sendOffsets({
3149
+ consumer,
3150
+ topics: [
3151
+ { topic: topic2, partitions: [{ partition, offset: nextOffset }] }
3152
+ ]
3153
+ });
3154
+ await tx.commit();
3155
+ deps.onMessage?.(envelope);
3156
+ } catch (err) {
3157
+ try {
3158
+ await tx.abort();
3159
+ } catch {
3160
+ }
3161
+ ctx.logger.warn(
3162
+ `startTransactionalConsumer: handler failed on ${topic2}[${partition}]@${message.offset} \u2014 tx aborted, message will be redelivered (${toError(err).message})`
3163
+ );
3164
+ throw err;
3165
+ }
3166
+ })
3167
+ });
3168
+ ctx.runningConsumers.set(gid, "eachMessage");
3169
+ return { groupId: gid, stop: () => stopConsumerByGid(ctx, gid), ready: () => readyPromise };
3170
+ }
3171
+ function stopConsumerByGid(ctx, gid) {
3172
+ return stopConsumerImpl(ctx, gid);
3173
+ }
3174
+
3175
+ // src/client/kafka.client/consumer/features/window.ts
3176
+ async function startWindowConsumerImpl(ctx, topic2, handler, options) {
3177
+ const { maxMessages, maxMs, ...consumerOptions } = options;
3178
+ if (maxMessages <= 0)
3179
+ throw new Error("startWindowConsumer: maxMessages must be > 0");
3180
+ if (maxMs <= 0) throw new Error("startWindowConsumer: maxMs must be > 0");
3181
+ if (consumerOptions.retryTopics) {
3182
+ throw new Error(
3183
+ "startWindowConsumer() does not support retryTopics. Use startConsumer() with retryTopics: true for guaranteed retry delivery."
3184
+ );
3185
+ }
3186
+ const buffer = [];
3187
+ let flushTimer = null;
3188
+ let windowStart = 0;
3189
+ const onLost = consumerOptions.onMessageLost ?? ctx.onMessageLost;
3190
+ const flush = async (trigger) => {
3191
+ if (flushTimer !== null) {
3192
+ clearTimeout(flushTimer);
3193
+ flushTimer = null;
3194
+ }
3195
+ if (buffer.length === 0) return;
3196
+ const envelopes = buffer.splice(0);
3197
+ try {
3198
+ await handler(envelopes, { trigger, windowStart, windowEnd: Date.now() });
3199
+ } catch (err) {
3200
+ const error = toError(err);
3201
+ ctx.logger.error(
3202
+ `startWindowConsumer: ${trigger}-triggered flush failed \u2014 window of ${envelopes.length} message(s) lost:`,
3203
+ error.stack
3204
+ );
3205
+ for (const envelope of envelopes) {
3206
+ await Promise.resolve(
3207
+ onLost?.({
3208
+ topic: envelope.topic,
3209
+ error,
3210
+ attempt: 0,
3211
+ headers: envelope.headers
3212
+ })
3213
+ ).catch(() => {
3214
+ });
3215
+ }
3216
+ }
3217
+ };
3218
+ const scheduleFlush = () => {
3219
+ if (flushTimer !== null) return;
3220
+ flushTimer = setTimeout(() => {
3221
+ flushTimer = null;
3222
+ void flush("time");
3223
+ }, maxMs);
3224
+ };
3225
+ const handle = await startConsumerImpl(
3226
+ ctx,
3227
+ [topic2],
3228
+ async (envelope) => {
3229
+ if (buffer.length === 0) windowStart = Date.now();
3230
+ buffer.push(envelope);
3231
+ scheduleFlush();
3232
+ if (buffer.length >= maxMessages) await flush("size");
3233
+ },
3234
+ consumerOptions
3235
+ );
3236
+ const originalStop = handle.stop.bind(handle);
3237
+ handle.stop = async () => {
3238
+ await flush("time");
3239
+ return originalStop();
3240
+ };
3241
+ return handle;
3242
+ }
3243
+
3244
+ // src/client/kafka.client/consumer/features/routed.ts
3245
+ async function startRoutedConsumerImpl(ctx, topics, routing, options) {
3246
+ const { header, routes, fallback } = routing;
3247
+ const handleMessage = async (envelope) => {
3248
+ const headerValue = envelope.headers[header];
3249
+ const routeHandler = headerValue === void 0 ? void 0 : routes[headerValue];
3250
+ if (routeHandler) {
3251
+ await routeHandler(envelope);
3252
+ } else {
3253
+ await fallback?.(envelope);
3254
+ }
3255
+ };
3256
+ return startConsumerImpl(ctx, topics, handleMessage, options);
3257
+ }
3258
+
3259
+ // src/client/kafka.client/consumer/features/delayed.ts
3260
+ function delayedTopicName(topic2) {
3261
+ return `${topic2}.delayed`;
3262
+ }
3263
+ async function startDelayedRelayImpl(ctx, topics, options) {
3264
+ if (topics.length === 0) {
3265
+ throw new Error("startDelayedRelay: at least one topic is required");
3266
+ }
3267
+ const gid = options?.groupId ?? `${ctx.defaultGroupId}-delayed-relay`;
3268
+ if (ctx.runningConsumers.has(gid)) {
3269
+ throw new Error(
3270
+ `startDelayedRelay("${gid}") called twice \u2014 this group is already consuming. Call stopConsumer("${gid}") first or pass a different groupId.`
3271
+ );
3272
+ }
3273
+ const delayedTopics = topics.map(delayedTopicName);
3274
+ for (const t of delayedTopics) await ensureTopic(ctx, t);
3275
+ const txProducer = await createRetryTxProducer(ctx, `${gid}-tx`);
3276
+ let resolveReady;
3277
+ const readyPromise = new Promise((resolve) => {
3278
+ resolveReady = resolve;
3279
+ });
3280
+ const consumer = getOrCreateConsumer(
3281
+ gid,
3282
+ false,
3283
+ false,
3284
+ ctx.consumerOpsDeps,
3285
+ void 0,
3286
+ resolveReady
3287
+ );
3288
+ await consumer.connect();
3289
+ await subscribeWithRetry(consumer, delayedTopics, ctx.logger);
3290
+ await consumer.run({
3291
+ eachMessage: async ({ topic: stagingTopic, partition, message }) => {
3292
+ const nextOffset = {
3293
+ topic: stagingTopic,
3294
+ partition,
3295
+ offset: (parseInt(message.offset, 10) + 1).toString()
3296
+ };
3297
+ if (!message.value) {
3298
+ await consumer.commitOffsets([nextOffset]);
3299
+ return;
3300
+ }
3301
+ const headers = decodeHeaders(message.headers);
3302
+ const target = headers[HEADER_DELAYED_TARGET] ?? stagingTopic.replace(/\.delayed$/, "");
3303
+ const until = parseInt(
3304
+ headers[HEADER_DELAYED_UNTIL] ?? "0",
3305
+ 10
3306
+ );
3307
+ const remaining = until - Date.now();
3308
+ if (remaining > 0) {
3309
+ consumer.pause([{ topic: stagingTopic, partitions: [partition] }]);
3310
+ await sleep(remaining);
3311
+ consumer.resume([{ topic: stagingTopic, partitions: [partition] }]);
3312
+ }
3313
+ const forwardHeaders = Object.fromEntries(
3314
+ Object.entries(headers).filter(
3315
+ ([k]) => k !== HEADER_DELAYED_UNTIL && k !== HEADER_DELAYED_TARGET
3316
+ )
3317
+ );
3318
+ const tx = await txProducer.transaction();
3319
+ try {
3320
+ await tx.send({
3321
+ topic: target,
3322
+ messages: [
3323
+ {
3324
+ // Forward the ORIGINAL wire bytes unchanged — no re-serialization,
3325
+ // so binary payloads (Avro/Protobuf) are relayed losslessly.
3326
+ value: message.value,
3327
+ key: message.key ? message.key.toString() : null,
3328
+ headers: forwardHeaders
3329
+ }
3330
+ ]
3331
+ });
3332
+ await tx.sendOffsets({
3333
+ consumer,
3334
+ topics: [
3335
+ {
3336
+ topic: nextOffset.topic,
3337
+ partitions: [
3338
+ { partition: nextOffset.partition, offset: nextOffset.offset }
3339
+ ]
3340
+ }
3341
+ ]
3342
+ });
3343
+ await tx.commit();
3344
+ ctx.logger.debug?.(
3345
+ `Delayed message relayed to "${target}" (deadline ${new Date(until).toISOString()})`
3346
+ );
3347
+ } catch (txErr) {
3348
+ try {
3349
+ await tx.abort();
3350
+ } catch {
3351
+ }
3352
+ ctx.logger.error(
3353
+ `Delayed relay to "${target}" failed \u2014 message will be redelivered:`,
3354
+ toError(txErr).stack
3355
+ );
3356
+ }
3357
+ }
3358
+ });
3359
+ ctx.runningConsumers.set(gid, "eachMessage");
3360
+ ctx.logger.log(
3361
+ `Delayed relay started for: ${delayedTopics.join(", ")} (group: ${gid})`
3362
+ );
3363
+ return {
3364
+ groupId: gid,
3365
+ ready: () => readyPromise,
3366
+ stop: async () => {
3367
+ await stopConsumerImpl(ctx, gid);
3368
+ }
3369
+ };
3370
+ }
3371
+
3372
+ // src/client/kafka.client/consumer/features/snapshot.ts
3373
+ async function readSnapshotImpl(ctx, topic2, options = {}) {
3374
+ await ctx.adminOps.ensureConnected();
3375
+ let offsets;
3376
+ try {
3377
+ offsets = await ctx.adminOps.admin.fetchTopicOffsets(topic2);
3378
+ } catch {
3379
+ ctx.logger.warn(
3380
+ `readSnapshot: could not fetch offsets for "${String(topic2)}", returning empty snapshot`
3381
+ );
3382
+ return /* @__PURE__ */ new Map();
3383
+ }
3384
+ const targets = /* @__PURE__ */ new Map();
3385
+ for (const { partition, high, low } of offsets) {
3386
+ const highN = Number.parseInt(high, 10);
3387
+ const lowN = Number.parseInt(low, 10);
3388
+ if (highN > lowN) targets.set(partition, highN - 1);
3389
+ }
3390
+ if (targets.size === 0) {
3391
+ ctx.logger.debug?.(
3392
+ `readSnapshot: topic "${String(topic2)}" is empty \u2014 returning empty snapshot`
3393
+ );
3394
+ return /* @__PURE__ */ new Map();
3395
+ }
3396
+ const snapshot = /* @__PURE__ */ new Map();
3397
+ const remaining = new Set(targets.keys());
3398
+ const snapshotGroupId = `${ctx.clientId}-snapshot-${Date.now()}`;
3399
+ await new Promise((resolve, reject) => {
3400
+ const consumer = ctx.transport.consumer({
3401
+ groupId: snapshotGroupId,
3402
+ fromBeginning: true
3403
+ });
3404
+ const cleanup = () => {
3405
+ consumer.disconnect().catch(() => {
3406
+ }).finally(() => {
3407
+ ctx.adminOps.deleteGroups([snapshotGroupId]).catch(() => {
3408
+ });
3409
+ });
3410
+ };
3411
+ consumer.connect().then(() => consumer.subscribe({ topics: [topic2] })).then(
3412
+ () => consumer.run({
3413
+ eachMessage: async ({ topic: t, partition, message }) => {
3414
+ if (!remaining.has(partition)) return;
3415
+ const msgOffsetN = Number.parseInt(message.offset, 10);
3416
+ applySnapshotMessage(snapshot, options, ctx, t, partition, message);
3417
+ if (msgOffsetN >= targets.get(partition)) {
3418
+ remaining.delete(partition);
3419
+ if (remaining.size === 0) {
3420
+ cleanup();
3421
+ resolve();
3422
+ }
3423
+ }
3424
+ }
3425
+ })
3426
+ ).catch((err) => {
3427
+ cleanup();
3428
+ reject(err);
3429
+ });
3430
+ });
3431
+ ctx.logger.log(
3432
+ `readSnapshot: ${snapshot.size} key(s) from "${String(topic2)}"`
3433
+ );
3434
+ return snapshot;
3435
+ }
3436
+ function applySnapshotMessage(snapshot, options, ctx, t, partition, message) {
3437
+ let key = null;
3438
+ if (message.key) {
3439
+ key = Buffer.isBuffer(message.key) ? message.key.toString() : String(message.key);
3440
+ }
3441
+ if (message.value === null || message.value === void 0) {
3442
+ if (key !== null) {
3443
+ snapshot.delete(key);
3444
+ options.onTombstone?.(key);
3445
+ }
3446
+ return;
3447
+ }
3448
+ if (key === null) return;
3449
+ const rawValue = Buffer.isBuffer(message.value) ? message.value.toString() : String(message.value);
3450
+ try {
3451
+ const jsonValue = JSON.parse(rawValue);
3452
+ const headers = decodeHeaders(message.headers);
3453
+ const parsed = options.schema ? options.schema.parse(jsonValue) : jsonValue;
3454
+ snapshot.set(
3455
+ key,
3456
+ extractEnvelope(parsed, headers, t, partition, message.offset)
3457
+ );
3458
+ } catch (err) {
3459
+ ctx.logger.warn(
3460
+ `readSnapshot: skipping ${t}:${partition}@${message.offset} \u2014 ${toError(err).message}`
3461
+ );
3462
+ }
3463
+ }
3464
+ async function checkpointOffsetsImpl(ctx, groupId, checkpointTopic) {
3465
+ const gid = groupId ?? ctx.defaultGroupId;
3466
+ await ctx.adminOps.ensureConnected();
3467
+ const committed = await ctx.adminOps.admin.fetchOffsets({ groupId: gid });
3468
+ const offsets = [];
3469
+ for (const { topic: topic2, partitions } of committed) {
3470
+ for (const { partition, offset } of partitions) {
3471
+ offsets.push({ topic: topic2, partition, offset });
3472
+ }
3473
+ }
3474
+ const savedAt = Date.now();
3475
+ const payload = JSON.stringify({ groupId: gid, offsets, savedAt });
3476
+ await ctx.producer.send({
3477
+ topic: checkpointTopic,
3478
+ messages: [
3479
+ {
3480
+ key: gid,
3481
+ value: payload,
3482
+ headers: {
3483
+ "x-checkpoint-group-id": [gid],
3484
+ "x-checkpoint-timestamp": [String(savedAt)]
3485
+ }
3486
+ }
3487
+ ]
3488
+ });
3489
+ const topics = [...new Set(offsets.map((e) => e.topic))];
3490
+ ctx.logger.log(
3491
+ `checkpointOffsets: saved ${offsets.length} partition(s) for group "${gid}" \u2192 "${checkpointTopic}"`
3492
+ );
3493
+ return { groupId: gid, topics, partitionCount: offsets.length, savedAt };
3494
+ }
3495
+ async function restoreFromCheckpointImpl(ctx, groupId, checkpointTopic, options = {}) {
3496
+ const gid = groupId ?? ctx.defaultGroupId;
3497
+ if (ctx.runningConsumers.has(gid)) {
3498
+ throw new Error(
3499
+ `restoreFromCheckpoint: consumer group "${gid}" is still running. Call stopConsumer("${gid}") before restoring offsets.`
3500
+ );
3501
+ }
3502
+ await ctx.adminOps.ensureConnected();
3503
+ const checkpoints = [];
3504
+ let hwmOffsets;
3505
+ try {
3506
+ hwmOffsets = await ctx.adminOps.admin.fetchTopicOffsets(checkpointTopic);
3507
+ } catch {
3508
+ throw new Error(
3509
+ `restoreFromCheckpoint: could not fetch offsets for "${checkpointTopic}" \u2014 does the topic exist?`
3510
+ );
3511
+ }
3512
+ const targets = /* @__PURE__ */ new Map();
3513
+ for (const { partition, high, low } of hwmOffsets) {
3514
+ const highN = Number.parseInt(high, 10);
3515
+ if (highN > Number.parseInt(low, 10)) targets.set(partition, highN - 1);
3516
+ }
3517
+ if (targets.size > 0) {
3518
+ const checkpointGroupId = `${ctx.clientId}-checkpoint-restore-${Date.now()}`;
3519
+ await new Promise((resolve, reject) => {
3520
+ const consumer = ctx.transport.consumer({
3521
+ groupId: checkpointGroupId,
3522
+ fromBeginning: true
3523
+ });
3524
+ const cleanup = () => {
3525
+ consumer.disconnect().catch(() => {
3526
+ }).finally(() => {
3527
+ ctx.adminOps.deleteGroups([checkpointGroupId]).catch(() => {
3528
+ });
3529
+ });
3530
+ };
3531
+ const remaining = new Set(targets.keys());
3532
+ consumer.connect().then(() => consumer.subscribe({ topics: [checkpointTopic] })).then(
3533
+ () => consumer.run({
3534
+ eachMessage: async ({ partition, message }) => {
3535
+ if (!remaining.has(partition)) return;
3536
+ let msgKey = null;
3537
+ if (message.key) {
3538
+ msgKey = Buffer.isBuffer(message.key) ? message.key.toString() : String(message.key);
3539
+ }
3540
+ if (msgKey === gid && message.value) {
3541
+ try {
3542
+ const raw = Buffer.isBuffer(message.value) ? message.value.toString() : String(message.value);
3543
+ const parsed = JSON.parse(raw);
3544
+ checkpoints.push({
3545
+ savedAt: parsed.savedAt,
3546
+ offsets: parsed.offsets
3547
+ });
3548
+ } catch {
3549
+ ctx.logger.warn(
3550
+ `restoreFromCheckpoint: skipping malformed checkpoint at partition ${partition}@${message.offset}`
3551
+ );
3552
+ }
3553
+ }
3554
+ if (Number.parseInt(message.offset, 10) >= targets.get(partition)) {
3555
+ remaining.delete(partition);
3556
+ if (remaining.size === 0) {
3557
+ cleanup();
3558
+ resolve();
3559
+ }
3560
+ }
3561
+ }
3562
+ })
3563
+ ).catch((err) => {
3564
+ cleanup();
3565
+ reject(err);
3566
+ });
3567
+ });
3568
+ }
3569
+ if (checkpoints.length === 0) {
3570
+ throw new Error(
3571
+ `restoreFromCheckpoint: no checkpoints found for group "${gid}" in "${checkpointTopic}"`
3572
+ );
3573
+ }
3574
+ const target = options.timestamp;
3575
+ let best;
3576
+ if (target === void 0) {
3577
+ best = checkpoints.reduce(
3578
+ (acc, c) => c.savedAt > acc.savedAt ? c : acc,
3579
+ checkpoints[0]
3580
+ );
3581
+ } else {
3582
+ const candidates = checkpoints.filter((c) => c.savedAt <= target);
3583
+ if (candidates.length > 0) {
3584
+ best = candidates.reduce(
3585
+ (acc, c) => c.savedAt > acc.savedAt ? c : acc,
3586
+ candidates[0]
3587
+ );
3588
+ } else {
3589
+ best = checkpoints.reduce(
3590
+ (acc, c) => c.savedAt < acc.savedAt ? c : acc,
3591
+ checkpoints[0]
3592
+ );
3593
+ ctx.logger.warn(
3594
+ `restoreFromCheckpoint: no checkpoint at or before ${new Date(target).toISOString()} \u2014 using oldest available (${new Date(best.savedAt).toISOString()})`
3595
+ );
3596
+ }
3597
+ }
3598
+ await ctx.adminOps.seekToOffset(gid, best.offsets);
3599
+ const checkpointAge = Date.now() - best.savedAt;
3600
+ ctx.logger.log(
3601
+ `restoreFromCheckpoint: restored ${best.offsets.length} partition(s) for group "${gid}" from checkpoint at ${new Date(best.savedAt).toISOString()} (age: ${checkpointAge}ms)`
3602
+ );
3603
+ return {
3604
+ groupId: gid,
3605
+ offsets: best.offsets,
3606
+ restoredAt: best.savedAt,
3607
+ checkpointAge
3608
+ };
3609
+ }
3610
+
3611
+ // src/client/kafka.client/index.ts
3612
+ var DLQ_HEADER_KEYS = /* @__PURE__ */ new Set([
3613
+ "x-dlq-original-topic",
3614
+ "x-dlq-failed-at",
3615
+ "x-dlq-error-message",
3616
+ "x-dlq-error-stack",
3617
+ "x-dlq-attempt-count"
3618
+ ]);
3619
+ var KafkaClient = class {
3620
+ clientId;
3621
+ ctx;
3622
+ /**
3623
+ * Create a new KafkaClient.
3624
+ * @param clientId Unique client identifier (used in Kafka metadata and logs).
3625
+ * @param groupId Default consumer group ID for this client.
3626
+ * @param brokers Array of broker addresses, e.g. `['localhost:9092']`.
3627
+ * @param options Optional client-wide configuration.
3628
+ * @example
3629
+ * ```ts
3630
+ * const kafka = new KafkaClient('my-service', 'my-service-group', ['localhost:9092'], {
3631
+ * lagThrottle: { maxLag: 10_000 },
3632
+ * onMessageLost: (ctx) => logger.error('Message lost', ctx),
3633
+ * });
3634
+ * await kafka.connectProducer();
3635
+ * ```
3636
+ */
3637
+ constructor(clientId, groupId, brokers, options) {
3638
+ validateClientOptions(clientId, groupId, brokers, options);
3639
+ this.clientId = clientId;
3640
+ const logger = options?.logger ?? {
3641
+ log: (msg) => console.log(`[KafkaClient:${clientId}] ${msg}`),
3642
+ warn: (msg, ...args) => console.warn(`[KafkaClient:${clientId}] ${msg}`, ...args),
3643
+ error: (msg, ...args) => console.error(`[KafkaClient:${clientId}] ${msg}`, ...args),
3644
+ debug: (msg, ...args) => console.debug(`[KafkaClient:${clientId}] ${msg}`, ...args)
3645
+ };
3646
+ const security = resolveSecurityOptions(options?.security, brokers, logger);
3647
+ const transport = options?.transport ?? new ConfluentTransport(clientId, brokers, security);
3648
+ const producer = transport.producer();
3649
+ const runningConsumers = /* @__PURE__ */ new Map();
3650
+ const consumers = /* @__PURE__ */ new Map();
3651
+ const consumerCreationOptions = /* @__PURE__ */ new Map();
3652
+ const schemaRegistry = /* @__PURE__ */ new Map();
3653
+ const serde = options?.serde ?? new JsonSerde();
3654
+ const adminOps = new AdminOps({
3655
+ admin: transport.admin(),
3656
+ logger,
3657
+ runningConsumers,
3658
+ defaultGroupId: groupId,
3659
+ clientId
3660
+ });
3661
+ const circuitBreaker = new CircuitBreakerManager({
3662
+ pauseConsumer: (gid, assignments) => pauseConsumerImpl(this.ctx, gid, assignments),
3663
+ resumeConsumer: (gid, assignments) => resumeConsumerImpl(this.ctx, gid, assignments),
3664
+ logger,
3665
+ instrumentation: options?.instrumentation ?? []
3666
+ });
3667
+ const metrics = new MetricsManager({
3668
+ instrumentation: options?.instrumentation ?? [],
3669
+ onCircuitFailure: (envelope, gid) => circuitBreaker.onFailure(envelope, gid),
3670
+ onCircuitSuccess: (envelope, gid) => circuitBreaker.onSuccess(envelope, gid)
3671
+ });
3672
+ const inFlight = new InFlightTracker((msg) => logger.warn(msg));
3673
+ const ctx = {
3674
+ clientId,
3675
+ defaultGroupId: groupId,
3676
+ logger,
3677
+ autoCreateTopicsEnabled: options?.autoCreateTopics ?? false,
3678
+ strictSchemasEnabled: options?.strictSchemas ?? true,
3679
+ numPartitions: options?.numPartitions ?? 1,
3680
+ serde,
3681
+ txId: options?.transactionalId ?? `${clientId}-tx`,
3682
+ clockRecoveryTopics: options?.clockRecovery?.topics ?? [],
3683
+ clockRecoveryTimeoutMs: options?.clockRecovery?.timeoutMs ?? 3e4,
3684
+ lagThrottleOpts: options?.lagThrottle,
3685
+ instrumentation: options?.instrumentation ?? [],
3686
+ onMessageLost: options?.onMessageLost,
3687
+ onTtlExpired: options?.onTtlExpired,
3688
+ onRebalance: options?.onRebalance,
3689
+ transport,
3690
+ producer,
3691
+ txProducer: void 0,
3692
+ txProducerInitPromise: void 0,
3693
+ _txChain: Promise.resolve(),
3694
+ retryTxProducers: /* @__PURE__ */ new Map(),
3695
+ consumers,
3696
+ runningConsumers,
3697
+ consumerCreationOptions,
3698
+ companionGroupIds: /* @__PURE__ */ new Map(),
3699
+ dedupStates: /* @__PURE__ */ new Map(),
3700
+ ensuredTopics: /* @__PURE__ */ new Set(),
3701
+ ensureTopicPromises: /* @__PURE__ */ new Map(),
3702
+ schemaRegistry,
3703
+ _lagThrottled: false,
3704
+ _lagThrottleTimer: void 0,
3705
+ _lamportClock: 0,
3706
+ circuitBreaker,
3707
+ adminOps,
3708
+ metrics,
3709
+ inFlight,
3710
+ producerOpsDeps: {
3711
+ schemaRegistry,
3712
+ strictSchemasEnabled: options?.strictSchemas ?? true,
3713
+ instrumentation: options?.instrumentation ?? [],
3714
+ logger,
3715
+ serde,
3716
+ nextLamportClock: () => 0
3717
+ // patched below
3718
+ },
3719
+ consumerOpsDeps: {
3720
+ consumers,
3721
+ consumerCreationOptions,
3722
+ transport,
3723
+ onRebalance: options?.onRebalance,
3724
+ logger
3725
+ },
3726
+ retryTopicDeps: {}
3727
+ // patched below
3728
+ };
3729
+ ctx.producerOpsDeps = {
3730
+ schemaRegistry,
3731
+ strictSchemasEnabled: options?.strictSchemas ?? true,
3732
+ instrumentation: options?.instrumentation ?? [],
3733
+ logger,
3734
+ serde,
3735
+ nextLamportClock: () => ++ctx._lamportClock
3736
+ };
3737
+ ctx.retryTopicDeps = buildRetryTopicDeps(ctx);
3738
+ this.ctx = ctx;
3739
+ }
3740
+ async sendMessage(topicOrDesc, message, options = {}) {
3741
+ return sendMessageImpl(this.ctx, topicOrDesc, message, options);
3742
+ }
3743
+ /** @inheritDoc */
3744
+ async sendTombstone(topic2, key, headers) {
3745
+ return sendTombstoneImpl(this.ctx, topic2, key, headers);
3746
+ }
3747
+ async sendBatch(topicOrDesc, messages, options) {
3748
+ return sendBatchImpl(this.ctx, topicOrDesc, messages, options);
3749
+ }
3750
+ /** @inheritDoc */
3751
+ async transaction(fn) {
3752
+ return transactionImpl(this.ctx, fn);
3753
+ }
3754
+ // ── Producer lifecycle ────────────────────────────────────────────
3755
+ /** @inheritDoc */
3756
+ async connectProducer() {
3757
+ return connectProducerImpl(this.ctx);
3758
+ }
3759
+ /** @internal */
3760
+ async disconnectProducer() {
3761
+ if (this.ctx._lagThrottleTimer) {
3762
+ clearInterval(this.ctx._lagThrottleTimer);
3763
+ this.ctx._lagThrottleTimer = void 0;
3764
+ }
3765
+ await this.ctx.producer.disconnect();
3766
+ this.ctx.logger.log("Producer disconnected");
3767
+ }
3768
+ async startConsumer(topics, handleMessage, options = {}) {
3769
+ return startConsumerImpl(this.ctx, topics, handleMessage, options);
3770
+ }
3771
+ async startBatchConsumer(topics, handleBatch, options = {}) {
3772
+ return startBatchConsumerImpl(this.ctx, topics, handleBatch, options);
3773
+ }
3774
+ // ── Consumer: AsyncIterableIterator ──────────────────────────────
3775
+ /** @inheritDoc */
3776
+ consume(topic2, options) {
3777
+ if (options?.retryTopics) {
3778
+ throw new Error(
3779
+ "consume() does not support retryTopics (EOS retry chains). Use startConsumer() with retryTopics: true for guaranteed retry delivery."
3780
+ );
3781
+ }
3782
+ const gid = options?.groupId ?? this.ctx.defaultGroupId;
3783
+ const queue = new AsyncQueue(
3784
+ options?.queueHighWaterMark,
3785
+ () => pauseTopicAllPartitions(this.ctx, gid, topic2),
3786
+ () => resumeTopicAllPartitions(this.ctx, gid, topic2)
3787
+ );
3788
+ const handlePromise = this.startConsumer(
3789
+ [topic2],
3790
+ async (envelope) => {
3791
+ queue.push(envelope);
3792
+ },
3793
+ options
3794
+ );
3795
+ handlePromise.catch((err) => queue.fail(err));
3796
+ return {
3797
+ [Symbol.asyncIterator]() {
3798
+ return this;
3799
+ },
3800
+ next: () => queue.next(),
3801
+ return: async () => {
3802
+ queue.close();
3803
+ const handle = await handlePromise;
3804
+ await handle.stop();
3805
+ return { value: void 0, done: true };
3806
+ }
3807
+ };
3808
+ }
3809
+ // ── Consumer: windowed ────────────────────────────────────────────
3810
+ /** @inheritDoc */
3811
+ startWindowConsumer(topic2, handler, options) {
3812
+ return startWindowConsumerImpl(this.ctx, topic2, handler, options);
3813
+ }
3814
+ // ── Consumer: header routing ──────────────────────────────────────
3815
+ /** @inheritDoc */
3816
+ startRoutedConsumer(topics, routing, options) {
3817
+ return startRoutedConsumerImpl(this.ctx, topics, routing, options);
3818
+ }
3819
+ // ── Consumer: delayed delivery relay ──────────────────────────────
3820
+ /**
3821
+ * Start a relay that delivers messages produced with
3822
+ * `SendOptions.deliverAfterMs` from `<topic>.delayed` to their target topic
3823
+ * once their deadline passes.
3824
+ *
3825
+ * Forwarding is transactional (produce + source-offset commit are atomic),
3826
+ * so no duplicates are relayed even if the relay crashes mid-forward.
3827
+ * Delivery time is a lower bound — the relay must be running for delayed
3828
+ * messages to be delivered at all.
3829
+ *
3830
+ * @param topics Target topic name(s) whose `<topic>.delayed` staging topics to relay.
3831
+ * @param options Optional `groupId` override (default: `<defaultGroupId>-delayed-relay`).
3832
+ *
3833
+ * @example
3834
+ * ```ts
3835
+ * await kafka.startDelayedRelay(['orders.reminder']);
3836
+ * await kafka.sendMessage('orders.reminder', payload, { deliverAfterMs: 60_000 });
3837
+ * // → delivered to orders.reminder ~60 s later
3838
+ * ```
3839
+ */
3840
+ async startDelayedRelay(topics, options) {
3841
+ const list = Array.isArray(topics) ? topics : [topics];
3842
+ return startDelayedRelayImpl(this.ctx, list, options);
3843
+ }
3844
+ // ── Consumer: transactional EOS ───────────────────────────────────
3845
+ /** @inheritDoc */
3846
+ async startTransactionalConsumer(topics, handler, options = {}) {
3847
+ return startTransactionalConsumerImpl(this.ctx, topics, handler, options);
3848
+ }
3849
+ // ── Consumer lifecycle ────────────────────────────────────────────
3850
+ /** @inheritDoc */
3851
+ async stopConsumer(groupId) {
3852
+ return stopConsumerImpl(this.ctx, groupId);
3853
+ }
3854
+ /** @inheritDoc */
3855
+ pauseConsumer(groupId, assignments) {
3856
+ return pauseConsumerImpl(this.ctx, groupId, assignments);
3857
+ }
3858
+ /** @inheritDoc */
3859
+ resumeConsumer(groupId, assignments) {
3860
+ return resumeConsumerImpl(this.ctx, groupId, assignments);
3861
+ }
3862
+ // ── DLQ replay ────────────────────────────────────────────────────
3863
+ /** @inheritDoc */
3864
+ async replayDlq(topic2, options = {}) {
3865
+ await this.ctx.adminOps.ensureConnected();
3866
+ return replayDlqTopic(
3867
+ topic2,
3868
+ {
3869
+ logger: this.ctx.logger,
3870
+ fetchTopicOffsets: (t) => this.ctx.adminOps.admin.fetchTopicOffsets(t),
3871
+ send: async (t, messages) => {
3872
+ await this.ctx.producer.send({ topic: t, messages });
3873
+ },
3874
+ createConsumer: (gid, fromBeginning) => getOrCreateConsumer(gid, fromBeginning, true, this.ctx.consumerOpsDeps),
3875
+ cleanupConsumer: (consumer, gid, deleteGroup) => {
3876
+ consumer.disconnect().catch(() => {
3877
+ }).finally(() => {
3878
+ this.ctx.consumers.delete(gid);
3879
+ this.ctx.runningConsumers.delete(gid);
3880
+ this.ctx.consumerCreationOptions.delete(gid);
3881
+ if (deleteGroup) {
3882
+ this.ctx.adminOps.deleteGroups([gid]).catch(() => {
3883
+ });
3884
+ }
3885
+ });
3886
+ },
3887
+ dlqHeaderKeys: DLQ_HEADER_KEYS
3888
+ },
3889
+ options
3890
+ );
3891
+ }
3892
+ // ── Snapshot & checkpoint ─────────────────────────────────────────
3893
+ /** @inheritDoc */
3894
+ async readSnapshot(topic2, options = {}) {
3895
+ return readSnapshotImpl(this.ctx, topic2, options);
3896
+ }
3897
+ /** @inheritDoc */
3898
+ async checkpointOffsets(groupId, checkpointTopic) {
3899
+ return checkpointOffsetsImpl(this.ctx, groupId, checkpointTopic);
3900
+ }
3901
+ /** @inheritDoc */
3902
+ async restoreFromCheckpoint(groupId, checkpointTopic, options = {}) {
3903
+ return restoreFromCheckpointImpl(this.ctx, groupId, checkpointTopic, options);
3904
+ }
3905
+ // ── Admin ─────────────────────────────────────────────────────────
3906
+ /** @inheritDoc */
3907
+ async resetOffsets(groupId, topic2, position) {
3908
+ return this.ctx.adminOps.resetOffsets(groupId, topic2, position);
3909
+ }
3910
+ /** @inheritDoc */
3911
+ async seekToOffset(groupId, assignments) {
3912
+ return this.ctx.adminOps.seekToOffset(groupId, assignments);
3913
+ }
3914
+ /** @inheritDoc */
3915
+ async seekToTimestamp(groupId, assignments) {
3916
+ return this.ctx.adminOps.seekToTimestamp(groupId, assignments);
3917
+ }
3918
+ /** @inheritDoc */
3919
+ async getConsumerLag(groupId) {
3920
+ return this.ctx.adminOps.getConsumerLag(groupId);
3921
+ }
3922
+ /** @inheritDoc */
3923
+ async checkStatus() {
3924
+ return this.ctx.adminOps.checkStatus();
3925
+ }
3926
+ /** @inheritDoc */
3927
+ async listConsumerGroups() {
3928
+ return this.ctx.adminOps.listConsumerGroups();
3929
+ }
3930
+ /** @inheritDoc */
3931
+ async describeTopics(topics) {
3932
+ return this.ctx.adminOps.describeTopics(topics);
3933
+ }
3934
+ /** @inheritDoc */
3935
+ async deleteRecords(topic2, partitions) {
3936
+ return this.ctx.adminOps.deleteRecords(topic2, partitions);
3937
+ }
3938
+ // ── Circuit breaker ───────────────────────────────────────────────
3939
+ /** @inheritDoc */
3940
+ getCircuitState(topic2, partition, groupId) {
3941
+ return this.ctx.circuitBreaker.getState(
3942
+ topic2,
3943
+ partition,
3944
+ groupId ?? this.ctx.defaultGroupId
3945
+ );
3946
+ }
3947
+ // ── Metrics ───────────────────────────────────────────────────────
3948
+ /** @inheritDoc */
3949
+ getMetrics(topic2) {
3950
+ return this.ctx.metrics.getMetrics(topic2);
3951
+ }
3952
+ /** @inheritDoc */
3953
+ resetMetrics(topic2) {
3954
+ this.ctx.metrics.resetMetrics(topic2);
3955
+ }
3956
+ getClientId() {
3957
+ return this.clientId;
3958
+ }
3959
+ // ── Lifecycle ─────────────────────────────────────────────────────
3960
+ /** @inheritDoc */
3961
+ async disconnect(drainTimeoutMs = 3e4) {
3962
+ return disconnectImpl(this.ctx, drainTimeoutMs);
3963
+ }
3964
+ /** NestJS lifecycle hook — called automatically on module teardown. */
3965
+ async onModuleDestroy() {
3966
+ await this.disconnect();
3967
+ }
3968
+ /** @inheritDoc */
3969
+ enableGracefulShutdown(signals = ["SIGTERM", "SIGINT"], drainTimeoutMs = 3e4) {
3970
+ const handler = () => {
3971
+ this.ctx.logger.log(
3972
+ "Shutdown signal received \u2014 draining in-flight handlers..."
3973
+ );
3974
+ this.disconnect(drainTimeoutMs).catch(
3975
+ (err) => this.ctx.logger.error(
3976
+ "Error during graceful shutdown:",
3977
+ toError(err).message
3978
+ )
3979
+ ).finally(() => process.exit(0));
3980
+ };
3981
+ for (const signal of signals) process.once(signal, handler);
3982
+ }
3983
+ };
3984
+
3985
+ // src/client/message/topic.ts
3986
+ function topic(name) {
3987
+ return {
3988
+ /** Provide an explicit message type without a runtime schema. */
3989
+ type: () => keyable({
3990
+ __topic: name,
3991
+ __type: void 0
3992
+ }),
3993
+ schema: (schema) => keyable({
3994
+ __topic: name,
3995
+ __type: void 0,
3996
+ __schema: schema
3997
+ })
3998
+ };
3999
+ }
4000
+ function keyable(desc) {
4001
+ return {
4002
+ ...desc,
4003
+ key: (extractor) => keyable({ ...desc, __key: extractor }),
4004
+ serde: (serde) => keyable({ ...desc, __serde: serde })
4005
+ };
4006
+ }
4007
+
4008
+ // src/client/message/versioned-schema.ts
4009
+ function versionedSchema(versions, options) {
4010
+ const registered = Object.keys(versions).map(Number).filter((v) => Number.isInteger(v) && v > 0).sort((a, b) => a - b);
4011
+ if (registered.length === 0) {
4012
+ throw new Error(
4013
+ "versionedSchema: at least one schema version must be registered (keys must be positive integers)"
4014
+ );
4015
+ }
4016
+ const latestVersion = registered[registered.length - 1];
4017
+ return {
4018
+ async parse(data, ctx) {
4019
+ const version = ctx?.version ?? latestVersion;
4020
+ const schema = versions[version];
4021
+ if (!schema) {
4022
+ throw new Error(
4023
+ `versionedSchema: no schema registered for version ${version}${ctx?.topic ? ` (topic "${ctx.topic}")` : ""} \u2014 registered versions: ${registered.join(", ")}`
4024
+ );
4025
+ }
4026
+ const parsed = await schema.parse(data, ctx);
4027
+ if (version < latestVersion && options?.migrate) {
4028
+ return options.migrate(parsed, version, latestVersion);
4029
+ }
4030
+ return parsed;
4031
+ }
4032
+ };
4033
+ }
4034
+
4035
+ // src/client/outbox/outbox.store.ts
4036
+ var InMemoryOutboxStore = class {
4037
+ /** Insertion-ordered rows. `published` flips to true after `markPublished`. */
4038
+ rows = [];
4039
+ /**
4040
+ * Append a message to the outbox. In a real store this INSERT would run inside
4041
+ * the same DB transaction as the corresponding business write.
4042
+ */
4043
+ add(message) {
4044
+ this.rows.push({ message, published: false });
4045
+ }
4046
+ async fetchUnpublished(limit) {
4047
+ const out = [];
4048
+ for (const row of this.rows) {
4049
+ if (row.published) continue;
4050
+ out.push(row.message);
4051
+ if (out.length >= limit) break;
4052
+ }
4053
+ return out;
4054
+ }
4055
+ async markPublished(ids) {
4056
+ const idSet = new Set(ids);
4057
+ for (const row of this.rows) {
4058
+ if (idSet.has(row.message.id)) row.published = true;
4059
+ }
4060
+ }
4061
+ /** Test helper: count of rows not yet marked published. */
4062
+ get pendingCount() {
4063
+ return this.rows.filter((r) => !r.published).length;
4064
+ }
4065
+ /** Test helper: count of rows marked published. */
4066
+ get publishedCount() {
4067
+ return this.rows.filter((r) => r.published).length;
4068
+ }
4069
+ };
4070
+
4071
+ // src/client/outbox/outbox.relay.ts
4072
+ function toError2(e) {
4073
+ return e instanceof Error ? e : new Error(String(e));
4074
+ }
4075
+ function startOutboxRelay(kafka, store, options = {}) {
4076
+ const pollIntervalMs = options.pollIntervalMs ?? 1e3;
4077
+ const batchSize = options.batchSize ?? 100;
4078
+ const onError = options.onError ?? ((error, batch) => {
4079
+ console.error(
4080
+ `[outbox] batch of ${batch.length} message(s) failed \u2014 will retry:`,
4081
+ error
4082
+ );
4083
+ });
4084
+ const onPublished = options.onPublished;
4085
+ let stopped = false;
4086
+ let running = false;
4087
+ let inFlight = Promise.resolve();
4088
+ const iterate = async () => {
4089
+ let batch = [];
4090
+ try {
4091
+ batch = await store.fetchUnpublished(batchSize);
4092
+ if (batch.length === 0) return;
4093
+ await kafka.transaction(async (tx) => {
4094
+ for (const msg of batch) {
4095
+ await tx.send(msg.topic, msg.payload, {
4096
+ key: msg.key,
4097
+ headers: msg.headers,
4098
+ correlationId: msg.correlationId,
4099
+ eventId: msg.eventId
4100
+ });
4101
+ }
4102
+ });
4103
+ await store.markPublished(batch.map((m) => m.id));
4104
+ onPublished?.(batch.length);
4105
+ } catch (err) {
4106
+ onError(toError2(err), batch);
4107
+ }
4108
+ };
4109
+ const tick = () => {
4110
+ if (stopped || running) return;
4111
+ running = true;
4112
+ inFlight = iterate().finally(() => {
4113
+ running = false;
4114
+ });
4115
+ };
4116
+ const timer = setInterval(tick, pollIntervalMs);
4117
+ timer.unref?.();
4118
+ return {
4119
+ stop: async () => {
4120
+ stopped = true;
4121
+ clearInterval(timer);
4122
+ await inFlight;
4123
+ }
4124
+ };
4125
+ }
4126
+
4127
+ // src/client/security/providers.ts
4128
+ var defaultImport = (specifier) => import(specifier);
4129
+ function awsMskIamProvider(options) {
4130
+ const importFn = options.importFn ?? defaultImport;
4131
+ return async () => {
4132
+ let signer;
4133
+ try {
4134
+ signer = await importFn("aws-msk-iam-sasl-signer-js");
4135
+ } catch {
4136
+ throw new Error(
4137
+ "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."
4138
+ );
4139
+ }
4140
+ const { token, expiryTime } = await signer.generateAuthToken({
4141
+ region: options.region
4142
+ });
4143
+ return {
4144
+ value: token,
4145
+ principal: "msk-iam",
4146
+ // expiryTime is epoch ms per the signer's contract
4147
+ lifetimeMs: expiryTime
4148
+ };
4149
+ };
4150
+ }
4151
+ function gcpAccessTokenProvider(options = {}) {
4152
+ const importFn = options.importFn ?? defaultImport;
4153
+ const ttlMs = options.tokenTtlMs ?? 50 * 6e4;
4154
+ return async () => {
4155
+ let lib;
4156
+ try {
4157
+ lib = await importFn("google-auth-library");
4158
+ } catch {
4159
+ throw new Error(
4160
+ "gcpAccessTokenProvider: package 'google-auth-library' is not installed. Run `npm install google-auth-library` to enable GCP authentication."
4161
+ );
4162
+ }
4163
+ const auth = new lib.GoogleAuth({
4164
+ scopes: options.scopes ?? ["https://www.googleapis.com/auth/cloud-platform"]
4165
+ });
4166
+ const token = await auth.getAccessToken();
4167
+ if (!token) {
4168
+ throw new Error(
4169
+ "gcpAccessTokenProvider: google-auth-library returned no access token \u2014 check Application Default Credentials."
4170
+ );
4171
+ }
4172
+ return {
4173
+ value: token,
4174
+ principal: options.principal ?? "gcp",
4175
+ lifetimeMs: Date.now() + ttlMs
4176
+ };
4177
+ };
4178
+ }
4179
+
4180
+ // src/client/security/acl.ts
4181
+ function addResource(out, r) {
4182
+ const key = `${r.resourceType}:${r.patternType}:${r.name}`;
4183
+ const existing = out.get(key);
4184
+ if (existing) {
4185
+ for (const op of r.operations)
4186
+ if (!existing.operations.includes(op)) existing.operations.push(op);
4187
+ if (!existing.reason.includes(r.reason))
4188
+ existing.reason += `; ${r.reason}`;
4189
+ } else {
4190
+ out.set(key, { ...r, operations: [...r.operations] });
4191
+ }
4192
+ }
4193
+ function describeRequiredAcls(input) {
4194
+ const out = /* @__PURE__ */ new Map();
4195
+ const f = input.features ?? {};
4196
+ const produce = input.produceTopics ?? [];
4197
+ const consume = input.consumeTopics ?? [];
4198
+ const groups = input.groupIds ?? [];
4199
+ for (const t of produce) {
4200
+ addResource(out, {
4201
+ resourceType: "topic",
4202
+ patternType: "literal",
4203
+ name: t,
4204
+ operations: ["WRITE", "DESCRIBE"],
4205
+ reason: "sendMessage/sendBatch"
4206
+ });
4207
+ }
4208
+ for (const t of consume) {
4209
+ addResource(out, {
4210
+ resourceType: "topic",
4211
+ patternType: "literal",
4212
+ name: t,
4213
+ operations: ["READ", "DESCRIBE"],
4214
+ reason: "startConsumer"
4215
+ });
4216
+ }
4217
+ for (const g of groups) {
4218
+ addResource(out, {
4219
+ resourceType: "group",
4220
+ patternType: "literal",
4221
+ name: g,
4222
+ operations: ["READ", "DESCRIBE"],
4223
+ reason: "consumer group membership + offset commits"
4224
+ });
4225
+ }
4226
+ if (f.dlq) {
4227
+ for (const t of consume) {
4228
+ addResource(out, {
4229
+ resourceType: "topic",
4230
+ patternType: "literal",
4231
+ name: `${t}.dlq`,
4232
+ operations: ["WRITE", "DESCRIBE"],
4233
+ reason: "dlq: true \u2014 failed messages routed to DLQ"
4234
+ });
4235
+ }
4236
+ }
4237
+ if (f.retryTopics) {
4238
+ for (const t of consume) {
4239
+ for (let level = 1; level <= f.retryTopics.maxRetries; level++) {
4240
+ addResource(out, {
4241
+ resourceType: "topic",
4242
+ patternType: "literal",
4243
+ name: `${t}.retry.${level}`,
4244
+ operations: ["READ", "WRITE", "DESCRIBE"],
4245
+ reason: "retryTopics \u2014 retry chain produce + companion consume"
4246
+ });
4247
+ }
4248
+ }
4249
+ for (const g of groups) {
4250
+ addResource(out, {
4251
+ resourceType: "group",
4252
+ patternType: "prefixed",
4253
+ name: `${g}-retry.`,
4254
+ operations: ["READ", "DESCRIBE"],
4255
+ reason: "retryTopics \u2014 companion retry-level consumer groups"
4256
+ });
4257
+ addResource(out, {
4258
+ resourceType: "transactional-id",
4259
+ patternType: "prefixed",
4260
+ name: `${g}-`,
4261
+ operations: ["WRITE", "DESCRIBE"],
4262
+ reason: "retryTopics \u2014 EOS routing transactions per retry level"
4263
+ });
4264
+ }
4265
+ }
4266
+ if (f.delayedDelivery) {
4267
+ for (const t of [.../* @__PURE__ */ new Set([...produce, ...consume])]) {
4268
+ addResource(out, {
4269
+ resourceType: "topic",
4270
+ patternType: "literal",
4271
+ name: `${t}.delayed`,
4272
+ operations: ["READ", "WRITE", "DESCRIBE"],
4273
+ reason: "deliverAfterMs staging + startDelayedRelay consume"
4274
+ });
4275
+ }
4276
+ for (const g of groups) {
4277
+ addResource(out, {
4278
+ resourceType: "group",
4279
+ patternType: "literal",
4280
+ name: `${g}-delayed-relay`,
4281
+ operations: ["READ", "DESCRIBE"],
4282
+ reason: "startDelayedRelay consumer group"
4283
+ });
4284
+ addResource(out, {
4285
+ resourceType: "transactional-id",
4286
+ patternType: "literal",
4287
+ name: `${g}-delayed-relay-tx`,
4288
+ operations: ["WRITE", "DESCRIBE"],
4289
+ reason: "startDelayedRelay transactional forwarding"
4290
+ });
4291
+ }
4292
+ }
4293
+ if (f.duplicatesTopic) {
4294
+ if (typeof f.duplicatesTopic === "string") {
4295
+ addResource(out, {
4296
+ resourceType: "topic",
4297
+ patternType: "literal",
4298
+ name: f.duplicatesTopic,
4299
+ operations: ["WRITE", "DESCRIBE"],
4300
+ reason: "deduplication.strategy 'topic' \u2014 custom duplicates topic"
4301
+ });
4302
+ } else {
4303
+ for (const t of consume) {
4304
+ addResource(out, {
4305
+ resourceType: "topic",
4306
+ patternType: "literal",
4307
+ name: `${t}.duplicates`,
4308
+ operations: ["WRITE", "DESCRIBE"],
4309
+ reason: "deduplication.strategy 'topic'"
4310
+ });
4311
+ }
4312
+ }
4313
+ }
4314
+ if (f.dlqReplay) {
4315
+ for (const t of consume) {
4316
+ addResource(out, {
4317
+ resourceType: "group",
4318
+ patternType: "prefixed",
4319
+ name: `${t}.dlq-replay`,
4320
+ operations: ["READ", "DESCRIBE", "DELETE"],
4321
+ reason: "replayDlq \u2014 ephemeral/stable replay groups (deleted after use)"
4322
+ });
4323
+ addResource(out, {
4324
+ resourceType: "topic",
4325
+ patternType: "literal",
4326
+ name: `${t}.dlq`,
4327
+ operations: ["READ", "DESCRIBE"],
4328
+ reason: "replayDlq \u2014 reads the DLQ"
4329
+ });
4330
+ }
4331
+ }
4332
+ if (f.snapshots) {
4333
+ addResource(out, {
4334
+ resourceType: "group",
4335
+ patternType: "prefixed",
4336
+ name: `${input.clientId}-snapshot-`,
4337
+ operations: ["READ", "DESCRIBE", "DELETE"],
4338
+ reason: "readSnapshot \u2014 timestamped ephemeral groups (deleted after use)"
4339
+ });
4340
+ }
4341
+ if (f.clockRecovery) {
4342
+ addResource(out, {
4343
+ resourceType: "group",
4344
+ patternType: "prefixed",
4345
+ name: `${input.clientId}-clock-recovery-`,
4346
+ operations: ["READ", "DESCRIBE", "DELETE"],
4347
+ reason: "clockRecovery \u2014 timestamped ephemeral groups (deleted after use)"
4348
+ });
4349
+ }
4350
+ if (f.transactions) {
4351
+ addResource(out, {
4352
+ resourceType: "transactional-id",
4353
+ patternType: "literal",
4354
+ name: `${input.clientId}-tx`,
4355
+ operations: ["WRITE", "DESCRIBE"],
4356
+ reason: "transaction() \u2014 default transactionalId (override-aware: adjust if you set one)"
4357
+ });
4358
+ }
4359
+ if (f.autoCreateTopics) {
4360
+ addResource(out, {
4361
+ resourceType: "cluster",
4362
+ patternType: "literal",
4363
+ name: "kafka-cluster",
4364
+ operations: ["CREATE"],
4365
+ reason: "autoCreateTopics: true \u2014 not recommended in production"
4366
+ });
4367
+ }
4368
+ return [...out.values()];
4369
+ }
4370
+ function toKafkaAclCommands(resources, principal, bootstrapServer = "<bootstrap-server>") {
4371
+ return resources.map((r) => {
4372
+ const ops = r.operations.map((o) => `--operation ${o}`).join(" ");
4373
+ const resourceFlag = r.resourceType === "topic" ? `--topic '${r.name}'` : r.resourceType === "group" ? `--group '${r.name}'` : r.resourceType === "transactional-id" ? `--transactional-id '${r.name}'` : "--cluster";
4374
+ const pattern = r.patternType === "prefixed" ? " --resource-pattern-type prefixed" : "";
4375
+ return `kafka-acls.sh --bootstrap-server ${bootstrapServer} --add --allow-principal '${principal}' ${ops} ${resourceFlag}${pattern} # ${r.reason}`;
4376
+ });
4377
+ }
4378
+ var MSK_TOPIC_ACTIONS = {
4379
+ READ: ["kafka-cluster:ReadData", "kafka-cluster:DescribeTopic"],
4380
+ WRITE: ["kafka-cluster:WriteData", "kafka-cluster:DescribeTopic"],
4381
+ DESCRIBE: ["kafka-cluster:DescribeTopic"],
4382
+ CREATE: ["kafka-cluster:CreateTopic"],
4383
+ DELETE: ["kafka-cluster:DeleteTopic"]
4384
+ };
4385
+ var MSK_GROUP_ACTIONS = {
4386
+ READ: ["kafka-cluster:AlterGroup", "kafka-cluster:DescribeGroup"],
4387
+ DESCRIBE: ["kafka-cluster:DescribeGroup"],
4388
+ DELETE: ["kafka-cluster:DeleteGroup"]
4389
+ };
4390
+ var MSK_TX_ACTIONS = {
4391
+ WRITE: [
4392
+ "kafka-cluster:AlterTransactionalId",
4393
+ "kafka-cluster:DescribeTransactionalId"
4394
+ ],
4395
+ DESCRIBE: ["kafka-cluster:DescribeTransactionalId"]
4396
+ };
4397
+ function toMskIamPolicy(resources, cluster) {
4398
+ const { region, accountId, clusterName, clusterUuid } = cluster;
4399
+ const arn = (type, name) => `arn:aws:kafka:${region}:${accountId}:${type}/${clusterName}/${clusterUuid}/${name}`;
4400
+ const statements = [
4401
+ {
4402
+ Sid: "Connect",
4403
+ Effect: "Allow",
4404
+ Action: ["kafka-cluster:Connect"],
4405
+ Resource: [
4406
+ `arn:aws:kafka:${region}:${accountId}:cluster/${clusterName}/${clusterUuid}`
4407
+ ]
4408
+ }
4409
+ ];
4410
+ let sid = 0;
4411
+ for (const r of resources) {
4412
+ const suffix = r.patternType === "prefixed" ? `${r.name}*` : r.name;
4413
+ let actions = [];
4414
+ let resource;
4415
+ if (r.resourceType === "topic") {
4416
+ actions = [...new Set(r.operations.flatMap((o) => MSK_TOPIC_ACTIONS[o] ?? []))];
4417
+ resource = arn("topic", suffix);
4418
+ } else if (r.resourceType === "group") {
4419
+ actions = [...new Set(r.operations.flatMap((o) => MSK_GROUP_ACTIONS[o] ?? []))];
4420
+ resource = arn("group", suffix);
4421
+ } else if (r.resourceType === "transactional-id") {
4422
+ actions = [...new Set(r.operations.flatMap((o) => MSK_TX_ACTIONS[o] ?? []))];
4423
+ resource = arn("transactional-id", suffix);
4424
+ } else {
4425
+ actions = ["kafka-cluster:CreateTopic"];
4426
+ resource = `arn:aws:kafka:${region}:${accountId}:topic/${clusterName}/${clusterUuid}/*`;
4427
+ }
4428
+ if (actions.length === 0 || !resource) continue;
4429
+ statements.push({
4430
+ Sid: `Acl${sid++}`,
4431
+ Effect: "Allow",
4432
+ Action: actions,
4433
+ Resource: [resource]
4434
+ });
4435
+ }
4436
+ return { Version: "2012-10-17", Statement: statements };
4437
+ }
4438
+
4439
+ // src/client/config/from-env.ts
4440
+ var TRUE_VALUES = /* @__PURE__ */ new Set(["true", "1", "yes"]);
4441
+ var FALSE_VALUES = /* @__PURE__ */ new Set(["false", "0", "no"]);
4442
+ function parseBool(name, raw) {
4443
+ const normalized = raw.trim().toLowerCase();
4444
+ if (TRUE_VALUES.has(normalized)) return true;
4445
+ if (FALSE_VALUES.has(normalized)) return false;
4446
+ throw new Error(
4447
+ `Invalid boolean for ${name}: "${raw}". Use one of true/false, 1/0, yes/no (case-insensitive).`
4448
+ );
4449
+ }
4450
+ function parseNum(name, raw) {
4451
+ const value = Number(raw.trim());
4452
+ if (Number.isNaN(value)) {
4453
+ throw new Error(`Invalid number for ${name}: "${raw}".`);
4454
+ }
4455
+ return value;
4456
+ }
4457
+ function parseList(raw) {
4458
+ return raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
4459
+ }
4460
+ function parseEnum(name, raw, allowed) {
4461
+ const value = raw.trim();
4462
+ if (!allowed.includes(value)) {
4463
+ throw new Error(
4464
+ `Invalid value for ${name}: "${raw}". Allowed: ${allowed.join(", ")}.`
4465
+ );
4466
+ }
4467
+ return value;
4468
+ }
4469
+ function readVar(env, key, apply) {
4470
+ const raw = env[key];
4471
+ if (raw === void 0 || raw.trim() === "") return;
4472
+ apply(raw);
4473
+ }
4474
+ function kafkaClientConfigFromEnv(env = process.env, prefix = "KAFKA_") {
4475
+ const options = {};
4476
+ const result = { options };
4477
+ readVar(env, `${prefix}CLIENT_ID`, (raw) => {
4478
+ result.clientId = raw.trim();
4479
+ });
4480
+ readVar(env, `${prefix}GROUP_ID`, (raw) => {
4481
+ result.groupId = raw.trim();
4482
+ });
4483
+ readVar(env, `${prefix}BROKERS`, (raw) => {
4484
+ result.brokers = parseList(raw);
4485
+ });
4486
+ readVar(env, `${prefix}AUTO_CREATE_TOPICS`, (raw) => {
4487
+ options.autoCreateTopics = parseBool(`${prefix}AUTO_CREATE_TOPICS`, raw);
4488
+ });
4489
+ readVar(env, `${prefix}STRICT_SCHEMAS`, (raw) => {
4490
+ options.strictSchemas = parseBool(`${prefix}STRICT_SCHEMAS`, raw);
4491
+ });
4492
+ readVar(env, `${prefix}NUM_PARTITIONS`, (raw) => {
4493
+ options.numPartitions = parseNum(`${prefix}NUM_PARTITIONS`, raw);
4494
+ });
4495
+ readVar(env, `${prefix}TRANSACTIONAL_ID`, (raw) => {
4496
+ options.transactionalId = raw.trim();
4497
+ });
4498
+ readVar(env, `${prefix}CLOCK_RECOVERY_TOPICS`, (raw) => {
4499
+ const topics = parseList(raw);
4500
+ if (topics.length === 0) return;
4501
+ options.clockRecovery = { topics };
4502
+ });
4503
+ readVar(env, `${prefix}CLOCK_RECOVERY_TIMEOUT_MS`, (raw) => {
4504
+ const timeoutMs = parseNum(`${prefix}CLOCK_RECOVERY_TIMEOUT_MS`, raw);
4505
+ if (options.clockRecovery) {
4506
+ options.clockRecovery.timeoutMs = timeoutMs;
4507
+ }
4508
+ });
4509
+ readVar(env, `${prefix}LAG_THROTTLE_MAX_LAG`, (raw) => {
4510
+ options.lagThrottle = {
4511
+ maxLag: parseNum(`${prefix}LAG_THROTTLE_MAX_LAG`, raw)
4512
+ };
4513
+ });
4514
+ readVar(env, `${prefix}LAG_THROTTLE_GROUP_ID`, (raw) => {
4515
+ if (options.lagThrottle) options.lagThrottle.groupId = raw.trim();
4516
+ });
4517
+ readVar(env, `${prefix}LAG_THROTTLE_POLL_INTERVAL_MS`, (raw) => {
4518
+ if (options.lagThrottle) {
4519
+ options.lagThrottle.pollIntervalMs = parseNum(
4520
+ `${prefix}LAG_THROTTLE_POLL_INTERVAL_MS`,
4521
+ raw
4522
+ );
4523
+ }
4524
+ });
4525
+ readVar(env, `${prefix}LAG_THROTTLE_MAX_WAIT_MS`, (raw) => {
4526
+ if (options.lagThrottle) {
4527
+ options.lagThrottle.maxWaitMs = parseNum(
4528
+ `${prefix}LAG_THROTTLE_MAX_WAIT_MS`,
4529
+ raw
4530
+ );
4531
+ }
4532
+ });
4533
+ const security = securityFromEnv(env, prefix);
4534
+ if (security) options.security = security;
4535
+ return result;
4536
+ }
4537
+ function securityFromEnv(env, prefix) {
4538
+ let ssl;
4539
+ let allowInsecure;
4540
+ let mechanism;
4541
+ let username;
4542
+ let password;
4543
+ readVar(env, `${prefix}SSL`, (raw) => {
4544
+ ssl = parseBool(`${prefix}SSL`, raw);
4545
+ });
4546
+ readVar(env, `${prefix}ALLOW_INSECURE`, (raw) => {
4547
+ allowInsecure = parseBool(`${prefix}ALLOW_INSECURE`, raw);
4548
+ });
4549
+ readVar(env, `${prefix}SASL_MECHANISM`, (raw) => {
4550
+ mechanism = parseEnum(`${prefix}SASL_MECHANISM`, raw, [
4551
+ "plain",
4552
+ "scram-sha-256",
4553
+ "scram-sha-512"
4554
+ ]);
4555
+ });
4556
+ readVar(env, `${prefix}SASL_USERNAME`, (raw) => {
4557
+ username = raw.trim();
4558
+ });
4559
+ readVar(env, `${prefix}SASL_PASSWORD`, (raw) => {
4560
+ password = raw;
4561
+ });
4562
+ if (ssl === void 0 && allowInsecure === void 0 && mechanism === void 0 && username === void 0 && password === void 0) {
4563
+ return void 0;
4564
+ }
4565
+ const security = {};
4566
+ if (ssl !== void 0) security.ssl = ssl;
4567
+ if (allowInsecure !== void 0) security.allowInsecure = allowInsecure;
4568
+ if (mechanism !== void 0 || username !== void 0 || password !== void 0) {
4569
+ if (mechanism === void 0 || username === void 0 || password === void 0) {
4570
+ throw new Error(
4571
+ `Incomplete SASL configuration: ${prefix}SASL_MECHANISM, ${prefix}SASL_USERNAME, and ${prefix}SASL_PASSWORD must all be set together (oauthbearer must be configured in code).`
4572
+ );
4573
+ }
4574
+ const sasl = { mechanism, username, password };
4575
+ security.sasl = sasl;
4576
+ }
4577
+ return security;
4578
+ }
4579
+ function consumerOptionsFromEnv(env = process.env, prefix = "KAFKA_CONSUMER_") {
4580
+ const options = {};
4581
+ readVar(env, `${prefix}GROUP_ID`, (raw) => {
4582
+ options.groupId = raw.trim();
4583
+ });
4584
+ readVar(env, `${prefix}FROM_BEGINNING`, (raw) => {
4585
+ options.fromBeginning = parseBool(`${prefix}FROM_BEGINNING`, raw);
4586
+ });
4587
+ readVar(env, `${prefix}AUTO_COMMIT`, (raw) => {
4588
+ options.autoCommit = parseBool(`${prefix}AUTO_COMMIT`, raw);
4589
+ });
4590
+ readVar(env, `${prefix}DLQ`, (raw) => {
4591
+ options.dlq = parseBool(`${prefix}DLQ`, raw);
4592
+ });
4593
+ readVar(env, `${prefix}RETRY_MAX_RETRIES`, (raw) => {
4594
+ const retry = {
4595
+ maxRetries: parseNum(`${prefix}RETRY_MAX_RETRIES`, raw)
4596
+ };
4597
+ options.retry = retry;
4598
+ });
4599
+ readVar(env, `${prefix}RETRY_BACKOFF_MS`, (raw) => {
4600
+ if (options.retry) {
4601
+ options.retry.backoffMs = parseNum(`${prefix}RETRY_BACKOFF_MS`, raw);
4602
+ }
4603
+ });
4604
+ readVar(env, `${prefix}RETRY_MAX_BACKOFF_MS`, (raw) => {
4605
+ if (options.retry) {
4606
+ options.retry.maxBackoffMs = parseNum(`${prefix}RETRY_MAX_BACKOFF_MS`, raw);
4607
+ }
4608
+ });
4609
+ readVar(env, `${prefix}RETRY_TOPICS`, (raw) => {
4610
+ options.retryTopics = parseBool(`${prefix}RETRY_TOPICS`, raw);
4611
+ });
4612
+ readVar(env, `${prefix}RETRY_TOPIC_ASSIGNMENT_TIMEOUT_MS`, (raw) => {
4613
+ options.retryTopicAssignmentTimeoutMs = parseNum(
4614
+ `${prefix}RETRY_TOPIC_ASSIGNMENT_TIMEOUT_MS`,
4615
+ raw
4616
+ );
4617
+ });
4618
+ readVar(env, `${prefix}HANDLER_TIMEOUT_MS`, (raw) => {
4619
+ options.handlerTimeoutMs = parseNum(`${prefix}HANDLER_TIMEOUT_MS`, raw);
4620
+ });
4621
+ readVar(env, `${prefix}MESSAGE_TTL_MS`, (raw) => {
4622
+ options.messageTtlMs = parseNum(`${prefix}MESSAGE_TTL_MS`, raw);
4623
+ });
4624
+ readVar(env, `${prefix}DEDUPLICATION_STRATEGY`, (raw) => {
4625
+ const strategy = parseEnum(`${prefix}DEDUPLICATION_STRATEGY`, raw, [
4626
+ "drop",
4627
+ "dlq",
4628
+ "topic"
4629
+ ]);
4630
+ const dedup = { strategy };
4631
+ options.deduplication = dedup;
4632
+ });
4633
+ readVar(env, `${prefix}DEDUPLICATION_TOPIC`, (raw) => {
4634
+ if (options.deduplication) {
4635
+ options.deduplication.duplicatesTopic = raw.trim();
4636
+ }
4637
+ });
4638
+ readVar(env, `${prefix}CIRCUIT_BREAKER_THRESHOLD`, (raw) => {
4639
+ const cb = {
4640
+ threshold: parseNum(`${prefix}CIRCUIT_BREAKER_THRESHOLD`, raw)
4641
+ };
4642
+ options.circuitBreaker = cb;
4643
+ });
4644
+ readVar(env, `${prefix}CIRCUIT_BREAKER_RECOVERY_MS`, (raw) => {
4645
+ if (options.circuitBreaker) {
4646
+ options.circuitBreaker.recoveryMs = parseNum(
4647
+ `${prefix}CIRCUIT_BREAKER_RECOVERY_MS`,
4648
+ raw
4649
+ );
4650
+ }
4651
+ });
4652
+ readVar(env, `${prefix}CIRCUIT_BREAKER_WINDOW_SIZE`, (raw) => {
4653
+ if (options.circuitBreaker) {
4654
+ options.circuitBreaker.windowSize = parseNum(
4655
+ `${prefix}CIRCUIT_BREAKER_WINDOW_SIZE`,
4656
+ raw
4657
+ );
4658
+ }
4659
+ });
4660
+ readVar(env, `${prefix}CIRCUIT_BREAKER_HALF_OPEN_SUCCESSES`, (raw) => {
4661
+ if (options.circuitBreaker) {
4662
+ options.circuitBreaker.halfOpenSuccesses = parseNum(
4663
+ `${prefix}CIRCUIT_BREAKER_HALF_OPEN_SUCCESSES`,
4664
+ raw
4665
+ );
4666
+ }
4667
+ });
4668
+ readVar(env, `${prefix}QUEUE_HIGH_WATER_MARK`, (raw) => {
4669
+ options.queueHighWaterMark = parseNum(`${prefix}QUEUE_HIGH_WATER_MARK`, raw);
4670
+ });
4671
+ readVar(env, `${prefix}PARTITION_ASSIGNER`, (raw) => {
4672
+ options.partitionAssigner = parseEnum(`${prefix}PARTITION_ASSIGNER`, raw, [
4673
+ "roundrobin",
4674
+ "range",
4675
+ "cooperative-sticky"
4676
+ ]);
4677
+ });
4678
+ readVar(env, `${prefix}GROUP_INSTANCE_ID`, (raw) => {
4679
+ options.groupInstanceId = raw.trim();
4680
+ });
4681
+ readVar(env, `${prefix}SUBSCRIBE_RETRY_RETRIES`, (raw) => {
4682
+ const subscribeRetry = {
4683
+ retries: parseNum(`${prefix}SUBSCRIBE_RETRY_RETRIES`, raw)
4684
+ };
4685
+ options.subscribeRetry = subscribeRetry;
4686
+ });
4687
+ readVar(env, `${prefix}SUBSCRIBE_RETRY_DELAY_MS`, (raw) => {
4688
+ if (options.subscribeRetry) {
4689
+ options.subscribeRetry.backoffMs = parseNum(
4690
+ `${prefix}SUBSCRIBE_RETRY_DELAY_MS`,
4691
+ raw
4692
+ );
4693
+ }
4694
+ });
4695
+ return options;
4696
+ }
4697
+ var NESTED_CONSUMER_KEYS = [
4698
+ "retry",
4699
+ "deduplication",
4700
+ "circuitBreaker",
4701
+ "subscribeRetry"
4702
+ ];
4703
+ function isPlainObject(value) {
4704
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4705
+ }
4706
+ function mergeConsumerOptions(...layers) {
4707
+ const result = {};
4708
+ for (const layer of layers) {
4709
+ if (!layer) continue;
4710
+ for (const [key, value] of Object.entries(layer)) {
4711
+ if (value === void 0) continue;
4712
+ if (NESTED_CONSUMER_KEYS.includes(key) && isPlainObject(value) && isPlainObject(result[key])) {
4713
+ result[key] = {
4714
+ ...result[key],
4715
+ ...value
4716
+ };
4717
+ } else {
4718
+ result[key] = value;
4719
+ }
4720
+ }
4721
+ }
4722
+ return result;
4723
+ }
4724
+
4725
+ export {
4726
+ ConfluentTransport,
4727
+ InMemoryDedupStore,
4728
+ HEADER_EVENT_ID,
4729
+ HEADER_CORRELATION_ID,
4730
+ HEADER_TIMESTAMP,
4731
+ HEADER_SCHEMA_VERSION,
4732
+ HEADER_TRACEPARENT,
4733
+ HEADER_LAMPORT_CLOCK,
4734
+ HEADER_DELAYED_UNTIL,
4735
+ HEADER_DELAYED_TARGET,
4736
+ getEnvelopeContext,
4737
+ runWithEnvelopeContext,
4738
+ buildEnvelopeHeaders,
4739
+ decodeHeaders,
4740
+ extractEnvelope,
4741
+ toError,
4742
+ KafkaProcessingError,
4743
+ KafkaValidationError,
4744
+ KafkaRetryExhaustedError,
4745
+ resolveSecurityOptions,
4746
+ KafkaClient,
4747
+ topic,
4748
+ versionedSchema,
4749
+ InMemoryOutboxStore,
4750
+ startOutboxRelay,
4751
+ awsMskIamProvider,
4752
+ gcpAccessTokenProvider,
4753
+ describeRequiredAcls,
4754
+ toKafkaAclCommands,
4755
+ toMskIamPolicy,
4756
+ kafkaClientConfigFromEnv,
4757
+ consumerOptionsFromEnv,
4758
+ mergeConsumerOptions
4759
+ };
4760
+ //# sourceMappingURL=chunk-OR7TPAAE.mjs.map