@drarzter/kafka-client 0.10.0 → 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 (57) hide show
  1. package/README.md +70 -2
  2. package/dist/{chunk-CMO7SMVK.mjs → chunk-OR7TPAAE.mjs} +110 -164
  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/index.js +115 -51
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/index.mjs +2 -1
  9. package/dist/cli/index.mjs.map +1 -1
  10. package/dist/client/kafka.client/consumer/features/delayed.d.ts.map +1 -1
  11. package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts +1 -1
  12. package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts.map +1 -1
  13. package/dist/client/kafka.client/consumer/features/snapshot.d.ts.map +1 -1
  14. package/dist/client/kafka.client/consumer/handler.d.ts +16 -2
  15. package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -1
  16. package/dist/client/kafka.client/consumer/ops.d.ts +13 -0
  17. package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -1
  18. package/dist/client/kafka.client/consumer/pipeline.d.ts +14 -13
  19. package/dist/client/kafka.client/consumer/pipeline.d.ts.map +1 -1
  20. package/dist/client/kafka.client/consumer/retry-topic.d.ts +4 -1
  21. package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -1
  22. package/dist/client/kafka.client/consumer/setup.d.ts +3 -0
  23. package/dist/client/kafka.client/consumer/setup.d.ts.map +1 -1
  24. package/dist/client/kafka.client/consumer/start.d.ts.map +1 -1
  25. package/dist/client/kafka.client/context.d.ts +3 -0
  26. package/dist/client/kafka.client/context.d.ts.map +1 -1
  27. package/dist/client/kafka.client/index.d.ts.map +1 -1
  28. package/dist/client/kafka.client/producer/ops.d.ts +12 -3
  29. package/dist/client/kafka.client/producer/ops.d.ts.map +1 -1
  30. package/dist/client/kafka.client/producer/send.d.ts +1 -1
  31. package/dist/client/message/schema-registry.d.ts +23 -4
  32. package/dist/client/message/schema-registry.d.ts.map +1 -1
  33. package/dist/client/message/serde.d.ts +68 -0
  34. package/dist/client/message/serde.d.ts.map +1 -0
  35. package/dist/client/message/topic.d.ts +25 -4
  36. package/dist/client/message/topic.d.ts.map +1 -1
  37. package/dist/client/transport/transport.interface.d.ts +6 -1
  38. package/dist/client/transport/transport.interface.d.ts.map +1 -1
  39. package/dist/client/types/config.types.d.ts +17 -0
  40. package/dist/client/types/config.types.d.ts.map +1 -1
  41. package/dist/core.d.ts +3 -0
  42. package/dist/core.d.ts.map +1 -1
  43. package/dist/core.js +146 -55
  44. package/dist/core.js.map +1 -1
  45. package/dist/core.mjs +9 -3
  46. package/dist/index.js +146 -55
  47. package/dist/index.js.map +1 -1
  48. package/dist/index.mjs +9 -3
  49. package/dist/index.mjs.map +1 -1
  50. package/dist/serde.d.ts +157 -0
  51. package/dist/serde.d.ts.map +1 -0
  52. package/dist/serde.js +308 -0
  53. package/dist/serde.js.map +1 -0
  54. package/dist/serde.mjs +158 -0
  55. package/dist/serde.mjs.map +1 -0
  56. package/package.json +20 -1
  57. package/dist/chunk-CMO7SMVK.mjs.map +0 -1
package/dist/core.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/core.ts
21
21
  var core_exports = {};
