@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
@@ -0,0 +1,149 @@
1
+ // src/client/message/serde.ts
2
+ var JsonSerde = class {
3
+ /** JSON-stringify the validated payload. Returns a UTF-8 string. */
4
+ serialize(value) {
5
+ return JSON.stringify(value);
6
+ }
7
+ /** JSON-parse UTF-8 wire bytes into an object. */
8
+ deserialize(data) {
9
+ return JSON.parse(data.toString("utf8"));
10
+ }
11
+ };
12
+
13
+ // src/client/message/schema-registry.ts
14
+ var SchemaRegistryClient = class {
15
+ constructor(options) {
16
+ this.options = options;
17
+ if (!options.baseUrl) {
18
+ throw new Error("SchemaRegistryClient: baseUrl is required");
19
+ }
20
+ this.fetchFn = options.fetchFn ?? fetch;
21
+ this.cacheTtlMs = options.cacheTtlMs ?? 3e5;
22
+ }
23
+ options;
24
+ fetchFn;
25
+ cacheTtlMs;
26
+ latestCache = /* @__PURE__ */ new Map();
27
+ /**
28
+ * `id → schema` cache. Schema ids are immutable in a Confluent-compatible
29
+ * registry (a given id always maps to the same schema string), so entries
30
+ * are cached for the lifetime of the client with no TTL.
31
+ */
32
+ byIdCache = /* @__PURE__ */ new Map();
33
+ headers() {
34
+ const h = {
35
+ "Content-Type": "application/vnd.schemaregistry.v1+json"
36
+ };
37
+ if (this.options.auth) {
38
+ const { username, password } = this.options.auth;
39
+ h["Authorization"] = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
40
+ }
41
+ return h;
42
+ }
43
+ async request(method, path, body) {
44
+ const url = `${this.options.baseUrl.replace(/\/$/, "")}${path}`;
45
+ const res = await this.fetchFn(url, {
46
+ method,
47
+ headers: this.headers(),
48
+ ...body !== void 0 && { body: JSON.stringify(body) }
49
+ });
50
+ if (!res.ok) {
51
+ const text = await res.text().catch(() => "");
52
+ throw new Error(
53
+ `SchemaRegistry ${method} ${path} failed: ${res.status} ${res.statusText}${text ? ` \u2014 ${text}` : ""}`
54
+ );
55
+ }
56
+ return await res.json();
57
+ }
58
+ /** Fetch the latest schema registered under `subject`. Cached for `cacheTtlMs`. */
59
+ async getLatestSchema(subject) {
60
+ const cached = this.latestCache.get(subject);
61
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
62
+ const raw = await this.request("GET", `/subjects/${encodeURIComponent(subject)}/versions/latest`);
63
+ const value = {
64
+ id: raw.id,
65
+ version: raw.version,
66
+ schema: raw.schema
67
+ };
68
+ this.latestCache.set(subject, {
69
+ value,
70
+ expiresAt: Date.now() + this.cacheTtlMs
71
+ });
72
+ return value;
73
+ }
74
+ /**
75
+ * Fetch a schema by its globally unique registry id (`GET /schemas/ids/{id}`).
76
+ *
77
+ * Used by the Avro/Protobuf serdes on the deserialize path: the writer schema
78
+ * id is read from the Confluent wire-format prefix, then resolved here. Results
79
+ * are cached forever (schema ids are immutable), so a given id triggers exactly
80
+ * one registry round-trip regardless of how many messages reference it.
81
+ */
82
+ async getSchemaById(id) {
83
+ const cached = this.byIdCache.get(id);
84
+ if (cached) return cached;
85
+ const raw = await this.request(
86
+ "GET",
87
+ `/schemas/ids/${id}`
88
+ );
89
+ const value = { id, schema: raw.schema, schemaType: raw.schemaType };
90
+ this.byIdCache.set(id, value);
91
+ return value;
92
+ }
93
+ /** Fetch a specific schema version of a subject. */
94
+ async getSchemaVersion(subject, version) {
95
+ const raw = await this.request(
96
+ "GET",
97
+ `/subjects/${encodeURIComponent(subject)}/versions/${version}`
98
+ );
99
+ return { id: raw.id, version: raw.version, schema: raw.schema };
100
+ }
101
+ /**
102
+ * Register a schema under `subject` (idempotent — re-registering the same
103
+ * schema returns the existing id). Returns the registry-assigned schema id.
104
+ */
105
+ async registerSchema(subject, schema, schemaType = "JSON") {
106
+ this.latestCache.delete(subject);
107
+ return this.request(
108
+ "POST",
109
+ `/subjects/${encodeURIComponent(subject)}/versions`,
110
+ { schema, schemaType }
111
+ );
112
+ }
113
+ /**
114
+ * Test `schema` against the subject's compatibility policy without registering.
115
+ * Returns `true` when the registry reports the schema as compatible.
116
+ */
117
+ async checkCompatibility(subject, schema, schemaType = "JSON") {
118
+ const res = await this.request(
119
+ "POST",
120
+ `/compatibility/subjects/${encodeURIComponent(subject)}/versions/latest`,
121
+ { schema, schemaType }
122
+ );
123
+ return res.is_compatible;
124
+ }
125
+ };
126
+ function registrySchema(client, subject, options) {
127
+ const enforceVersion = options?.enforceVersion ?? true;
128
+ return {
129
+ async parse(data, ctx) {
130
+ const latest = await client.getLatestSchema(subject);
131
+ if (enforceVersion && ctx?.version !== void 0 && ctx.version > latest.version) {
132
+ throw new Error(
133
+ `registrySchema: message version ${ctx.version} for subject "${subject}" is newer than the latest registered version ${latest.version} \u2014 register the schema before producing with it`
134
+ );
135
+ }
136
+ if (options?.validator) {
137
+ return options.validator.parse(data, ctx);
138
+ }
139
+ return data;
140
+ }
141
+ };
142
+ }
143
+
144
+ export {
145
+ JsonSerde,
146
+ SchemaRegistryClient,
147
+ registrySchema
148
+ };
149
+ //# sourceMappingURL=chunk-PQVBRDNV.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/message/serde.ts","../src/client/message/schema-registry.ts"],"sourcesContent":["import type { MessageHeaders } from \"../types\";\n\n/**\n * Context passed to `MessageSerde.serialize` / `deserialize`.\n *\n * Carries the topic name, decoded message headers, and which side of the\n * record is being (de)serialized. A Confluent Schema Registry serde uses\n * `topic` + `isKey` to derive the subject name (`<topic>-value` / `<topic>-key`)\n * and reads the schema id from the header/magic-byte prefix on `data`.\n */\nexport interface SerdeContext {\n /** Topic the message is produced to / consumed from. */\n topic: string;\n /** Decoded message headers (envelope headers included). */\n headers: MessageHeaders;\n /**\n * Which side of the Kafka record this call is (de)serializing.\n * `false` / omitted → the value (default); `true` → the key.\n * Used by schema-registry serdes to pick the `value` vs `key` subject.\n */\n isKey?: boolean;\n}\n\n/**\n * Pluggable serialization layer for message payloads.\n *\n * A `MessageSerde` converts a validated payload object to the wire form\n * (`Buffer` or `string`) on produce, and back to an object on consume.\n * The default is {@link JsonSerde}, which reproduces the client's historical\n * `JSON.stringify` / `JSON.parse` behaviour exactly.\n *\n * Serde only touches the message VALUE. Envelope metadata\n * (`x-event-id`, `x-correlation-id`, `x-lamport-clock`, `traceparent`, …)\n * always travels in headers and is never serialized through this layer.\n *\n * Set a client-wide serde via `KafkaClientOptions.serde`, or a per-topic\n * override via `topic(...).serde(mySerde)` — the per-topic serde wins for\n * that topic.\n *\n * @example\n * ```ts\n * const kafka = new KafkaClient(id, group, brokers, { serde: new JsonSerde() });\n * ```\n */\nexport interface MessageSerde {\n /**\n * Serialize a validated payload object to wire bytes (`Buffer`) or a\n * `string`. Validation has already run on `value` before this is called.\n */\n serialize(\n value: unknown,\n ctx: SerdeContext,\n ): Buffer | string | Promise<Buffer | string>;\n /**\n * Deserialize raw wire bytes into a payload object. Schema validation\n * (if any) runs on the returned object afterwards.\n */\n deserialize(data: Buffer, ctx: SerdeContext): unknown | Promise<unknown>;\n}\n\n/**\n * Default {@link MessageSerde}: JSON via `JSON.stringify` / `JSON.parse`.\n *\n * Byte-for-byte identical to the client's historical serialization, so it is\n * a zero-behaviour-change default. Produces a UTF-8 `string` on serialize and\n * decodes UTF-8 bytes on deserialize.\n */\nexport class JsonSerde implements MessageSerde {\n /** JSON-stringify the validated payload. Returns a UTF-8 string. */\n serialize(value: unknown): string {\n return JSON.stringify(value);\n }\n\n /** JSON-parse UTF-8 wire bytes into an object. */\n deserialize(data: Buffer): unknown {\n return JSON.parse(data.toString(\"utf8\"));\n }\n}\n","import type { SchemaLike, SchemaParseContext } from \"./topic\";\n\n/** A schema registered in a Confluent-compatible Schema Registry. */\nexport interface RegisteredSchema {\n /** Globally unique schema id assigned by the registry. */\n id: number;\n /** Version of the schema within its subject. */\n version: number;\n /** The schema definition string (JSON Schema / Avro / Protobuf source). */\n schema: string;\n}\n\n/** Options for `SchemaRegistryClient`. */\nexport interface SchemaRegistryClientOptions {\n /** Registry base URL, e.g. `http://localhost:8081` or a Confluent Cloud SR endpoint. */\n baseUrl: string;\n /** HTTP Basic credentials (Confluent Cloud SR API key/secret). */\n auth?: { username: string; password: string };\n /** Cache TTL for subject lookups in ms. Default: `300_000` (5 min). */\n cacheTtlMs?: number;\n /** Injectable fetch implementation (tests). Default: global `fetch`. */\n fetchFn?: typeof fetch;\n}\n\n/** Schema type accepted by Confluent-compatible registries. */\nexport type RegistrySchemaType = \"JSON\" | \"AVRO\" | \"PROTOBUF\";\n\n/**\n * Minimal, dependency-free client for the Confluent Schema Registry REST API\n * (works with Confluent Platform/Cloud, Redpanda, Karapace, AWS Glue SR proxy).\n *\n * Scope: subject/version management, compatibility checks, and id->schema\n * lookups. Used to keep locally-defined schemas in lockstep with a central\n * registry, and as the backing lookup for the Avro/Protobuf serdes in\n * `@drarzter/kafka-client/serde` (which handle the wire-format framing).\n *\n * @example\n * ```ts\n * const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });\n * const { id } = await registry.registerSchema(\n * 'order.created-value',\n * JSON.stringify(orderJsonSchema),\n * 'JSON',\n * );\n * ```\n */\nexport class SchemaRegistryClient {\n private readonly fetchFn: typeof fetch;\n private readonly cacheTtlMs: number;\n private readonly latestCache = new Map<\n string,\n { value: RegisteredSchema; expiresAt: number }\n >();\n /**\n * `id → schema` cache. Schema ids are immutable in a Confluent-compatible\n * registry (a given id always maps to the same schema string), so entries\n * are cached for the lifetime of the client with no TTL.\n */\n private readonly byIdCache = new Map<\n number,\n { id: number; schema: string; schemaType?: string }\n >();\n\n constructor(private readonly options: SchemaRegistryClientOptions) {\n if (!options.baseUrl) {\n throw new Error(\"SchemaRegistryClient: baseUrl is required\");\n }\n this.fetchFn = options.fetchFn ?? fetch;\n this.cacheTtlMs = options.cacheTtlMs ?? 300_000;\n }\n\n private headers(): Record<string, string> {\n const h: Record<string, string> = {\n \"Content-Type\": \"application/vnd.schemaregistry.v1+json\",\n };\n if (this.options.auth) {\n const { username, password } = this.options.auth;\n h[\"Authorization\"] =\n \"Basic \" + Buffer.from(`${username}:${password}`).toString(\"base64\");\n }\n return h;\n }\n\n private async request<R>(\n method: \"GET\" | \"POST\",\n path: string,\n body?: unknown,\n ): Promise<R> {\n const url = `${this.options.baseUrl.replace(/\\/$/, \"\")}${path}`;\n const res = await this.fetchFn(url, {\n method,\n headers: this.headers(),\n ...(body !== undefined && { body: JSON.stringify(body) }),\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\n `SchemaRegistry ${method} ${path} failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : \"\"}`,\n );\n }\n return (await res.json()) as R;\n }\n\n /** Fetch the latest schema registered under `subject`. Cached for `cacheTtlMs`. */\n async getLatestSchema(subject: string): Promise<RegisteredSchema> {\n const cached = this.latestCache.get(subject);\n if (cached && cached.expiresAt > Date.now()) return cached.value;\n const raw = await this.request<{\n id: number;\n version: number;\n schema: string;\n }>(\"GET\", `/subjects/${encodeURIComponent(subject)}/versions/latest`);\n const value: RegisteredSchema = {\n id: raw.id,\n version: raw.version,\n schema: raw.schema,\n };\n this.latestCache.set(subject, {\n value,\n expiresAt: Date.now() + this.cacheTtlMs,\n });\n return value;\n }\n\n /**\n * Fetch a schema by its globally unique registry id (`GET /schemas/ids/{id}`).\n *\n * Used by the Avro/Protobuf serdes on the deserialize path: the writer schema\n * id is read from the Confluent wire-format prefix, then resolved here. Results\n * are cached forever (schema ids are immutable), so a given id triggers exactly\n * one registry round-trip regardless of how many messages reference it.\n */\n async getSchemaById(\n id: number,\n ): Promise<{ id: number; schema: string; schemaType?: string }> {\n const cached = this.byIdCache.get(id);\n if (cached) return cached;\n const raw = await this.request<{ schema: string; schemaType?: string }>(\n \"GET\",\n `/schemas/ids/${id}`,\n );\n const value = { id, schema: raw.schema, schemaType: raw.schemaType };\n this.byIdCache.set(id, value);\n return value;\n }\n\n /** Fetch a specific schema version of a subject. */\n async getSchemaVersion(\n subject: string,\n version: number,\n ): Promise<RegisteredSchema> {\n const raw = await this.request<{\n id: number;\n version: number;\n schema: string;\n }>(\n \"GET\",\n `/subjects/${encodeURIComponent(subject)}/versions/${version}`,\n );\n return { id: raw.id, version: raw.version, schema: raw.schema };\n }\n\n /**\n * Register a schema under `subject` (idempotent — re-registering the same\n * schema returns the existing id). Returns the registry-assigned schema id.\n */\n async registerSchema(\n subject: string,\n schema: string,\n schemaType: RegistrySchemaType = \"JSON\",\n ): Promise<{ id: number }> {\n this.latestCache.delete(subject);\n return this.request<{ id: number }>(\n \"POST\",\n `/subjects/${encodeURIComponent(subject)}/versions`,\n { schema, schemaType },\n );\n }\n\n /**\n * Test `schema` against the subject's compatibility policy without registering.\n * Returns `true` when the registry reports the schema as compatible.\n */\n async checkCompatibility(\n subject: string,\n schema: string,\n schemaType: RegistrySchemaType = \"JSON\",\n ): Promise<boolean> {\n const res = await this.request<{ is_compatible: boolean }>(\n \"POST\",\n `/compatibility/subjects/${encodeURIComponent(subject)}/versions/latest`,\n { schema, schemaType },\n );\n return res.is_compatible;\n }\n}\n\n/** Options for `registrySchema()`. */\nexport interface RegistrySchemaOptions<T> {\n /**\n * Local structural validator (Zod/Valibot/…) applied to every message.\n * The registry governs schema *evolution*; this governs runtime *shape*.\n */\n validator?: SchemaLike<T>;\n /**\n * When `true` (default), the message's `x-schema-version` must not be newer\n * than the latest version registered for the subject — a producer publishing\n * an unregistered version fails loudly instead of drifting silently.\n */\n enforceVersion?: boolean;\n}\n\n/**\n * Bridge a Schema Registry subject to this library's `SchemaLike` seam.\n *\n * On each `parse` the adapter resolves the subject's latest registered version\n * (cached), optionally verifies the message's schema version does not exceed\n * it, and delegates structural validation to the provided local validator.\n * Attach the result to a `TopicDescriptor` like any other schema:\n *\n * @example\n * ```ts\n * const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });\n *\n * const OrderCreated = topic('order.created').schema(\n * registrySchema(registry, 'order.created-value', {\n * validator: z.object({ orderId: z.string() }),\n * }),\n * );\n * ```\n */\nexport function registrySchema<T = any>(\n client: SchemaRegistryClient,\n subject: string,\n options?: RegistrySchemaOptions<T>,\n): SchemaLike<T> {\n const enforceVersion = options?.enforceVersion ?? true;\n return {\n async parse(data: unknown, ctx?: SchemaParseContext): Promise<T> {\n const latest = await client.getLatestSchema(subject);\n if (enforceVersion && ctx?.version !== undefined && ctx.version > latest.version) {\n throw new Error(\n `registrySchema: message version ${ctx.version} for subject \"${subject}\" ` +\n `is newer than the latest registered version ${latest.version} — ` +\n `register the schema before producing with it`,\n );\n }\n if (options?.validator) {\n return options.validator.parse(data, ctx);\n }\n return data as T;\n },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/cli/index.js CHANGED
@@ -204,6 +204,18 @@ var ConfluentTransport = class {
204
204
  }
