@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 @@
1
+ {"version":3,"sources":["../src/serde.ts","../src/client/message/serde.ts","../src/client/message/schema-registry.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport type { MessageSerde, SerdeContext } from \"./client/message/serde\";\nimport type { SchemaRegistryClient } from \"./client/message/schema-registry\";\n\n// Re-export the core serde surface so consumers of `@drarzter/kafka-client/serde`\n// don't also have to import from the root/core entry point.\nexport { JsonSerde } from \"./client/message/serde\";\nexport type { MessageSerde, SerdeContext } from \"./client/message/serde\";\nexport { SchemaRegistryClient } from \"./client/message/schema-registry\";\n\n/** Confluent wire-format magic byte. Always `0x00` for framed records. */\nconst MAGIC_BYTE = 0x00;\n\n/** Injectable dynamic-import function — overridable in tests. */\ntype ImportFn = (specifier: string) => Promise<any>;\n\n/**\n * Load an optional peer (`avsc` / `protobufjs`).\n *\n * Both are CommonJS packages. We resolve them with a `require` built from\n * `createRequire` so the loader works uniformly across this library's dual\n * CJS/ESM output *and* under the Jest CommonJS VM (a raw `import()` throws\n * \"A dynamic import callback was invoked without --experimental-vm-modules\"\n * there). The require base is derived from the current module when available\n * (`__filename` in CJS output / Jest), falling back to the process cwd — either\n * resolves a top-level node_modules dependency. We normalise a potential\n * `default` interop wrapper.\n */\nconst defaultImport: ImportFn = async (specifier) => {\n const base =\n typeof __filename !== \"undefined\"\n ? __filename\n : `${process.cwd()}/index.js`;\n const req = createRequire(base);\n const mod = req(specifier);\n return mod?.default ?? mod;\n};\n\n/**\n * Resolve a subject name for a serde call.\n *\n * Default is Confluent's `TopicNameStrategy`:\n * `<topic>-value` for the value, `<topic>-key` for the key. A `subject` option\n * may override with either a literal string or a function of the context.\n */\nfunction resolveSubject(\n ctx: SerdeContext,\n subject?: string | ((ctx: SerdeContext) => string),\n): string {\n if (typeof subject === \"function\") return subject(ctx);\n if (typeof subject === \"string\") return subject;\n return `${ctx.topic}-${ctx.isKey ? \"key\" : \"value\"}`;\n}\n\n/** Read the `[magic][4-byte BE id]` prefix; assert the magic byte. Returns `{ id, offset }`. */\nfunction readWireHeader(\n data: Buffer,\n serdeName: string,\n): { id: number; offset: number } {\n if (data.length < 5) {\n throw new Error(\n `${serdeName}: message too short to be Confluent-framed (need >= 5 bytes, got ${data.length}).`,\n );\n }\n const magic = data[0];\n if (magic !== MAGIC_BYTE) {\n throw new Error(\n `${serdeName}: unexpected magic byte 0x${(magic ?? 0).toString(16).padStart(2, \"0\")} ` +\n `(expected 0x00). The message is not Confluent wire-format — check the producer's serializer.`,\n );\n }\n const id = data.readInt32BE(1);\n return { id, offset: 5 };\n}\n\n/** Frame `[magic][4-byte BE id]` + `payloadParts` into a single Buffer. */\nfunction frame(id: number, ...payloadParts: Buffer[]): Buffer {\n const header = Buffer.alloc(5);\n header.writeUInt8(MAGIC_BYTE, 0);\n header.writeInt32BE(id, 1);\n return Buffer.concat([header, ...payloadParts]);\n}\n\n/** Options common to the registry-backed serdes. */\ninterface RegistrySerdeCommonOptions {\n /** Schema Registry client used to resolve/register schema ids. */\n registry: SchemaRegistryClient;\n /**\n * Subject name. Defaults to Confluent `TopicNameStrategy`\n * (`<topic>-value` / `<topic>-key`). Provide a literal string or a\n * function of the {@link SerdeContext} to override.\n */\n subject?: string | ((ctx: SerdeContext) => string);\n /**\n * Register `schema` on first serialize to obtain its id (dev-friendly).\n * Default `false` → the id is resolved via `getLatestSchema(subject)`.\n */\n autoRegister?: boolean;\n /** @internal Injectable dynamic import for tests. */\n importFn?: ImportFn;\n}\n\n/** Options for {@link avroSerde}. */\nexport interface AvroSerdeOptions extends RegistrySerdeCommonOptions {\n /**\n * Avro schema (JSON string or object) used to serialize, and as the\n * write-schema fallback. Required to serialize; deserialize resolves the\n * writer schema from the registry via the wire-format id.\n */\n schema?: string | object;\n}\n\n/**\n * Confluent-wire-format **Avro** serde backed by a Schema Registry.\n *\n * Produces/consumes the exact byte layout Java/Go clients use, so this library\n * interoperates with them through a shared registry:\n *\n * ```\n * [magic 0x00][schema id: 4-byte big-endian][avro binary]\n * ```\n *\n * Uses the optional peer dependency [`avsc`](https://www.npmjs.com/package/avsc)\n * via dynamic import — install it to enable Avro:\n *\n * ```bash\n * npm install avsc\n * ```\n *\n * - **serialize**: resolves the subject from the context, obtains the schema id\n * (`registerSchema` when `autoRegister`, else `getLatestSchema`), Avro-encodes\n * `value` against `schema`, and frames the bytes.\n * - **deserialize**: reads the magic byte + big-endian id, resolves the writer\n * schema via `registry.getSchemaById(id)` (cached forever), and Avro-decodes\n * the remainder. The reader schema equals the writer schema in v1 — full\n * reader-schema resolution / schema evolution is a future enhancement.\n *\n * The parsed `avsc` type is cached per schema string so repeated messages don't\n * re-parse the schema.\n *\n * @example\n * ```ts\n * import { avroSerde } from '@drarzter/kafka-client/serde';\n * import { SchemaRegistryClient } from '@drarzter/kafka-client';\n *\n * const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });\n * const orderSchema = {\n * type: 'record',\n * name: 'Order',\n * fields: [{ name: 'orderId', type: 'string' }, { name: 'amount', type: 'double' }],\n * };\n *\n * // Per-topic:\n * const Orders = topic('orders')\n * .serde(avroSerde({ registry, schema: orderSchema }))\n * .type<Order>();\n *\n * // Client-wide:\n * const kafka = new KafkaClient(id, group, brokers, {\n * serde: avroSerde({ registry, schema: orderSchema }),\n * });\n * ```\n */\nexport function avroSerde(options: AvroSerdeOptions): MessageSerde {\n const importFn = options.importFn ?? defaultImport;\n const schemaString =\n options.schema === undefined\n ? undefined\n : typeof options.schema === \"string\"\n ? options.schema\n : JSON.stringify(options.schema);\n\n /** Parsed-type cache keyed by schema string, so we never re-parse per message. */\n const typeCache = new Map<string, any>();\n let avscMod: any;\n\n async function loadAvsc(): Promise<any> {\n if (avscMod) return avscMod;\n try {\n avscMod = await importFn(\"avsc\");\n } catch {\n throw new Error(\n \"avroSerde: package 'avsc' is not installed. \" +\n \"Run `npm install avsc` to enable Avro serialization.\",\n );\n }\n return avscMod;\n }\n\n async function typeFor(schemaStr: string): Promise<any> {\n const cached = typeCache.get(schemaStr);\n if (cached) return cached;\n const avsc = await loadAvsc();\n const type = avsc.Type.forSchema(JSON.parse(schemaStr));\n typeCache.set(schemaStr, type);\n return type;\n }\n\n return {\n async serialize(value: unknown, ctx: SerdeContext): Promise<Buffer> {\n if (schemaString === undefined) {\n throw new Error(\n \"avroSerde: `schema` is required to serialize — pass the Avro schema \" +\n \"(JSON string or object) in the serde options.\",\n );\n }\n const subject = resolveSubject(ctx, options.subject);\n const id = options.autoRegister\n ? (await options.registry.registerSchema(subject, schemaString, \"AVRO\"))\n .id\n : (await options.registry.getLatestSchema(subject)).id;\n const type = await typeFor(schemaString);\n const payload: Buffer = type.toBuffer(value);\n return frame(id, payload);\n },\n\n async deserialize(data: Buffer, _ctx: SerdeContext): Promise<unknown> {\n const { id, offset } = readWireHeader(data, \"avroSerde\");\n const registered = await options.registry.getSchemaById(id);\n const type = await typeFor(registered.schema);\n return type.fromBuffer(data.subarray(offset));\n },\n };\n}\n\n/** Options for {@link protobufSerde}. */\nexport interface ProtobufSerdeOptions extends RegistrySerdeCommonOptions {\n /**\n * Fully-qualified Protobuf message name to encode/decode,\n * e.g. `\"com.acme.orders.Order\"`.\n */\n messageType: string;\n /**\n * `.proto` source string defining {@link ProtobufSerdeOptions.messageType}.\n * Required to serialize; deserialize resolves the writer `.proto` from the\n * registry via the wire-format id.\n */\n schema?: string;\n}\n\n/**\n * Confluent-wire-format **Protobuf** serde backed by a Schema Registry.\n *\n * Produces/consumes the exact byte layout Java/Go clients use:\n *\n * ```\n * [magic 0x00][schema id: 4-byte big-endian][message-index][protobuf binary]\n * ```\n *\n * The **message-index** identifies which message type within the `.proto` file\n * was used. For the first/top-level message type (index `[0]`) Confluent writes\n * the single byte `0x00`. This serde implements that top-level case only —\n * multiple/nested message types are a documented v1 limitation and cause a\n * clear error on deserialize.\n *\n * Uses the optional peer dependency\n * [`protobufjs`](https://www.npmjs.com/package/protobufjs) via dynamic import —\n * install it to enable Protobuf:\n *\n * ```bash\n * npm install protobufjs\n * ```\n *\n * - **serialize**: obtains the schema id (`registerSchema` when `autoRegister`,\n * else `getLatestSchema`), encodes `value` with the `protobufjs` `Type`, and\n * frames it with the `0x00` message-index byte.\n * - **deserialize**: reads the magic byte + big-endian id + message-index (which\n * must be the single `0x00` byte), resolves the writer `.proto` via\n * `registry.getSchemaById(id)` (cached forever), and decodes the remainder.\n *\n * The parsed `protobufjs` `Type` is cached per schema string so repeated\n * messages don't re-parse the `.proto`.\n *\n * @example\n * ```ts\n * import { protobufSerde } from '@drarzter/kafka-client/serde';\n * import { SchemaRegistryClient } from '@drarzter/kafka-client';\n *\n * const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });\n * const proto = `\n * syntax = \"proto3\";\n * message Order { string orderId = 1; double amount = 2; }\n * `;\n *\n * // Per-topic:\n * const Orders = topic('orders')\n * .serde(protobufSerde({ registry, schema: proto, messageType: 'Order' }))\n * .type<Order>();\n *\n * // Client-wide:\n * const kafka = new KafkaClient(id, group, brokers, {\n * serde: protobufSerde({ registry, schema: proto, messageType: 'Order' }),\n * });\n * ```\n */\nexport function protobufSerde(options: ProtobufSerdeOptions): MessageSerde {\n const importFn = options.importFn ?? defaultImport;\n\n /** Parsed-`Type` cache keyed by `.proto` source string. */\n const typeCache = new Map<string, any>();\n let protobufMod: any;\n\n async function loadProtobuf(): Promise<any> {\n if (protobufMod) return protobufMod;\n try {\n protobufMod = await importFn(\"protobufjs\");\n } catch {\n throw new Error(\n \"protobufSerde: package 'protobufjs' is not installed. \" +\n \"Run `npm install protobufjs` to enable Protobuf serialization.\",\n );\n }\n return protobufMod;\n }\n\n async function typeFor(protoSource: string): Promise<any> {\n const cacheKey = `${protoSource}::${options.messageType}`;\n const cached = typeCache.get(cacheKey);\n if (cached) return cached;\n const protobuf = await loadProtobuf();\n const parsed = protobuf.parse(protoSource);\n const type = parsed.root.lookupType(options.messageType);\n typeCache.set(cacheKey, type);\n return type;\n }\n\n return {\n async serialize(value: unknown, ctx: SerdeContext): Promise<Buffer> {\n if (options.schema === undefined) {\n throw new Error(\n \"protobufSerde: `schema` is required to serialize — pass the .proto \" +\n \"source string in the serde options.\",\n );\n }\n const subject = resolveSubject(ctx, options.subject);\n const id = options.autoRegister\n ? (\n await options.registry.registerSchema(\n subject,\n options.schema,\n \"PROTOBUF\",\n )\n ).id\n : (await options.registry.getLatestSchema(subject)).id;\n const type = await typeFor(options.schema);\n const payload: Buffer = Buffer.from(\n type.encode(type.create(value as object)).finish(),\n );\n // Message-index for the top-level type ([0]) is the single byte 0x00.\n const messageIndex = Buffer.from([0x00]);\n return frame(id, messageIndex, payload);\n },\n\n async deserialize(data: Buffer, _ctx: SerdeContext): Promise<unknown> {\n const { id, offset } = readWireHeader(data, \"protobufSerde\");\n // Message-index: Confluent writes 0x00 for the top-level type ([0]). Any\n // other leading value is a varint length of a multi-element index array.\n const indexByte = data[offset];\n if (indexByte !== 0x00) {\n throw new Error(\n \"protobufSerde: nested/multiple message types are not supported in v1 \" +\n `(message-index byte was 0x${(indexByte ?? 0).toString(16).padStart(2, \"0\")}, ` +\n \"expected 0x00 for the top-level message type).\",\n );\n }\n const registered = await options.registry.getSchemaById(id);\n const type = await typeFor(registered.schema);\n const decoded = type.decode(data.subarray(offset + 1));\n return type.toObject(decoded, {\n longs: String,\n enums: String,\n bytes: Buffer,\n defaults: true,\n });\n },\n };\n}\n","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/serde.mjs ADDED
@@ -0,0 +1,158 @@
1
+ import {
2
+ JsonSerde,
3
+ SchemaRegistryClient
4
+ } from "./chunk-PQVBRDNV.mjs";
5
+ import "./chunk-EQQGB2QZ.mjs";
6
+
7
+ // src/serde.ts
8
+ import { createRequire } from "module";
9
+ var MAGIC_BYTE = 0;
10
+ var defaultImport = async (specifier) => {
11
+ const base = typeof __filename !== "undefined" ? __filename : `${process.cwd()}/index.js`;
12
+ const req = createRequire(base);
13
+ const mod = req(specifier);
14
+ return mod?.default ?? mod;
15
+ };
16
+ function resolveSubject(ctx, subject) {
17
+ if (typeof subject === "function") return subject(ctx);
18
+ if (typeof subject === "string") return subject;
19
+ return `${ctx.topic}-${ctx.isKey ? "key" : "value"}`;
20
+ }
21
+ function readWireHeader(data, serdeName) {
22
+ if (data.length < 5) {
23
+ throw new Error(
24
+ `${serdeName}: message too short to be Confluent-framed (need >= 5 bytes, got ${data.length}).`
25
+ );
26
+ }
27
+ const magic = data[0];
28
+ if (magic !== MAGIC_BYTE) {
29
+ throw new Error(
30
+ `${serdeName}: unexpected magic byte 0x${(magic ?? 0).toString(16).padStart(2, "0")} (expected 0x00). The message is not Confluent wire-format \u2014 check the producer's serializer.`
31
+ );
32
+ }
33
+ const id = data.readInt32BE(1);
34
+ return { id, offset: 5 };
35
+ }
36
+ function frame(id, ...payloadParts) {
37
+ const header = Buffer.alloc(5);
38
+ header.writeUInt8(MAGIC_BYTE, 0);
39
+ header.writeInt32BE(id, 1);
40
+ return Buffer.concat([header, ...payloadParts]);
41
+ }
42
+ function avroSerde(options) {
43
+ const importFn = options.importFn ?? defaultImport;
44
+ const schemaString = options.schema === void 0 ? void 0 : typeof options.schema === "string" ? options.schema : JSON.stringify(options.schema);
45
+ const typeCache = /* @__PURE__ */ new Map();
46
+ let avscMod;
47
+ async function loadAvsc() {
48
+ if (avscMod) return avscMod;
49
+ try {
50
+ avscMod = await importFn("avsc");
51
+ } catch {
52
+ throw new Error(
53
+ "avroSerde: package 'avsc' is not installed. Run `npm install avsc` to enable Avro serialization."
54
+ );
55
+ }
56
+ return avscMod;
57
+ }
58
+ async function typeFor(schemaStr) {
59
+ const cached = typeCache.get(schemaStr);
60
+ if (cached) return cached;
61
+ const avsc = await loadAvsc();
62
+ const type = avsc.Type.forSchema(JSON.parse(schemaStr));
63
+ typeCache.set(schemaStr, type);
64
+ return type;
65
+ }
66
+ return {
67
+ async serialize(value, ctx) {
68
+ if (schemaString === void 0) {
69
+ throw new Error(
70
+ "avroSerde: `schema` is required to serialize \u2014 pass the Avro schema (JSON string or object) in the serde options."
71
+ );
72
+ }
73
+ const subject = resolveSubject(ctx, options.subject);
74
+ const id = options.autoRegister ? (await options.registry.registerSchema(subject, schemaString, "AVRO")).id : (await options.registry.getLatestSchema(subject)).id;
75
+ const type = await typeFor(schemaString);
76
+ const payload = type.toBuffer(value);
77
+ return frame(id, payload);
78
+ },
79
+ async deserialize(data, _ctx) {
80
+ const { id, offset } = readWireHeader(data, "avroSerde");
81
+ const registered = await options.registry.getSchemaById(id);
82
+ const type = await typeFor(registered.schema);
83
+ return type.fromBuffer(data.subarray(offset));
84
+ }
85
+ };
86
+ }
87
+ function protobufSerde(options) {
88
+ const importFn = options.importFn ?? defaultImport;
89
+ const typeCache = /* @__PURE__ */ new Map();
90
+ let protobufMod;
91
+ async function loadProtobuf() {
92
+ if (protobufMod) return protobufMod;
93
+ try {
94
+ protobufMod = await importFn("protobufjs");
95
+ } catch {
96
+ throw new Error(
97
+ "protobufSerde: package 'protobufjs' is not installed. Run `npm install protobufjs` to enable Protobuf serialization."
98
+ );
99
+ }
100
+ return protobufMod;
101
+ }
102
+ async function typeFor(protoSource) {
103
+ const cacheKey = `${protoSource}::${options.messageType}`;
104
+ const cached = typeCache.get(cacheKey);
105
+ if (cached) return cached;
106
+ const protobuf = await loadProtobuf();
107
+ const parsed = protobuf.parse(protoSource);
108
+ const type = parsed.root.lookupType(options.messageType);
109
+ typeCache.set(cacheKey, type);
110
+ return type;
111
+ }
112
+ return {
113
+ async serialize(value, ctx) {
114
+ if (options.schema === void 0) {
115
+ throw new Error(
116
+ "protobufSerde: `schema` is required to serialize \u2014 pass the .proto source string in the serde options."
117
+ );
118
+ }
119
+ const subject = resolveSubject(ctx, options.subject);
120
+ const id = options.autoRegister ? (await options.registry.registerSchema(
121
+ subject,
122
+ options.schema,
123
+ "PROTOBUF"
124
+ )).id : (await options.registry.getLatestSchema(subject)).id;
125
+ const type = await typeFor(options.schema);
126
+ const payload = Buffer.from(
127
+ type.encode(type.create(value)).finish()
128
+ );
129
+ const messageIndex = Buffer.from([0]);
130
+ return frame(id, messageIndex, payload);
131
+ },
132
+ async deserialize(data, _ctx) {
133
+ const { id, offset } = readWireHeader(data, "protobufSerde");
134
+ const indexByte = data[offset];
135
+ if (indexByte !== 0) {
136
+ throw new Error(
137
+ `protobufSerde: nested/multiple message types are not supported in v1 (message-index byte was 0x${(indexByte ?? 0).toString(16).padStart(2, "0")}, expected 0x00 for the top-level message type).`
138
+ );
139
+ }
140
+ const registered = await options.registry.getSchemaById(id);
141
+ const type = await typeFor(registered.schema);
142
+ const decoded = type.decode(data.subarray(offset + 1));
143
+ return type.toObject(decoded, {
144
+ longs: String,
145
+ enums: String,
146
+ bytes: Buffer,
147
+ defaults: true
148
+ });
149
+ }
150
+ };
151
+ }
152
+ export {
153
+ JsonSerde,
154
+ SchemaRegistryClient,
155
+ avroSerde,
156
+ protobufSerde
157
+ };
158
+ //# sourceMappingURL=serde.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/serde.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport type { MessageSerde, SerdeContext } from \"./client/message/serde\";\nimport type { SchemaRegistryClient } from \"./client/message/schema-registry\";\n\n// Re-export the core serde surface so consumers of `@drarzter/kafka-client/serde`\n// don't also have to import from the root/core entry point.\nexport { JsonSerde } from \"./client/message/serde\";\nexport type { MessageSerde, SerdeContext } from \"./client/message/serde\";\nexport { SchemaRegistryClient } from \"./client/message/schema-registry\";\n\n/** Confluent wire-format magic byte. Always `0x00` for framed records. */\nconst MAGIC_BYTE = 0x00;\n\n/** Injectable dynamic-import function — overridable in tests. */\ntype ImportFn = (specifier: string) => Promise<any>;\n\n/**\n * Load an optional peer (`avsc` / `protobufjs`).\n *\n * Both are CommonJS packages. We resolve them with a `require` built from\n * `createRequire` so the loader works uniformly across this library's dual\n * CJS/ESM output *and* under the Jest CommonJS VM (a raw `import()` throws\n * \"A dynamic import callback was invoked without --experimental-vm-modules\"\n * there). The require base is derived from the current module when available\n * (`__filename` in CJS output / Jest), falling back to the process cwd — either\n * resolves a top-level node_modules dependency. We normalise a potential\n * `default` interop wrapper.\n */\nconst defaultImport: ImportFn = async (specifier) => {\n const base =\n typeof __filename !== \"undefined\"\n ? __filename\n : `${process.cwd()}/index.js`;\n const req = createRequire(base);\n const mod = req(specifier);\n return mod?.default ?? mod;\n};\n\n/**\n * Resolve a subject name for a serde call.\n *\n * Default is Confluent's `TopicNameStrategy`:\n * `<topic>-value` for the value, `<topic>-key` for the key. A `subject` option\n * may override with either a literal string or a function of the context.\n */\nfunction resolveSubject(\n ctx: SerdeContext,\n subject?: string | ((ctx: SerdeContext) => string),\n): string {\n if (typeof subject === \"function\") return subject(ctx);\n if (typeof subject === \"string\") return subject;\n return `${ctx.topic}-${ctx.isKey ? \"key\" : \"value\"}`;\n}\n\n/** Read the `[magic][4-byte BE id]` prefix; assert the magic byte. Returns `{ id, offset }`. */\nfunction readWireHeader(\n data: Buffer,\n serdeName: string,\n): { id: number; offset: number } {\n if (data.length < 5) {\n throw new Error(\n `${serdeName}: message too short to be Confluent-framed (need >= 5 bytes, got ${data.length}).`,\n );\n }\n const magic = data[0];\n if (magic !== MAGIC_BYTE) {\n throw new Error(\n `${serdeName}: unexpected magic byte 0x${(magic ?? 0).toString(16).padStart(2, \"0\")} ` +\n `(expected 0x00). The message is not Confluent wire-format — check the producer's serializer.`,\n );\n }\n const id = data.readInt32BE(1);\n return { id, offset: 5 };\n}\n\n/** Frame `[magic][4-byte BE id]` + `payloadParts` into a single Buffer. */\nfunction frame(id: number, ...payloadParts: Buffer[]): Buffer {\n const header = Buffer.alloc(5);\n header.writeUInt8(MAGIC_BYTE, 0);\n header.writeInt32BE(id, 1);\n return Buffer.concat([header, ...payloadParts]);\n}\n\n/** Options common to the registry-backed serdes. */\ninterface RegistrySerdeCommonOptions {\n /** Schema Registry client used to resolve/register schema ids. */\n registry: SchemaRegistryClient;\n /**\n * Subject name. Defaults to Confluent `TopicNameStrategy`\n * (`<topic>-value` / `<topic>-key`). Provide a literal string or a\n * function of the {@link SerdeContext} to override.\n */\n subject?: string | ((ctx: SerdeContext) => string);\n /**\n * Register `schema` on first serialize to obtain its id (dev-friendly).\n * Default `false` → the id is resolved via `getLatestSchema(subject)`.\n */\n autoRegister?: boolean;\n /** @internal Injectable dynamic import for tests. */\n importFn?: ImportFn;\n}\n\n/** Options for {@link avroSerde}. */\nexport interface AvroSerdeOptions extends RegistrySerdeCommonOptions {\n /**\n * Avro schema (JSON string or object) used to serialize, and as the\n * write-schema fallback. Required to serialize; deserialize resolves the\n * writer schema from the registry via the wire-format id.\n */\n schema?: string | object;\n}\n\n/**\n * Confluent-wire-format **Avro** serde backed by a Schema Registry.\n *\n * Produces/consumes the exact byte layout Java/Go clients use, so this library\n * interoperates with them through a shared registry:\n *\n * ```\n * [magic 0x00][schema id: 4-byte big-endian][avro binary]\n * ```\n *\n * Uses the optional peer dependency [`avsc`](https://www.npmjs.com/package/avsc)\n * via dynamic import — install it to enable Avro:\n *\n * ```bash\n * npm install avsc\n * ```\n *\n * - **serialize**: resolves the subject from the context, obtains the schema id\n * (`registerSchema` when `autoRegister`, else `getLatestSchema`), Avro-encodes\n * `value` against `schema`, and frames the bytes.\n * - **deserialize**: reads the magic byte + big-endian id, resolves the writer\n * schema via `registry.getSchemaById(id)` (cached forever), and Avro-decodes\n * the remainder. The reader schema equals the writer schema in v1 — full\n * reader-schema resolution / schema evolution is a future enhancement.\n *\n * The parsed `avsc` type is cached per schema string so repeated messages don't\n * re-parse the schema.\n *\n * @example\n * ```ts\n * import { avroSerde } from '@drarzter/kafka-client/serde';\n * import { SchemaRegistryClient } from '@drarzter/kafka-client';\n *\n * const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });\n * const orderSchema = {\n * type: 'record',\n * name: 'Order',\n * fields: [{ name: 'orderId', type: 'string' }, { name: 'amount', type: 'double' }],\n * };\n *\n * // Per-topic:\n * const Orders = topic('orders')\n * .serde(avroSerde({ registry, schema: orderSchema }))\n * .type<Order>();\n *\n * // Client-wide:\n * const kafka = new KafkaClient(id, group, brokers, {\n * serde: avroSerde({ registry, schema: orderSchema }),\n * });\n * ```\n */\nexport function avroSerde(options: AvroSerdeOptions): MessageSerde {\n const importFn = options.importFn ?? defaultImport;\n const schemaString =\n options.schema === undefined\n ? undefined\n : typeof options.schema === \"string\"\n ? options.schema\n : JSON.stringify(options.schema);\n\n /** Parsed-type cache keyed by schema string, so we never re-parse per message. */\n const typeCache = new Map<string, any>();\n let avscMod: any;\n\n async function loadAvsc(): Promise<any> {\n if (avscMod) return avscMod;\n try {\n avscMod = await importFn(\"avsc\");\n } catch {\n throw new Error(\n \"avroSerde: package 'avsc' is not installed. \" +\n \"Run `npm install avsc` to enable Avro serialization.\",\n );\n }\n return avscMod;\n }\n\n async function typeFor(schemaStr: string): Promise<any> {\n const cached = typeCache.get(schemaStr);\n if (cached) return cached;\n const avsc = await loadAvsc();\n const type = avsc.Type.forSchema(JSON.parse(schemaStr));\n typeCache.set(schemaStr, type);\n return type;\n }\n\n return {\n async serialize(value: unknown, ctx: SerdeContext): Promise<Buffer> {\n if (schemaString === undefined) {\n throw new Error(\n \"avroSerde: `schema` is required to serialize — pass the Avro schema \" +\n \"(JSON string or object) in the serde options.\",\n );\n }\n const subject = resolveSubject(ctx, options.subject);\n const id = options.autoRegister\n ? (await options.registry.registerSchema(subject, schemaString, \"AVRO\"))\n .id\n : (await options.registry.getLatestSchema(subject)).id;\n const type = await typeFor(schemaString);\n const payload: Buffer = type.toBuffer(value);\n return frame(id, payload);\n },\n\n async deserialize(data: Buffer, _ctx: SerdeContext): Promise<unknown> {\n const { id, offset } = readWireHeader(data, \"avroSerde\");\n const registered = await options.registry.getSchemaById(id);\n const type = await typeFor(registered.schema);\n return type.fromBuffer(data.subarray(offset));\n },\n };\n}\n\n/** Options for {@link protobufSerde}. */\nexport interface ProtobufSerdeOptions extends RegistrySerdeCommonOptions {\n /**\n * Fully-qualified Protobuf message name to encode/decode,\n * e.g. `\"com.acme.orders.Order\"`.\n */\n messageType: string;\n /**\n * `.proto` source string defining {@link ProtobufSerdeOptions.messageType}.\n * Required to serialize; deserialize resolves the writer `.proto` from the\n * registry via the wire-format id.\n */\n schema?: string;\n}\n\n/**\n * Confluent-wire-format **Protobuf** serde backed by a Schema Registry.\n *\n * Produces/consumes the exact byte layout Java/Go clients use:\n *\n * ```\n * [magic 0x00][schema id: 4-byte big-endian][message-index][protobuf binary]\n * ```\n *\n * The **message-index** identifies which message type within the `.proto` file\n * was used. For the first/top-level message type (index `[0]`) Confluent writes\n * the single byte `0x00`. This serde implements that top-level case only —\n * multiple/nested message types are a documented v1 limitation and cause a\n * clear error on deserialize.\n *\n * Uses the optional peer dependency\n * [`protobufjs`](https://www.npmjs.com/package/protobufjs) via dynamic import —\n * install it to enable Protobuf:\n *\n * ```bash\n * npm install protobufjs\n * ```\n *\n * - **serialize**: obtains the schema id (`registerSchema` when `autoRegister`,\n * else `getLatestSchema`), encodes `value` with the `protobufjs` `Type`, and\n * frames it with the `0x00` message-index byte.\n * - **deserialize**: reads the magic byte + big-endian id + message-index (which\n * must be the single `0x00` byte), resolves the writer `.proto` via\n * `registry.getSchemaById(id)` (cached forever), and decodes the remainder.\n *\n * The parsed `protobufjs` `Type` is cached per schema string so repeated\n * messages don't re-parse the `.proto`.\n *\n * @example\n * ```ts\n * import { protobufSerde } from '@drarzter/kafka-client/serde';\n * import { SchemaRegistryClient } from '@drarzter/kafka-client';\n *\n * const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });\n * const proto = `\n * syntax = \"proto3\";\n * message Order { string orderId = 1; double amount = 2; }\n * `;\n *\n * // Per-topic:\n * const Orders = topic('orders')\n * .serde(protobufSerde({ registry, schema: proto, messageType: 'Order' }))\n * .type<Order>();\n *\n * // Client-wide:\n * const kafka = new KafkaClient(id, group, brokers, {\n * serde: protobufSerde({ registry, schema: proto, messageType: 'Order' }),\n * });\n * ```\n */\nexport function protobufSerde(options: ProtobufSerdeOptions): MessageSerde {\n const importFn = options.importFn ?? defaultImport;\n\n /** Parsed-`Type` cache keyed by `.proto` source string. */\n const typeCache = new Map<string, any>();\n let protobufMod: any;\n\n async function loadProtobuf(): Promise<any> {\n if (protobufMod) return protobufMod;\n try {\n protobufMod = await importFn(\"protobufjs\");\n } catch {\n throw new Error(\n \"protobufSerde: package 'protobufjs' is not installed. \" +\n \"Run `npm install protobufjs` to enable Protobuf serialization.\",\n );\n }\n return protobufMod;\n }\n\n async function typeFor(protoSource: string): Promise<any> {\n const cacheKey = `${protoSource}::${options.messageType}`;\n const cached = typeCache.get(cacheKey);\n if (cached) return cached;\n const protobuf = await loadProtobuf();\n const parsed = protobuf.parse(protoSource);\n const type = parsed.root.lookupType(options.messageType);\n typeCache.set(cacheKey, type);\n return type;\n }\n\n return {\n async serialize(value: unknown, ctx: SerdeContext): Promise<Buffer> {\n if (options.schema === undefined) {\n throw new Error(\n \"protobufSerde: `schema` is required to serialize — pass the .proto \" +\n \"source string in the serde options.\",\n );\n }\n const subject = resolveSubject(ctx, options.subject);\n const id = options.autoRegister\n ? (\n await options.registry.registerSchema(\n subject,\n options.schema,\n \"PROTOBUF\",\n )\n ).id\n : (await options.registry.getLatestSchema(subject)).id;\n const type = await typeFor(options.schema);\n const payload: Buffer = Buffer.from(\n type.encode(type.create(value as object)).finish(),\n );\n // Message-index for the top-level type ([0]) is the single byte 0x00.\n const messageIndex = Buffer.from([0x00]);\n return frame(id, messageIndex, payload);\n },\n\n async deserialize(data: Buffer, _ctx: SerdeContext): Promise<unknown> {\n const { id, offset } = readWireHeader(data, \"protobufSerde\");\n // Message-index: Confluent writes 0x00 for the top-level type ([0]). Any\n // other leading value is a varint length of a multi-element index array.\n const indexByte = data[offset];\n if (indexByte !== 0x00) {\n throw new Error(\n \"protobufSerde: nested/multiple message types are not supported in v1 \" +\n `(message-index byte was 0x${(indexByte ?? 0).toString(16).padStart(2, \"0\")}, ` +\n \"expected 0x00 for the top-level message type).\",\n );\n }\n const registered = await options.registry.getSchemaById(id);\n const type = await typeFor(registered.schema);\n const decoded = type.decode(data.subarray(offset + 1));\n return type.toObject(decoded, {\n longs: String,\n enums: String,\n bytes: Buffer,\n defaults: true,\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drarzter/kafka-client",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Type-safe Kafka client wrapper for NestJS with typed topic-message maps",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -29,6 +29,11 @@
29
29
  "types": "./dist/otel.d.ts",
30
30
  "import": "./dist/otel.mjs",
31
31
  "require": "./dist/otel.js"
32
+ },
33
+ "./serde": {
34
+ "types": "./dist/serde.d.ts",
35
+ "import": "./dist/serde.mjs",
36
+ "require": "./dist/serde.js"
32
37
  }
33
38
  },
