@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
|
@@ -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:
|
|
398
|
-
|
|
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
|
|
1173
|
-
const shouldProcess = !options.filter || options.filter(headers,
|
|
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
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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: [
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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);
|