205
205
  };
206
206
 
207
+ // src/client/message/serde.ts
208
+ var JsonSerde = class {
209
+ /** JSON-stringify the validated payload. Returns a UTF-8 string. */
210
+ serialize(value) {
211
+ return JSON.stringify(value);
212
+ }
213
+ /** JSON-parse UTF-8 wire bytes into an object. */
214
+ deserialize(data) {
215
+ return JSON.parse(data.toString("utf8"));
216
+ }
217
+ };
218
+
207
219
  // src/client/kafka.client/infra/dedup.store.ts
208
220
  var InMemoryDedupStore = class {
209
221
  constructor(states) {
@@ -328,6 +340,9 @@ var KafkaRetryExhaustedError = class extends KafkaProcessingError {
328
340
  };
329
341
 
330
342
  // src/client/kafka.client/producer/ops.ts
343
+ function resolveSerde(topicOrDesc, clientSerde) {
344
+ return topicOrDesc?.__serde ?? clientSerde;
345
+ }
331
346
  function resolveTopicName(topicOrDescriptor) {
332
347
  if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
333
348
  if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
@@ -374,6 +389,7 @@ async function validateMessage(topicOrDesc, message, deps, ctx) {
374
389
  }
375
390
  async function buildSendPayload(topicOrDesc, messages, deps, compression) {
376
391
  const topic = resolveTopicName(topicOrDesc);
392
+ const serde = resolveSerde(topicOrDesc, deps.serde);
377
393
  const builtMessages = await Promise.all(
378
394
  messages.map(async (m) => {
379
395
  const envelopeHeaders = buildEnvelopeHeaders({
@@ -393,10 +409,13 @@ async function buildSendPayload(topicOrDesc, messages, deps, compression) {
393
409
  headers: envelopeHeaders,
394
410
  version: m.schemaVersion ?? 1
395
411
  };
412
+ const validated = await validateMessage(topicOrDesc, m.value, deps, sendCtx);
396
413
  return {
397
- value: JSON.stringify(
398
- await validateMessage(topicOrDesc, m.value, deps, sendCtx)
399
- ),
414
+ value: await serde.serialize(validated, {
415
+ topic,
416
+ headers: envelopeHeaders,
417
+ isKey: false
418
+ }),
400
419
  // Explicit key wins; otherwise fall back to the descriptor's .key()
401
420
  // extractor (runs on the original, pre-validation payload).
402
421
  key: m.key ?? topicOrDesc?.__key?.(m.value) ?? null,
@@ -481,6 +500,15 @@ function buildSchemaMap(topics, schemaRegistry, optionSchemas, logger) {
481
500
  }
482
501
  return schemaMap;
483
502
  }
503
+ function buildSerdeMap(topics) {
504
+ let serdeMap;
505
+ for (const t of topics) {
506
+ if (t?.__serde) {
507
+ (serdeMap ??= /* @__PURE__ */ new Map()).set(resolveTopicName(t), t.__serde);
508
+ }
509
+ }
510
+ return serdeMap;
511
+ }
484
512
 
485
513
  // src/client/kafka.client/admin/ops.ts
486
514
  var AdminOps = class {
@@ -766,17 +794,6 @@ var AdminOps = class {
766
794
  function sleep(ms) {
767
795
  return new Promise((resolve) => setTimeout(resolve, ms));
768
796
  }
769
- function parseJsonMessage(raw, topic, logger) {
770
- try {
771
- return JSON.parse(raw);
772
- } catch (error) {
773
- logger.error(
774
- `Failed to parse message from topic ${topic}:`,
775
- toError(error).stack
776
- );
777
- return null;
778
- }
779
- }
780
797
  async function validateWithSchema(message, raw, topic, schemaMap, interceptors, dlq, deps) {
781
798
  const schema = schemaMap.get(topic);
782
799
  if (!schema) return message;
@@ -1169,15 +1186,15 @@ async function replayDlqTopic(topic, deps, options = {}) {
1169
1186
  const originalHeaders = Object.fromEntries(
1170
1187
  Object.entries(headers).filter(([k]) => !deps.dlqHeaderKeys.has(k))
1171
1188
  );
1172
- const value = message.value.toString();
1173
- const shouldProcess = !options.filter || options.filter(headers, value);
1189
+ const bytes = message.value;
1190
+ const shouldProcess = !options.filter || options.filter(headers, bytes.toString("utf8"));
1174
1191
  if (!targetTopic || !shouldProcess) {
1175
1192
  skipped++;
1176
1193
  } else if (options.dryRun) {
1177
1194
  deps.logger.log(`[DLQ replay dry-run] Would replay to "${targetTopic}"`);
1178
1195
  replayed++;
1179
1196
  } else {
1180
- await deps.send(targetTopic, [{ value, headers: originalHeaders }]);
1197
+ await deps.send(targetTopic, [{ value: bytes, headers: originalHeaders }]);
1181
1198
  replayed++;
1182
1199
  }
1183
1200
  const allDone = Array.from(highWatermarks.entries()).every(
@@ -2051,7 +2068,7 @@ async function waitForPartitionAssignment(consumer, topics, logger, timeoutMs =
2051
2068
  `Retry consumer did not receive partition assignments for [${topics.join(", ")}] within ${timeoutMs}ms`
2052
2069
  );
2053
2070
  }
2054
- async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopics, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs) {
2071
+ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopics, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
2055
2072
  const {
2056
2073
  logger,
2057
2074
  producer,
@@ -2097,20 +2114,35 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
2097
2114
  await sleep(remaining);
2098
2115
  consumer.resume([{ topic: levelTopic, partitions: [partition] }]);
2099
2116
  }
2100
- const raw = message.value.toString();
2101
- const parsed = parseJsonMessage(raw, levelTopic, logger);
2102
- if (parsed === null) {
2103
- await consumer.commitOffsets([nextOffset]);
2104
- return;
2105
- }
2117
+ const rawBytes = message.value;
2106
2118
  const currentMaxRetries = parseInt(
2107
2119
  headers[RETRY_HEADER_MAX_RETRIES] ?? String(retry.maxRetries),
2108
2120
  10
2109
2121
  );
2110
2122
  const originalTopic = headers[RETRY_HEADER_ORIGINAL_TOPIC] ?? levelTopic.replace(/\.retry\.\d+$/, "");
2123
+ const serde = serdeMap?.get(originalTopic) ?? deps.serde;
2124
+ let parsed;
2125
+ try {
2126
+ parsed = await serde.deserialize(rawBytes, {
2127
+ topic: originalTopic,
2128
+ headers,
2129
+ isKey: false
2130
+ });
2131
+ } catch (err) {
2132
+ logger.error(
2133
+ `Failed to deserialize retry message from topic ${levelTopic}:`,
2134
+ toError(err).stack
2135
+ );
2136
+ await consumer.commitOffsets([nextOffset]);
2137
+ return;
2138
+ }
2139
+ if (parsed === null) {
2140
+ await consumer.commitOffsets([nextOffset]);
2141
+ return;
2142
+ }
2111
2143
  const validated = await validateWithSchema(
2112
2144
  parsed,
2113
- raw,
2145
+ rawBytes,
2114
2146
  originalTopic,
2115
2147
  schemaMap,
2116
2148
  interceptors,
@@ -2164,7 +2196,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
2164
2196
  const delay = Math.floor(Math.random() * cap);
2165
2197
  const { topic: rtTopic, messages: rtMsgs } = buildRetryTopicPayload(
2166
2198
  originalTopic,
2167
- [raw],
2199
+ [rawBytes],
2168
2200
  nextLevel,
2169
2201
  currentMaxRetries,
2170
2202
  delay,
@@ -2206,7 +2238,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
2206
2238
  } else if (dlq) {
2207
2239
  const { topic: dTopic, messages: dMsgs } = buildDlqPayload(
2208
2240
  originalTopic,
2209
- raw,
2241
+ rawBytes,
2210
2242
  {
2211
2243
  error,
2212
2244
  // +1 to account for the main consumer's initial attempt before routing.
@@ -2268,7 +2300,7 @@ async function startLevelConsumer(level, levelTopics, levelGroupId, originalTopi
2268
2300
  `Retry level ${level}/${retry.maxRetries} consumer started for: ${originalTopics.join(", ")} (group: ${levelGroupId})`
2269
2301
  );
2270
2302
  }
2271
- async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs) {
2303
+ async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps, assignmentTimeoutMs, serdeMap) {
2272
2304
  const levelGroupIds = new Array(retry.maxRetries);
2273
2305
  await Promise.all(
2274
2306
  Array.from({ length: retry.maxRetries }, async (_, i) => {
@@ -2286,7 +2318,8 @@ async function startRetryTopicConsumers(originalTopics, originalGroupId, handleM
2286
2318
  interceptors,
2287
2319
  schemaMap,
2288
2320
  deps,
2289
- assignmentTimeoutMs
2321
+ assignmentTimeoutMs,
2322
+ serdeMap
2290
2323
  );
2291
2324
  levelGroupIds[i] = levelGroupId;
2292
2325
  })
@@ -2370,6 +2403,7 @@ async function setupConsumer(ctx, topics, mode, options) {
2370
2403
  optionSchemas,
2371
2404
  ctx.logger
2372
2405
  );
2406
+ const serdeMap = buildSerdeMap(stringTopics);
2373
2407
  const topicNames = stringTopics.map((t) => resolveTopicName(t));
2374
2408
  const subscribeTopics = [...topicNames, ...regexTopics];
2375
2409
  await ensureConsumerTopics(ctx, topicNames, dlq, options.deduplication);
@@ -2387,7 +2421,7 @@ async function setupConsumer(ctx, topics, mode, options) {
2387
2421
  ctx.logger.log(
2388
2422
  `${mode === "eachBatch" ? "Batch consumer" : "Consumer"} subscribed to topics: ${displayTopics}`
2389
2423
  );
2390
- return { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, hasRegex, readyPromise };
2424
+ return { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, hasRegex, readyPromise };
2391
2425
  }
2392
2426
  function resolveDeduplicationContext(ctx, groupId, options) {
2393
2427
  if (!options) return void 0;
@@ -2399,6 +2433,7 @@ function messageDepsFor(ctx, gid, options) {
2399
2433
  return {
2400
2434
  logger: ctx.logger,
2401
2435
  producer: ctx.producer,
2436
+ serde: ctx.serde,
2402
2437
  instrumentation: ctx.instrumentation,
2403
2438
  onMessageLost: options?.onMessageLost ?? ctx.onMessageLost,
2404
2439
  onTtlExpired: ctx.onTtlExpired,
@@ -2416,6 +2451,7 @@ function buildRetryTopicDeps(ctx) {
2416
2451
  return {
2417
2452
  logger: ctx.logger,
2418
2453
  producer: ctx.producer,
2454
+ serde: ctx.serde,
2419
2455
  instrumentation: ctx.instrumentation,
2420
2456
  onMessageLost: ctx.onMessageLost,
2421
2457
  onRetry: ctx.metrics.notifyRetry.bind(ctx.metrics),
@@ -2433,7 +2469,7 @@ async function makeEosMainContext(ctx, gid, consumer, options) {
2433
2469
  return { txProducer, consumer };
2434
2470
  }
2435
2471
  async function launchRetryChain(ctx, gid, topicNames, handleMessage, opts) {
2436
- const { retry, dlq, interceptors, schemaMap, assignmentTimeoutMs } = opts;
2472
+ const { retry, dlq, interceptors, schemaMap, serdeMap, assignmentTimeoutMs } = opts;
2437
2473
  if (!ctx.autoCreateTopicsEnabled) {
2438
2474
  await ctx.adminOps.validateRetryTopicsExist(topicNames, retry.maxRetries);
2439
2475
  }
@@ -2457,7 +2493,8 @@ async function launchRetryChain(ctx, gid, topicNames, handleMessage, opts) {
2457
2493
  ctx.companionGroupIds.get(gid).push(levelGroupId);
2458
2494
  }
2459
2495
  },
2460
- assignmentTimeoutMs
2496
+ assignmentTimeoutMs,
2497
+ serdeMap
2461
2498
  );
2462
2499
  }
2463
2500
 
@@ -2515,18 +2552,29 @@ async function applyDeduplication(envelope, raw, dedup, dlq, deps) {
2515
2552
  }
2516
2553
  return false;
2517
2554
  }
2518
- async function parseSingleMessage(message, topic, partition, schemaMap, interceptors, dlq, deps) {
2555
+ async function parseSingleMessage(message, topic, partition, schemaMap, interceptors, dlq, deps, serdeMap) {
2519
2556
  if (!message.value) {
2520
2557
  deps.logger.warn(`Received empty message from topic ${topic}`);
2521
2558
  return null;
2522
2559
  }
2523
- const raw = message.value.toString();
2524
- const parsed = parseJsonMessage(raw, topic, deps.logger);
2525
- if (parsed === null) return null;
2560
+ const bytes = message.value;
2526
2561
  const headers = decodeHeaders(message.headers);
2562
+ const serde = serdeMap?.get(topic) ?? deps.serde;
2563
+ let parsed;
2564
+ try {
2565
+ parsed = await serde.deserialize(bytes, { topic, headers, isKey: false });
2566
+ } catch (error) {
2567
+ deps.logger.error(
2568
+ `Failed to deserialize message from topic ${topic}:`,
2569
+ toError(error).stack
2570
+ );
2571
+ return null;
2572
+ }
2573
+ if (parsed === null) return null;
2527
2574
  const validated = await validateWithSchema(
2528
2575
  parsed,
2529
- raw,
2576
+ // Forward the ORIGINAL bytes to DLQ on validation failure (binary-safe).
2577
+ bytes,
2530
2578
  topic,
2531
2579
  schemaMap,
2532
2580
  interceptors,
@@ -2540,6 +2588,7 @@ async function handleEachMessage(payload, opts, deps) {
2540
2588
  const { topic, partition, message } = payload;
2541
2589
  const {
2542
2590
  schemaMap,
2591
+ serdeMap,
2543
2592
  handleMessage,
2544
2593
  interceptors,
2545
2594
  dlq,
@@ -2548,6 +2597,7 @@ async function handleEachMessage(payload, opts, deps) {
2548
2597
  timeoutMs,
2549
2598
  wrapWithTimeout
2550
2599
  } = opts;
2600
+ const rawBytes = message.value;
2551
2601
  const eos = opts.eosMainContext;
2552
2602
  const nextOffsetStr = (parseInt(message.offset, 10) + 1).toString();
2553
2603
  const commitOffset = eos ? async () => {
@@ -2592,7 +2642,8 @@ async function handleEachMessage(payload, opts, deps) {
2592
2642
  schemaMap,
2593
2643
  interceptors,
2594
2644
  dlq,
2595
- deps
2645
+ deps,
2646
+ serdeMap
2596
2647
  );
2597
2648
  if (envelope === null) {
2598
2649
  await commitOffset?.();
@@ -2601,7 +2652,7 @@ async function handleEachMessage(payload, opts, deps) {
2601
2652
  if (opts.deduplication) {
2602
2653
  const isDuplicate = await applyDeduplication(
2603
2654
  envelope,
2604
- message.value.toString(),
2655
+ rawBytes,
2605
2656
  opts.deduplication,
2606
2657
  dlq,
2607
2658
  deps
@@ -2618,7 +2669,7 @@ async function handleEachMessage(payload, opts, deps) {
2618
2669
  `[KafkaClient] TTL expired on ${topic}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
2619
2670
  );
2620
2671
  if (dlq) {
2621
- await sendToDlq(topic, message.value.toString(), deps, {
2672
+ await sendToDlq(topic, rawBytes, deps, {
2622
2673
  error: new Error(`Message TTL expired: age ${ageMs}ms`),
2623
2674
  attempt: 0,
2624
2675
  originalHeaders: envelope.headers
@@ -2650,7 +2701,7 @@ async function handleEachMessage(payload, opts, deps) {
2650
2701
  },
2651
2702
  {
2652
2703
  envelope,
2653
- rawMessages: [message.value.toString()],
2704
+ rawMessages: [rawBytes],
2654
2705
  interceptors,
2655
2706
  dlq,
2656
2707
  retry,
@@ -2663,6 +2714,7 @@ async function handleEachBatch(payload, opts, deps) {
2663
2714
  const { batch, heartbeat, resolveOffset, commitOffsetsIfNecessary } = payload;
2664
2715
  const {
2665
2716
  schemaMap,
2717
+ serdeMap,
2666
2718
  handleBatch,
2667
2719
  interceptors,
2668
2720
  dlq,
@@ -2718,6 +2770,7 @@ async function handleEachBatch(payload, opts, deps) {
2718
2770
  const envelopes = [];
2719
2771
  const rawMessages = [];
2720
2772
  for (const message of batch.messages) {
2773
+ const rawBytes = message.value;
2721
2774
  const envelope = await parseSingleMessage(
2722
2775
  message,
2723
2776
  batch.topic,
@@ -2725,14 +2778,14 @@ async function handleEachBatch(payload, opts, deps) {
2725
2778
  schemaMap,
2726
2779
  interceptors,
2727
2780
  dlq,
2728
- deps
2781
+ deps,
2782
+ serdeMap
2729
2783
  );
2730
2784
  if (envelope === null) continue;
2731
2785
  if (opts.deduplication) {
2732
- const raw = message.value.toString();
2733
2786
  const isDuplicate = await applyDeduplication(
2734
2787
  envelope,
2735
- raw,
2788
+ rawBytes,
2736
2789
  opts.deduplication,
2737
2790
  dlq,
2738
2791
  deps
@@ -2746,7 +2799,7 @@ async function handleEachBatch(payload, opts, deps) {
2746
2799
  `[KafkaClient] TTL expired on ${batch.topic}: age ${ageMs}ms > ${opts.messageTtlMs}ms`
2747
2800
  );
2748
2801
  if (dlq) {
2749
- await sendToDlq(batch.topic, message.value.toString(), deps, {
2802
+ await sendToDlq(batch.topic, rawBytes, deps, {
2750
2803
  error: new Error(`Message TTL expired: age ${ageMs}ms`),
2751
2804
  attempt: 0,
2752
2805
  originalHeaders: envelope.headers
@@ -2765,7 +2818,7 @@ async function handleEachBatch(payload, opts, deps) {
2765
2818
  }
2766
2819
  }
2767
2820
  envelopes.push(envelope);
2768
- rawMessages.push(message.value.toString());
2821
+ rawMessages.push(rawBytes);
2769
2822
  }
2770
2823
  if (envelopes.length === 0) {
2771
2824
  await commitBatchOffset?.();
@@ -2928,7 +2981,7 @@ function resumeTopicAllPartitions(ctx, gid, topic) {
2928
2981
  async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
2929
2982
  validateTopicConsumerOpts(topics, options);
2930
2983
  const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
2931
- const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachMessage", setupOptions);
2984
+ const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachMessage", setupOptions);
2932
2985
  if (options.circuitBreaker)
2933
2986
  ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
2934
2987
  const deps = messageDepsFor(ctx, gid, options);
@@ -2939,6 +2992,7 @@ async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
2939
2992
  payload,
2940
2993
  {
2941
2994
  schemaMap,
2995
+ serdeMap,
2942
2996
  handleMessage,
2943
2997
  interceptors,
2944
2998
  dlq,
@@ -2966,6 +3020,7 @@ async function startConsumerImpl(ctx, topics, handleMessage, options = {}) {
2966
3020
  dlq,
2967
3021
  interceptors,
2968
3022
  schemaMap,
3023
+ serdeMap,
2969
3024
  assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
2970
3025
  });
2971
3026
  }
@@ -2979,7 +3034,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
2979
3034
  );
2980
3035
  }
2981
3036
  const setupOptions = options.retryTopics ? { ...options, autoCommit: false } : options;
2982
- const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachBatch", setupOptions);
3037
+ const { consumer, schemaMap, serdeMap, topicNames, gid, dlq, interceptors, retry, readyPromise } = await setupConsumer(ctx, topics, "eachBatch", setupOptions);
2983
3038
  if (options.circuitBreaker)
2984
3039
  ctx.circuitBreaker.setConfig(gid, options.circuitBreaker);
2985
3040
  const deps = messageDepsFor(ctx, gid, options);
@@ -2990,6 +3045,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
2990
3045
  payload,
2991
3046
  {
2992
3047
  schemaMap,
3048
+ serdeMap,
2993
3049
  handleBatch,
2994
3050
  interceptors,
2995
3051
  dlq,
@@ -3027,6 +3083,7 @@ async function startBatchConsumerImpl(ctx, topics, handleBatch, options = {}) {
3027
3083
  dlq,
3028
3084
  interceptors,
3029
3085
  schemaMap,
3086
+ serdeMap,
3030
3087
  assignmentTimeoutMs: options.retryTopicAssignmentTimeoutMs
3031
3088
  });
3032
3089
  }
@@ -3039,7 +3096,7 @@ async function startTransactionalConsumerImpl(ctx, topics, handler, options = {}
3039
3096
  );
3040
3097
  }
3041
3098
  const setupOptions = { ...options, autoCommit: false };
3042
- const { consumer, schemaMap, gid, readyPromise } = await setupConsumer(
3099
+ const { consumer, schemaMap, serdeMap, gid, readyPromise } = await setupConsumer(
3043
3100
  ctx,
3044
3101
  topics,
3045
3102
  "eachMessage",
@@ -3056,7 +3113,8 @@ async function startTransactionalConsumerImpl(ctx, topics, handler, options = {}
3056
3113
  schemaMap,
3057
3114
  options.interceptors ?? [],
3058
3115
  false,
3059
- deps
3116
+ deps,
3117
+ serdeMap
3060
3118
  );
3061
3119
  const nextOffset = String(Number.parseInt(message.offset, 10) + 1);
3062
3120
  if (envelope === null) {
@@ -3274,7 +3332,9 @@ async function startDelayedRelayImpl(ctx, topics, options) {
3274
3332
  topic: target,
3275
3333
  messages: [
3276
3334
  {
3277
- value: message.value.toString(),
3335
+ // Forward the ORIGINAL wire bytes unchanged — no re-serialization,
3336
+ // so binary payloads (Avro/Protobuf) are relayed losslessly.
3337
+ value: message.value,
3278
3338
  key: message.key ? message.key.toString() : null,
3279
3339
  headers: forwardHeaders
3280
3340
  }
@@ -3601,6 +3661,7 @@ var KafkaClient = class {
3601
3661
  const consumers = /* @__PURE__ */ new Map();
3602
3662
  const consumerCreationOptions = /* @__PURE__ */ new Map();
3603
3663
  const schemaRegistry = /* @__PURE__ */ new Map();
3664
+ const serde = options?.serde ?? new JsonSerde();
3604
3665
  const adminOps = new AdminOps({
3605
3666
  admin: transport.admin(),
3606
3667
  logger,
@@ -3627,6 +3688,7 @@ var KafkaClient = class {
3627
3688
  autoCreateTopicsEnabled: options?.autoCreateTopics ?? false,
3628
3689
  strictSchemasEnabled: options?.strictSchemas ?? true,
3629
3690
  numPartitions: options?.numPartitions ?? 1,
3691
+ serde,
3630
3692
  txId: options?.transactionalId ?? `${clientId}-tx`,
3631
3693
  clockRecoveryTopics: options?.clockRecovery?.topics ?? [],
3632
3694
  clockRecoveryTimeoutMs: options?.clockRecovery?.timeoutMs ?? 3e4,
@@ -3661,6 +3723,7 @@ var KafkaClient = class {
3661
3723
  strictSchemasEnabled: options?.strictSchemas ?? true,
3662
3724
  instrumentation: options?.instrumentation ?? [],
3663
3725
  logger,
3726
+ serde,
3664
3727
  nextLamportClock: () => 0
3665
3728
  // patched below
3666
3729
  },
@@ -3679,6 +3742,7 @@ var KafkaClient = class {
3679
3742
  strictSchemasEnabled: options?.strictSchemas ?? true,
3680
3743
  instrumentation: options?.instrumentation ?? [],
3681
3744
  logger,
3745
+ serde,
3682
3746
  nextLamportClock: () => ++ctx._lamportClock
3683
3747
  };
3684
3748
  ctx.retryTopicDeps = buildRetryTopicDeps(ctx);