34
39
  "typesVersions": {
@@ -41,6 +46,9 @@
41
46
  ],
42
47
  "otel": [
43
48
  "./dist/otel.d.ts"
49
+ ],
50
+ "serde": [
51
+ "./dist/serde.d.ts"
44
52
  ]
45
53
  }
46
54
  },
@@ -82,6 +90,8 @@
82
90
  "@nestjs/common": ">=10.0.0",
83
91
  "@nestjs/core": ">=10.0.0",
84
92
  "@opentelemetry/api": ">=1.0.0",
93
+ "avsc": ">=5.0.0",
94
+ "protobufjs": ">=7.0.0",
85
95
  "reflect-metadata": ">=0.1.13",
86
96
  "rxjs": ">=7.0.0"
87
97
  },
@@ -95,6 +105,12 @@
95
105
  "@opentelemetry/api": {
96
106
  "optional": true
97
107
  },
108
+ "avsc": {
109
+ "optional": true
110
+ },
111
+ "protobufjs": {
112
+ "optional": true
113
+ },
98
114
  "reflect-metadata": {
99
115
  "optional": true
100
116
  },
@@ -107,16 +123,19 @@
107
123
  "@nestjs/core": "^11.1.13",
108
124
  "@opentelemetry/api": "^1.9.0",
109
125
  "@testcontainers/kafka": "^12.0.4",
126
+ "@testcontainers/redpanda": "^12.0.4",
110
127
  "@types/jest": "^30.0.0",
111
128
  "@types/node": "^26.1.0",
112
129
  "@types/pg": "^8.11.10",
113
130
  "@typescript-eslint/eslint-plugin": "^8.55.0",
114
131
  "@typescript-eslint/parser": "^8.55.0",
132
+ "avsc": "^5.7.7",
115
133
  "eslint": "^10.2.0",
116
134
  "eslint-config-prettier": "^10.1.8",
117
135
  "jest": "^30.2.0",
118
136
  "pg": "^8.13.1",
119
137
  "prettier": "^3.8.1",
138
+ "protobufjs": "^7.4.0",
120
139
  "redis": "^4.7.0",
121
140
  "reflect-metadata": "^0.2.2",
122
141
  "rxjs": "^7.8.2",