22
22
  __export(core_exports, {
23
+ ConfluentTransport: () => ConfluentTransport,
23
24
  HEADER_CORRELATION_ID: () => HEADER_CORRELATION_ID,
24
25
  HEADER_DELAYED_TARGET: () => HEADER_DELAYED_TARGET,
25
26
  HEADER_DELAYED_UNTIL: () => HEADER_DELAYED_UNTIL,
@@ -30,6 +31,7 @@ __export(core_exports, {
30
31
  HEADER_TRACEPARENT: () => HEADER_TRACEPARENT,
31
32
  InMemoryDedupStore: () => InMemoryDedupStore,
32
33
  InMemoryOutboxStore: () => InMemoryOutboxStore,
34
+ JsonSerde: () => JsonSerde,
33
35
  KafkaClient: () => KafkaClient,
34
36
  KafkaProcessingError: () => KafkaProcessingError,
35
37
  KafkaRetryExhaustedError: () => KafkaRetryExhaustedError,
@@ -260,6 +262,18 @@ var ConfluentTransport = class {
260
262
  }
261
263
  };
262
264
 
265
+ // src/client/message/serde.ts
266
+ var JsonSerde = class {
267
+ /** JSON-stringify the validated payload. Returns a UTF-8 string. */
268
+ serialize(value) {
269
+ return JSON.stringify(value);
270
+ }
271
+ /** JSON-parse UTF-8 wire bytes into an object. */
272
+ deserialize(data) {
273
+ return JSON.parse(data.toString("utf8"));
274
+ }
275
+ };
276
+
263
277
  // src/client/kafka.client/infra/dedup.store.ts
264
278
  var InMemoryDedupStore = class {
265
279
  constructor(states) {
@@ -384,6 +398,9 @@ var KafkaRetryExhaustedError = class extends KafkaProcessingError {
384
398
  };
385
399
 
386
400
  // src/client/kafka.client/producer/ops.ts
401
+ function resolveSerde(topicOrDesc, clientSerde) {
402
+ return topicOrDesc?.__serde ?? clientSerde;
403
+ }
387
404
  function resolveTopicName(topicOrDescriptor) {
388
405
  if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
389
406
  if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
@@ -430,6 +447,7 @@ async function validateMessage(topicOrDesc, message, deps, ctx) {
430
447
  }
431
448
  async function buildSendPayload(topicOrDesc, messages, deps, compression) {
432
449
  const topic2 = resolveTopicName(topicOrDesc);
450
+ const serde = resolveSerde(topicOrDesc, deps.serde);
433
451
  const builtMessages = await Promise.all(
434
452
  messages.map(async (m) => {
435
453
  const envelopeHeaders = buildEnvelopeHeaders({
@@ -449,10 +467,13 @@ async function buildSendPayload(topicOrDesc, messages, deps, compression) {
449
467
  headers: envelopeHeaders,
450
468
  version: m.schemaVersion ?? 1
451
469
  };
470
+ const validated = await validateMessage(topicOrDesc, m.value, deps, sendCtx);
452
471
  return {
453
- value: JSON.stringify(
454
- await validateMessage(topicOrDesc, m.value, deps, sendCtx)
455
- ),
472
+ value: await serde.serialize(validated, {
473
+ topic: topic2,
474
+ headers: envelopeHeaders,
475
+ isKey: false
476
+ }),
456
477
  // Explicit key wins; otherwise fall back to the descriptor's .key()
457
478
  // extractor (runs on the original, pre-validation payload).
458
479
  key: m.key ?? topicOrDesc?.__key?.(m.value) ?? null,
@@ -537,6 +558,15 @@ function buildSchemaMap(topics, schemaRegistry, optionSchemas, logger) {
537
558
  }
538
559
  return schemaMap;
539
560
  }
561
+ function buildSerdeMap(topics) {
562
+ let serdeMap;
563
+ for (const t of topics) {
564
+ if (t?.__serde) {
565
+ (serdeMap ??= /* @__PURE__ */ new Map()).set(resolveTopicName(t), t.__serde);
566
+ }
567
+ }
568
+ return serdeMap;
569
+ }
540
570
 
541
571
  // src/client/kafka.client/admin/ops.ts
542
572
  var AdminOps = class {
@@ -822,17 +852,6 @@ var AdminOps = class {
822
852
  function sleep(ms) {
823
853
  return new Promise((resolve) => setTimeout(resolve, ms));
824
854
  }
825
- function parseJsonMessage(raw, topic2, logger) {
826
- try {
827
- return JSON.parse(raw);
828
- } catch (error) {
829
- logger.error(
830
- `Failed to parse message from topic ${topic2}:`,
831
- toError(error).stack
832
- );
833
- return null;
834
- }
835
- }
836
855
  async function validateWithSchema(message, raw, topic2, schemaMap, interceptors, dlq, deps) {
837
856
  const schema = schemaMap.get(topic2);
838
857
  if (!schema) return message;
@@ -1225,15 +1244,15 @@ async function replayDlqTopic(topic2, deps, options = {}) {
1225
1244
  const originalHeaders = Object.fromEntries(
1226
1245
  Object.entries(headers).filter(([k]) => !deps.dlqHeaderKeys.has(k))
1227
1246
  );
1228
- const value = message.value.toString();
1229
- const shouldProcess = !options.filter || options.filter(headers, value);
1247
+ const bytes = message.value;
1248
+ const shouldProcess = !options.filter || options.filter(headers, bytes.toString("utf8"));
1230
1249
  if (!targetTopic || !shouldProcess) {
1231
1250
  skipped++;
1232
1251
  } else if (options.dryRun) {
1233
1252
  deps.logger.log(`[DLQ replay dry-run] Would replay to "${targetTopic}"`);
1234
1253
  replayed++;
1235
1254
  } else {
1236
- await deps.send(targetTopic, [{ value, headers: originalHeaders }]);
1255
+ await deps.send(targetTopic, [{ value: bytes, headers: originalHeaders }]);
1237
1256
  replayed++;
1238
1257
  }
1239
1258
  const allDone = Array.from(highWatermarks.entries()).every(
@@ -2107,7 +2126,7 @@ async function waitForPartitionAssignment(consumer, topics, logger, timeoutMs =
2107
2126
  `Retry consumer did not receive partition assignments for [${topics.join(", ")}] within ${timeoutMs}ms`
2108
2127
  );
2109
2128
  }
2110
- async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopics, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs) {
2129
+ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopics, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
2111
2130
  const {
2112
2131
  logger,
2113
2132
  producer,
@@ -2153,20 +2172,35 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
2153
2172
  await sleep(remaining);
2154
2173
  consumer.resume([{ topic: levelTopic, partitions: [partition] }]);
2155
2174
  }
2156
- const raw = message.value.toString();
2157
- const parsed = parseJsonMessage(raw, levelTopic, logger);
2158
- if (parsed === null) {
2159
- await consumer.commitOffsets([nextOffset]);
2160
- return;
2161
- }
2175
+ const rawBytes = message.value;
2162
2176
  const currentMaxRetries = parseInt(
2163
2177
  headers[RETRY_HEADER_MAX_RETRIES] ?? String(retry.maxRetries),
2164
2178
  10
2165
2179
  );
2166
2180
  const originalTopic = headers[RETRY_HEADER_ORIGINAL_TOPIC] ?? levelTopic.replace(/\.retry\.\d+$/, "");
2181
+ const serde = serdeMap?.get(originalTopic) ?? deps.serde;
2182
+ let parsed;
2183
+ try {
2184
+ parsed = await serde.deserialize(rawBytes, {
2185
+ topic: originalTopic,
2186
+ headers,
2187
+ isKey: false
2188
+ });
2189
+ } catch (err) {
2190
+ logger.error(
2191
+ `Failed to deserialize retry message from topic ${levelTopic}:`,
2192
+ toError(err).stack
2193
+ );
2194
+ await consumer.commitOffsets([nextOffset]);
2195
+ return;
2196
+ }
2197
+ if (parsed === null) {
2198
+ await consumer.commitOffsets([nextOffset]);
2199
+ return;
2200
+ }
2167
2201
  const validated = await validateWithSchema(
2168
2202
  parsed,
2169
- raw,
2203
+ rawBytes,
2170
2204
  originalTopic,
2171
2205
  schemaMap,
2172
2206
  interceptors,
@@ -2220,7 +2254,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
2220
2254
  const delay = Math.floor(Math.random() * cap);
2221
2255
  const { topic: rtTopic, messages: rtMsgs } = buildRetryTopicPayload(
2222
2256
  originalTopic,
2223
- [raw],
2257
+ [rawBytes],
2224
2258
  nextLevel,
2225
2259
  currentMaxRetries,
2226
2260
  delay,
@@ -2262,7 +2296,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
2262
2296
  } else if (dlq) {
2263
2297
  const { topic: dTopic, messages: dMsgs } = buildDlqPayload(
2264
2298
  originalTopic,
2265
- raw,
2299
+ rawBytes,
2266
2300
  {
2267
2301
  error,
2268
2302
  // +1 to account for the main consumer's initial attempt before routing.
@@ -2324,7 +2358,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
2324
2358
  `Retry level ${level}/${retry.maxRetries} consumer started for: ${originalTopics.join(", ")} (group: ${levelGroupId})`
2325
2359
  );
2326
2360
  }
2327
- async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs) {
2361
+ async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
2328
2362
  const levelGroupIds = new Array(retry.maxRetries);
2329
2363
  await Promise.all(
2330
2364
  Array.from({ length: retry.maxRetries }, async (_, i) => {
@@ -2342,7 +2376,8 @@ async function startRetryTopicConsumers(originalTopics, originalGroupId, handleM
2342
2376
  interceptors,
2343
2377
  schemaMap,
2344
2378
  deps,
2345
- assignmentTimeoutMs
2379
+ assignmentTimeoutMs,
2380
+ serdeMap
2346
2381
  );
2347
2382
  levelGroupIds[i] = levelGroupId;
2348
2383
  })
@@ -2426,6 +2461,7 @@ async function setupConsumer(ctx, topics, mode, options) {
2426
2461
  optionSchemas,
2427
2462
  ctx.logger
2428
2463
  );
2464
+ const serdeMap = buildSerdeMap(stringTopics);
2429
2465
  const topicNames = stringTopics.map((t) => resolveTopicName(t));
2430
2466
  const subscribeTopics = [...topicNames, ...regexTopics];
2431
2467
  await ensureConsumerTopics(ctx, topicNames, dlq, options.deduplication);
@@ -2443,7 +2479,7 @@ async function setupConsumer(ctx, topics, mode, options) {
2443
2479
  ctx.logger.log(
2444
2480
  `${mode === "eachBatch" ? "Batch consumer" : "Consumer"} subscribed to topics: ${displayTopics}`
2445
2481
  );
2446
- return { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, hasRegex, readyPromise };
2482
+ return { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, hasRegex, readyPromise };
2447
2483
  }
2448
2484
  function resolveDeduplicationContext(ctx, groupId, options) {
2449
2485
  if (!options) return void 0;
@@ -2455,6 +2491,7 @@ function messageDepsFor(ctx, gid, options) {
2455
2491
  return {
2456
2492
  logger: ctx.logger,
2457
2493
  producer: ctx.producer,
2494
+ serde: ctx.serde,
2458
2495
  instrumentation: ctx.instrumentation,
2459
2496
  onMessageLost: options?.onMessageLost ?? ctx.onMessageLost,
2460
2497
  onTtlExpired: ctx.onTtlExpired,
@@ -2472,6 +2509,7 @@ function buildRetryTopicDeps(ctx) {
2472
2509
  return {
2473
2510
  logger: ctx.logger,
2474
2511
  producer: ctx.producer,
2512
+ serde: ctx.serde,
2475
2513
  instrumentation: ctx.instrumentation,
2476
2514
  onMessageLost: ctx.onMessageLost,
2477
2515
  onRetry: ctx.metrics.notifyRetry.bind(ctx.metrics),
@@ -2489,7 +2527,7 @@ async function makeEosMainContext(ctx, gid, consumer, options) {
2489
2527
  return { txProducer, consumer };
2490
2528
  }
2491
2529
  async function launchRetryChain(ctx, gid, topicNames, handleMessage, opts) {
2492
- const { retry, dlq, interceptors, schemaMap, assignmentTimeoutMs } = opts;
2530
+ const { retry, dlq, interceptors, schemaMap, serdeMap, assignmentTimeoutMs } = opts;
2493
2531
  if (!ctx.autoCreateTopicsEnabled) {
2494
2532
  await ctx.adminOps.validateRetryTopicsExist(topicNames, retry.maxRetries);
2495
2533
  }
@@ -2513,7 +2551,8 @@ async function launchRetryChain(ctx, gid, topicNames, handleMessage, opts) {
2513
2551
  ctx.companionGroupIds.get(gid).push(levelGroupId);
2514
2552
  }
2515
2553
  },
2516
- assignmentTimeoutMs
2554
+ assignmentTimeoutMs,
2555
+ serdeMap
2517
2556
  );
2518
2557
  }
2519
2558
 
@@ -2571,18 +2610,29 @@ async function applyDeduplication(envelope, raw, dedup, dlq, deps) {
2571
2610
  }
2572
2611
  return false;
2573
2612
  }
2574
- async function parseSingleMessage(message, topic2, partition, schemaMap, interceptors, dlq, deps) {
2613
+ async function parseSingleMessage(message, topic2, partition, schemaMap, interceptors, dlq, deps, serdeMap) {
2575
2614
  if (!message.value) {
2576
2615
  deps.logger.warn(`Received empty message from topic ${topic2}`);
2577
2616
  return null;
2578
2617
  }
2579
- const raw = message.value.toString();
2580
- const parsed = parseJsonMessage(raw, topic2, deps.logger);
2581
- if (parsed === null) return null;
2618
+ const bytes = message.value;
2582
2619
  const headers = decodeHeaders(message.headers);
2620
+ const serde = serdeMap?.get(topic2) ?? deps.serde;
2621
+ let parsed;
2622
+ try {
2623
+ parsed = await serde.deserialize(bytes, { topic: topic2, headers, isKey: false });
2624
+ } catch (error) {
2625
+ deps.logger.error(
2626
+ `Failed to deserialize message from topic ${topic2}:`,
2627
+ toError(error).stack
2628
+ );
2629
+ return null;
2630
+ }
2631
+ if (parsed === null) return null;
2583
2632
  const validated = await validateWithSchema(
2584
2633
  parsed,
2585
- raw,
2634
+ // Forward the ORIGINAL bytes to DLQ on validation failure (binary-safe).
2635
+ bytes,
2586
2636
  topic2,
2587
2637
  schemaMap,
2588
2638
  interceptors,
@@ -2596,6 +2646,7 @@ async function handleEachMessage(payload, opts, deps) {
2596
2646
  const { topic: topic2, partition, message } = payload;
2597
2647
  const {
2598
2648
  schemaMap,
2649
+ serdeMap,
2599
2650
  handleMessage,
2600
2651
  interceptors,
2601
2652
  dlq,
@@ -2604,6 +2655,7 @@ async function handleEachMessage(payload, opts, deps) {
2604
2655
  timeoutMs,
2605
2656
  wrapWithTimeout
2606
2657
  } = opts;
2658
+ const rawBytes = message.value;
2607
2659
  const eos = opts.eosMainContext;
2608
2660
  const nextOffsetStr = (parseInt(message.offset, 10) + 1).toString();
2609
2661
  const commitOffset = eos ? async () => {
@@ -2648,7 +2700,8 @@ async function handleEachMessage(payload, opts, deps) {
2648
2700
  schemaMap,
2649
2701
  interceptors,
2650
2702
  dlq,
2651
- deps
2703
+ deps,
2704
+ serdeMap
2652
2705
  );
2653
2706
  if (envelope === null) {
2654
2707
  await commitOffset?.();
@@ -2657,7 +2710,7 @@ async function handleEachMessage(payload, opts, deps) {
2657
2710
  if (opts.deduplication) {
2658
2711
  const isDuplicate = await applyDeduplication(
2659
2712
  envelope,
2660
- message.value.toString(),
2713
+ rawBytes,
2661
2714
  opts.deduplication,
2662
2715
  dlq,
2663
2716
  deps
@@ -2674,7 +2727,7 @@ async function handleEachMessage(payload, opts, deps) {
2674
2727
  `[KafkaClient] TTL expired on ${topic2}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
2675
2728
  );
2676
2729
  if (dlq) {
2677
- await sendToDlq(topic2, message.value.toString(), deps, {
2730
+ await sendToDlq(topic2, rawBytes, deps, {
2678
2731
  error: new Error(`Message TTL expired: age ${ageMs}ms`),
2679
2732
  attempt: 0,
2680
2733
  originalHeaders: envelope.headers
@@ -2706,7 +2759,7 @@ async function handleEachMessage(payload, opts, deps) {
2706
2759
  },
2707
2760
  {
2708
2761
  envelope,
2709
- rawMessages: [message.value.toString()],
2762
+ rawMessages: [rawBytes],
2710
2763
  interceptors,
2711
2764
  dlq,
2712
2765
  retry,
@@ -2719,6 +2772,7 @@ async function handleEachBatch(payload, opts, deps) {
2719
2772
  const { batch, heartbeat, resolveOffset, commitOffsetsIfNecessary } = payload;
2720
2773
  const {
2721
2774
  schemaMap,
2775
+ serdeMap,
2722
2776
  handleBatch,
2723
2777
  interceptors,
2724
2778
  dlq,
@@ -2774,6 +2828,7 @@ async function handleEachBatch(payload, opts, deps) {
2774
2828
  const envelopes = [];
2775
2829
  const rawMessages = [];
2776
2830
  for (const message of batch.messages) {
2831
+ const rawBytes = message.value;
2777
2832
  const envelope = await parseSingleMessage(
2778
2833
  message,
2779
2834
  batch.topic,
@@ -2781,14 +2836,14 @@ async function handleEachBatch(payload, opts, deps) {
2781
2836
  schemaMap,
2782
2837
  interceptors,
2783
2838
  dlq,
2784
- deps
2839
+ deps,
2840
+ serdeMap
2785
2841
  );
2786
2842
  if (envelope === null) continue;
2787
2843
  if (opts.deduplication) {
2788
- const raw = message.value.toString();
2789
2844
  const isDuplicate = await applyDeduplication(
2790
2845
  envelope,
2791
- raw,
2846
+ rawBytes,
2792
2847
  opts.deduplication,
2793
2848
  dlq,
2794
2849
  deps
@@ -2802,7 +2857,7 @@ async function handleEachBatch(payload, opts, deps) {
2802
2857
  `[KafkaClient] TTL expired on ${batch.topic}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
2803
2858
  );
2804
2859
  if (dlq) {
2805
- await sendToDlq(batch.topic, message.value.toString(), deps, {
2860
+ await sendToDlq(batch.topic, rawBytes, deps, {
2806
2861
  error: new Error(`Message TTL expired: age ${ageMs}ms`),
2807
2862
  attempt: 0,
2808
2863
  originalHeaders: envelope.headers
@@ -2821,7 +2876,7 @@ async function handleEachBatch(payload, opts, deps) {
2821
2876
  }
2822
2877
  }
2823
2878
  envelopes.push(envelope);
2824
- rawMessages.push(message.value.toString());
2879
+ rawMessages.push(rawBytes);
2825
2880
  }
2826
2881
  if (envelopes.length === 0) {
2827
2882
  await commitBatchOffset?.();
@@ -2984,7 +3039,7 @@ function resumeTopicAllPartitions(ctx, gid, topic2) {
2984
3039
  async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
2985
3040
  validateTopicConsumerOpts(topics, options);
2986
3041
  const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
2987
- const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachMessage", setupOptions);
3042
+ const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachMessage", setupOptions);
2988
3043
  if (options.circuitBreaker)
2989
3044
  ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
2990
3045
  const deps = messageDepsFor(ctx, gid, options);
@@ -2995,6 +3050,7 @@ async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
2995
3050
  payload,
2996
3051
  {
2997
3052
  schemaMap,
3053
+ serdeMap,
2998
3054
  handleMessage,
2999
3055
  interceptors,
3000
3056
  dlq,
@@ -3022,6 +3078,7 @@ async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
3022
3078
  dlq,
3023
3079
  interceptors,
3024
3080
  schemaMap,
3081
+ serdeMap,
3025
3082
  assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
3026
3083
  });
3027
3084
  }
@@ -3035,7 +3092,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
3035
3092
  );
3036
3093
  }
3037
3094
  const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
3038
- const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachBatch", setupOptions);
3095
+ const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachBatch", setupOptions);
3039
3096
  if (options.circuitBreaker)
3040
3097
  ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
3041
3098
  const deps = messageDepsFor(ctx, gid, options);
@@ -3046,6 +3103,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
3046
3103
  payload,
3047
3104
  {
3048
3105
  schemaMap,
3106
+ serdeMap,
3049
3107
  handleBatch,
3050
3108
  interceptors,
3051
3109
  dlq,
@@ -3083,6 +3141,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
3083
3141
  dlq,
3084
3142
  interceptors,
3085
3143
  schemaMap,
3144
+ serdeMap,
3086
3145
  assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
3087
3146
  });
3088
3147
  }
@@ -3095,7 +3154,7 @@ async function startTransactionalConsumerImpl(ctx, topics, handler, options = {}
3095
3154
  );
3096
3155
  }
3097
3156
  const setupOptions = { ...options, autoCommit: false };
3098
- const { consumer, schemaMap, gid, readyPromise } = await setupConsumer(
3157
+ const { consumer, schemaMap, serdeMap, gid, readyPromise } = await setupConsumer(
3099
3158
  ctx,
3100
3159
  topics,
3101
3160
  "eachMessage",
@@ -3112,7 +3171,8 @@ async function startTransactionalConsumerImpl(ctx, topics, handler, options = {}
3112
3171
  schemaMap,
3113
3172
  options.interceptors ?? [],
3114
3173
  false,
3115
- deps
3174
+ deps,
3175
+ serdeMap
3116
3176
  );
3117
3177
  const nextOffset = String(Number.parseInt(message.offset, 10) + 1);
3118
3178
  if (envelope === null) {
@@ -3330,7 +3390,9 @@ async function startDelayedRelayImpl(ctx, topics, options) {
3330
3390
  topic: target,
3331
3391
  messages: [
3332
3392
  {
3333
- value: message.value.toString(),
3393
+ // Forward the ORIGINAL wire bytes unchanged — no re-serialization,
3394
+ // so binary payloads (Avro/Protobuf) are relayed losslessly.
3395
+ value: message.value,
3334
3396
  key: message.key ? message.key.toString() : null,
3335
3397
  headers: forwardHeaders
3336
3398
  }
@@ -3657,6 +3719,7 @@ var KafkaClient = class {
3657
3719
  const consumers = /* @__PURE__ */ new Map();
3658
3720
  const consumerCreationOptions = /* @__PURE__ */ new Map();
3659
3721
  const schemaRegistry = /* @__PURE__ */ new Map();
3722
+ const serde = options?.serde ?? new JsonSerde();
3660
3723
  const adminOps = new AdminOps({
3661
3724
  admin: transport.admin(),
3662
3725
  logger,
@@ -3683,6 +3746,7 @@ var KafkaClient = class {
3683
3746
  autoCreateTopicsEnabled: options?.autoCreateTopics ?? false,
3684
3747
  strictSchemasEnabled: options?.strictSchemas ?? true,
3685
3748
  numPartitions: options?.numPartitions ?? 1,
3749
+ serde,
3686
3750
  txId: options?.transactionalId ?? `${clientId}-tx`,
3687
3751
  clockRecoveryTopics: options?.clockRecovery?.topics ?? [],
3688
3752
  clockRecoveryTimeoutMs: options?.clockRecovery?.timeoutMs ?? 3e4,
@@ -3717,6 +3781,7 @@ var KafkaClient = class {
3717
3781
  strictSchemasEnabled: options?.strictSchemas ?? true,
3718
3782
  instrumentation: options?.instrumentation ?? [],
3719
3783
  logger,
3784
+ serde,
3720
3785
  nextLamportClock: () => 0
3721
3786
  // patched below
3722
3787
  },
@@ -3735,6 +3800,7 @@ var KafkaClient = class {
3735
3800
  strictSchemasEnabled: options?.strictSchemas ?? true,
3736
3801
  instrumentation: options?.instrumentation ?? [],
3737
3802
  logger,
3803
+ serde,
3738
3804
  nextLamportClock: () => ++ctx._lamportClock
3739
3805
  };
3740
3806
  ctx.retryTopicDeps = buildRetryTopicDeps(ctx);
@@ -4003,10 +4069,8 @@ function topic(name) {
4003
4069
  function keyable(desc) {
4004
4070
  return {
4005
4071
  ...desc,
4006
- key: (extractor) => ({
4007
- ...desc,
4008
- __key: extractor
4009
- })
4072
+ key: (extractor) => keyable({ ...desc, __key: extractor }),
4073
+ serde: (serde) => keyable({ ...desc, __serde: serde })
4010
4074
  };
4011
4075
  }
4012
4076
 
@@ -4051,6 +4115,12 @@ var SchemaRegistryClient = class {
4051
4115
  fetchFn;
4052
4116
  cacheTtlMs;
4053
4117
  latestCache = /* @__PURE__ */ new Map();
4118
+ /**
4119
+ * `id → schema` cache. Schema ids are immutable in a Confluent-compatible
4120
+ * registry (a given id always maps to the same schema string), so entries
4121
+ * are cached for the lifetime of the client with no TTL.
4122
+ */
4123
+ byIdCache = /* @__PURE__ */ new Map();
4054
4124
  headers() {
4055
4125
  const h = {
4056
4126
  "Content-Type": "application/vnd.schemaregistry.v1+json"
@@ -4092,6 +4162,25 @@ var SchemaRegistryClient = class {
4092
4162
  });
4093
4163
  return value;
4094
4164
  }
4165
+ /**
4166
+ * Fetch a schema by its globally unique registry id (`GET /schemas/ids/{id}`).
4167
+ *
4168
+ * Used by the Avro/Protobuf serdes on the deserialize path: the writer schema
4169
+ * id is read from the Confluent wire-format prefix, then resolved here. Results
4170
+ * are cached forever (schema ids are immutable), so a given id triggers exactly
4171
+ * one registry round-trip regardless of how many messages reference it.
4172
+ */
4173
+ async getSchemaById(id) {
4174
+ const cached = this.byIdCache.get(id);
4175
+ if (cached) return cached;
4176
+ const raw = await this.request(
4177
+ "GET",
4178
+ `/schemas/ids/${id}`
4179
+ );
4180
+ const value = { id, schema: raw.schema, schemaType: raw.schemaType };
4181
+ this.byIdCache.set(id, value);
4182
+ return value;
4183
+ }
4095
4184
  /** Fetch a specific schema version of a subject. */
4096
4185
  async getSchemaVersion(subject, version) {
4097
4186
  const raw = await this.request(
@@ -4834,6 +4923,7 @@ function mergeConsumerOptions(...layers) {
4834
4923
  }
4835
4924
  // Annotate the CommonJS export names for ESM import in node:
4836
4925
  0 && (module.exports = {
4926
+ ConfluentTransport,
4837
4927
  HEADER_CORRELATION_ID,
4838
4928
  HEADER_DELAYED_TARGET,
4839
4929
  HEADER_DELAYED_UNTIL,
@@ -4844,6 +4934,7 @@ function mergeConsumerOptions(...layers) {
4844
4934
  HEADER_TRACEPARENT,
4845
4935
  InMemoryDedupStore,
4846
4936
  InMemoryOutboxStore,
4937
+ JsonSerde,
4847
4938
  KafkaClient,
4848
4939
  KafkaProcessingError,
4849
4940
  KafkaRetryExhaustedError,