@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.
- package/README.md +70 -2
- package/dist/{chunk-CMO7SMVK.mjs → chunk-OR7TPAAE.mjs} +110 -164
- package/dist/chunk-OR7TPAAE.mjs.map +1 -0
- package/dist/chunk-PQVBRDNV.mjs +149 -0
- package/dist/chunk-PQVBRDNV.mjs.map +1 -0
- package/dist/cli/index.js +115 -51
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2 -1
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/kafka.client/consumer/features/delayed.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts +1 -1
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/features/snapshot.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/handler.d.ts +16 -2
- package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/ops.d.ts +13 -0
- package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/pipeline.d.ts +14 -13
- package/dist/client/kafka.client/consumer/pipeline.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/retry-topic.d.ts +4 -1
- package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/setup.d.ts +3 -0
- package/dist/client/kafka.client/consumer/setup.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/start.d.ts.map +1 -1
- package/dist/client/kafka.client/context.d.ts +3 -0
- package/dist/client/kafka.client/context.d.ts.map +1 -1
- package/dist/client/kafka.client/index.d.ts.map +1 -1
- package/dist/client/kafka.client/producer/ops.d.ts +12 -3
- package/dist/client/kafka.client/producer/ops.d.ts.map +1 -1
- package/dist/client/kafka.client/producer/send.d.ts +1 -1
- package/dist/client/message/schema-registry.d.ts +23 -4
- package/dist/client/message/schema-registry.d.ts.map +1 -1
- package/dist/client/message/serde.d.ts +68 -0
- package/dist/client/message/serde.d.ts.map +1 -0
- package/dist/client/message/topic.d.ts +25 -4
- package/dist/client/message/topic.d.ts.map +1 -1
- package/dist/client/transport/transport.interface.d.ts +6 -1
- package/dist/client/transport/transport.interface.d.ts.map +1 -1
- package/dist/client/types/config.types.d.ts +17 -0
- package/dist/client/types/config.types.d.ts.map +1 -1
- package/dist/core.d.ts +3 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +146 -55
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +9 -3
- package/dist/index.js +146 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -3
- package/dist/index.mjs.map +1 -1
- package/dist/serde.d.ts +157 -0
- package/dist/serde.d.ts.map +1 -0
- package/dist/serde.js +308 -0
- package/dist/serde.js.map +1 -0
- package/dist/serde.mjs +158 -0
- package/dist/serde.mjs.map +1 -0
- package/package.json +20 -1
- 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:
|
|
454
|
-
|
|
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
|
|
1229
|
-
const shouldProcess = !options.filter || options.filter(headers,
|
|
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
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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: [
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|