@drarzter/kafka-client 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -2
- package/dist/{chunk-CMO7SMVK.mjs → chunk-OR7TPAAE.mjs} +110 -164
- package/dist/chunk-OR7TPAAE.mjs.map +1 -0
- package/dist/chunk-PQVBRDNV.mjs +149 -0
- package/dist/chunk-PQVBRDNV.mjs.map +1 -0
- package/dist/cli/index.js +115 -51
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2 -1
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/kafka.client/consumer/features/delayed.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts +1 -1
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/features/snapshot.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/handler.d.ts +16 -2
- package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/ops.d.ts +13 -0
- package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/pipeline.d.ts +14 -13
- package/dist/client/kafka.client/consumer/pipeline.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/retry-topic.d.ts +4 -1
- package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/setup.d.ts +3 -0
- package/dist/client/kafka.client/consumer/setup.d.ts.map +1 -1
- package/dist/client/kafka.client/consumer/start.d.ts.map +1 -1
- package/dist/client/kafka.client/context.d.ts +3 -0
- package/dist/client/kafka.client/context.d.ts.map +1 -1
- package/dist/client/kafka.client/index.d.ts.map +1 -1
- package/dist/client/kafka.client/producer/ops.d.ts +12 -3
- package/dist/client/kafka.client/producer/ops.d.ts.map +1 -1
- package/dist/client/kafka.client/producer/send.d.ts +1 -1
- package/dist/client/message/schema-registry.d.ts +23 -4
- package/dist/client/message/schema-registry.d.ts.map +1 -1
- package/dist/client/message/serde.d.ts +68 -0
- package/dist/client/message/serde.d.ts.map +1 -0
- package/dist/client/message/topic.d.ts +25 -4
- package/dist/client/message/topic.d.ts.map +1 -1
- package/dist/client/transport/transport.interface.d.ts +6 -1
- package/dist/client/transport/transport.interface.d.ts.map +1 -1
- package/dist/client/types/config.types.d.ts +17 -0
- package/dist/client/types/config.types.d.ts.map +1 -1
- package/dist/core.d.ts +3 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +146 -55
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +9 -3
- package/dist/index.js +146 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -3
- package/dist/index.mjs.map +1 -1
- package/dist/serde.d.ts +157 -0
- package/dist/serde.d.ts.map +1 -0
- package/dist/serde.js +308 -0
- package/dist/serde.js.map +1 -0
- package/dist/serde.mjs +158 -0
- package/dist/serde.mjs.map +1 -0
- package/package.json +20 -1
- package/dist/chunk-CMO7SMVK.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ConfluentTransport,
|
|
2
3
|
HEADER_CORRELATION_ID,
|
|
3
4
|
HEADER_DELAYED_TARGET,
|
|
4
5
|
HEADER_DELAYED_UNTIL,
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
KafkaProcessingError,
|
|
14
15
|
KafkaRetryExhaustedError,
|
|
15
16
|
KafkaValidationError,
|
|
16
|
-
SchemaRegistryClient,
|
|
17
17
|
awsMskIamProvider,
|
|
18
18
|
buildEnvelopeHeaders,
|
|
19
19
|
consumerOptionsFromEnv,
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
getEnvelopeContext,
|
|
25
25
|
kafkaClientConfigFromEnv,
|
|
26
26
|
mergeConsumerOptions,
|
|
27
|
-
registrySchema,
|
|
28
27
|
resolveSecurityOptions,
|
|
29
28
|
runWithEnvelopeContext,
|
|
30
29
|
startOutboxRelay,
|
|
@@ -33,7 +32,12 @@ import {
|
|
|
33
32
|
toMskIamPolicy,
|
|
34
33
|
topic,
|
|
35
34
|
versionedSchema
|
|
36
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-OR7TPAAE.mjs";
|
|
36
|
+
import {
|
|
37
|
+
JsonSerde,
|
|
38
|
+
SchemaRegistryClient,
|
|
39
|
+
registrySchema
|
|
40
|
+
} from "./chunk-PQVBRDNV.mjs";
|
|
37
41
|
import {
|
|
38
42
|
__decorateClass,
|
|
39
43
|
__decorateParam
|
|
@@ -238,6 +242,7 @@ KafkaHealthIndicator = __decorateClass([
|
|
|
238
242
|
Injectable2()
|
|
239
243
|
], KafkaHealthIndicator);
|
|
240
244
|
export {
|
|
245
|
+
ConfluentTransport,
|
|
241
246
|
HEADER_CORRELATION_ID,
|
|
242
247
|
HEADER_DELAYED_TARGET,
|
|
243
248
|
HEADER_DELAYED_UNTIL,
|
|
@@ -249,6 +254,7 @@ export {
|
|
|
249
254
|
InMemoryDedupStore,
|
|
250
255
|
InMemoryOutboxStore,
|
|
251
256
|
InjectKafkaClient,
|
|
257
|
+
JsonSerde,
|
|
252
258
|
KAFKA_CLIENT,
|
|
253
259
|
KAFKA_SUBSCRIBER_METADATA,
|
|
254
260
|
KafkaClient,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/nest/kafka.module.ts","../src/nest/kafka.constants.ts","../src/nest/kafka.explorer.ts","../src/nest/kafka.decorator.ts","../src/nest/kafka.health.ts"],"sourcesContent":["import { Module, DynamicModule, Provider, Logger } from \"@nestjs/common\";\nimport { DiscoveryModule } from \"@nestjs/core\";\nimport {\n KafkaClient,\n ClientId,\n GroupId,\n TopicMapConstraint,\n KafkaInstrumentation,\n KafkaClientOptions,\n} from \"../client/kafka.client\";\nimport { getKafkaClientToken } from \"./kafka.constants\";\nimport { KafkaExplorer } from \"./kafka.explorer\";\n\n/** Shared configuration fields for both `register()` and `registerAsync()`. */\ninterface KafkaModuleBaseOptions {\n /** Optional name for multi-client setups. Must match `@InjectKafkaClient(name)`. */\n name?: string;\n /** If true, makes KAFKA_CLIENT available globally without importing KafkaModule in every feature module. */\n isGlobal?: boolean;\n}\n\n/** Synchronous configuration for `KafkaModule.register()`. */\nexport interface KafkaModuleOptions extends KafkaModuleBaseOptions {\n /** Unique Kafka client identifier. */\n clientId: ClientId;\n /** Consumer group identifier. */\n groupId: GroupId;\n /** List of Kafka broker addresses. */\n brokers: string[];\n /** Auto-create topics via admin on first use (send/consume). Useful for development. */\n autoCreateTopics?: boolean;\n /** When `true`, string topic keys are validated against any schema previously registered via a TopicDescriptor. Default: `true`. */\n strictSchemas?: boolean;\n /** Number of partitions for auto-created topics. Default: `1`. */\n numPartitions?: number;\n /** Client-wide instrumentation hooks (e.g. OTel). Applied to both send and consume paths. */\n instrumentation?: KafkaInstrumentation[];\n /** Called when a message is dropped without being sent to a DLQ. @see `KafkaClientOptions.onMessageLost` */\n onMessageLost?: KafkaClientOptions[\"onMessageLost\"];\n /** Called whenever a consumer group rebalance occurs. @see `KafkaClientOptions.onRebalance` */\n onRebalance?: KafkaClientOptions[\"onRebalance\"];\n /** Transactional producer ID — must be unique per process/replica. @see `KafkaClientOptions.transactionalId` */\n transactionalId?: KafkaClientOptions[\"transactionalId\"];\n /** Recover the Lamport clock from these topics on startup. @see `KafkaClientOptions.clockRecovery` */\n clockRecovery?: KafkaClientOptions[\"clockRecovery\"];\n /** Delay producer sends when consumer lag exceeds a threshold. @see `KafkaClientOptions.lagThrottle` */\n lagThrottle?: KafkaClientOptions[\"lagThrottle\"];\n /** Client-wide TTL expiry callback. @see `KafkaClientOptions.onTtlExpired` */\n onTtlExpired?: KafkaClientOptions[\"onTtlExpired\"];\n /** Custom transport implementation (e.g. `FakeTransport` in tests). @see `KafkaClientOptions.transport` */\n transport?: KafkaClientOptions[\"transport\"];\n /** Transport security (TLS + SASL, incl. MSK IAM / GCP OAUTHBEARER). @see `KafkaClientOptions.security` */\n security?: KafkaClientOptions[\"security\"];\n}\n\n/** Async configuration for `KafkaModule.registerAsync()` with dependency injection. */\nexport interface KafkaModuleAsyncOptions extends KafkaModuleBaseOptions {\n imports?: any[];\n useFactory: (\n ...args: any[]\n ) => KafkaModuleOptions | Promise<KafkaModuleOptions>;\n inject?: any[];\n}\n\n/**\n * NestJS dynamic module for registering type-safe Kafka clients.\n * Use `register()` for static config or `registerAsync()` for DI-based config.\n */\n@Module({})\nexport class KafkaModule {\n /** Register a Kafka client with static options. */\n static register<T extends TopicMapConstraint<T>>(\n options: KafkaModuleOptions,\n ): DynamicModule {\n const token = getKafkaClientToken(options.name);\n\n const kafkaClientProvider: Provider = {\n provide: token,\n useFactory: () => KafkaModule.buildClient<T>(options),\n };\n\n return {\n global: options.isGlobal ?? false,\n module: KafkaModule,\n imports: [DiscoveryModule],\n providers: [kafkaClientProvider, KafkaExplorer],\n exports: [kafkaClientProvider],\n };\n }\n\n /** Register a Kafka client with async/factory-based options. */\n static registerAsync<T extends TopicMapConstraint<T>>(\n asyncOptions: KafkaModuleAsyncOptions,\n ): DynamicModule {\n const token = getKafkaClientToken(asyncOptions.name);\n\n const kafkaClientProvider: Provider = {\n provide: token,\n useFactory: async (...args: any[]): Promise<KafkaClient<T>> =>\n KafkaModule.buildClient<T>(await asyncOptions.useFactory(...args)),\n inject: asyncOptions.inject || [],\n };\n\n return {\n global: asyncOptions.isGlobal ?? false,\n module: KafkaModule,\n imports: [...(asyncOptions.imports || []), DiscoveryModule],\n providers: [kafkaClientProvider, KafkaExplorer],\n exports: [kafkaClientProvider],\n };\n }\n\n private static async buildClient<T extends TopicMapConstraint<T>>(\n options: KafkaModuleOptions,\n ): Promise<KafkaClient<T>> {\n const client = new KafkaClient<T>(\n options.clientId,\n options.groupId,\n options.brokers,\n {\n autoCreateTopics: options.autoCreateTopics,\n strictSchemas: options.strictSchemas,\n numPartitions: options.numPartitions,\n instrumentation: options.instrumentation,\n onMessageLost: options.onMessageLost,\n onRebalance: options.onRebalance,\n transactionalId: options.transactionalId,\n clockRecovery: options.clockRecovery,\n lagThrottle: options.lagThrottle,\n onTtlExpired: options.onTtlExpired,\n transport: options.transport,\n security: options.security,\n logger: new Logger(`KafkaClient:${options.clientId}`),\n },\n );\n await client.connectProducer();\n return client;\n }\n}\n","/** Default DI token for the Kafka client. */\nexport const KAFKA_CLIENT = \"KAFKA_CLIENT\";\n\n/** Returns the DI token for a named (or default) Kafka client instance. */\nexport const getKafkaClientToken = (name?: string): string =>\n name ? `KAFKA_CLIENT_${name}` : KAFKA_CLIENT;\n","import { Inject, Injectable, OnModuleInit, Logger } from \"@nestjs/common\";\nimport { DiscoveryService, ModuleRef } from \"@nestjs/core\";\nimport { KafkaClient } from \"../client/kafka.client\";\nimport {\n KAFKA_SUBSCRIBER_METADATA,\n KafkaSubscriberMetadata,\n} from \"./kafka.decorator\";\nimport { getKafkaClientToken } from \"./kafka.constants\";\n\ninterface SubscriberEntry extends KafkaSubscriberMetadata {\n methodName: string | symbol;\n}\n\n/**\n * Process-level registry of already-wired subscriptions, keyed by provider\n * instance. Every `KafkaModule.register()` call contributes its own\n * `KafkaExplorer`, and each explorer scans ALL providers — without this guard\n * a multi-client app would wire every `@SubscribeTo` handler once per\n * registered module (duplicate consumers / \"called twice\" startup errors).\n * Keyed by instance (not constructor) so separate Nest apps in one process\n * (e.g. tests) still wire their own instances independently.\n */\nconst wiredSubscriptions = new WeakMap<object, Set<string>>();\n\n/** Discovers `@SubscribeTo()` decorators and wires them to their Kafka clients on startup. */\n@Injectable()\nexport class KafkaExplorer implements OnModuleInit {\n private readonly logger = new Logger(KafkaExplorer.name);\n\n constructor(\n @Inject(DiscoveryService)\n private readonly discoveryService: DiscoveryService,\n @Inject(ModuleRef)\n private readonly moduleRef: ModuleRef,\n ) {}\n\n /**\n * Scan all NestJS providers for `@SubscribeTo()` metadata and wire each decorated\n * method to its Kafka client via `startConsumer` or `startBatchConsumer`.\n *\n * Called automatically by the NestJS lifecycle — do not invoke manually.\n */\n async onModuleInit() {\n const providers = this.discoveryService.getProviders();\n\n for (const wrapper of providers) {\n const { instance } = wrapper;\n if (!instance || typeof instance !== \"object\") continue;\n\n const metadata: SubscriberEntry[] | undefined = Reflect.getMetadata(\n KAFKA_SUBSCRIBER_METADATA,\n instance.constructor,\n );\n\n if (!metadata || metadata.length === 0) continue;\n\n for (const entry of metadata) {\n const token = getKafkaClientToken(entry.clientName);\n\n const entryKey = `${token}:${String(entry.methodName)}`;\n let wired = wiredSubscriptions.get(instance);\n if (!wired) {\n wired = new Set();\n wiredSubscriptions.set(instance, wired);\n }\n if (wired.has(entryKey)) continue; // already wired by another KafkaExplorer instance\n wired.add(entryKey);\n\n let client: KafkaClient<any>;\n\n try {\n client = this.moduleRef.get(token, { strict: false });\n } catch {\n this.logger.error(\n `KafkaClient \"${entry.clientName || \"default\"}\" not found for @SubscribeTo on ${instance.constructor.name}.${String(entry.methodName)}`,\n );\n continue;\n }\n\n const handler = (instance as any)[entry.methodName].bind(instance);\n\n const consumerOptions = { ...entry.options };\n if (entry.schemas) {\n consumerOptions.schemas = entry.schemas;\n }\n\n if (entry.batch) {\n await client.startBatchConsumer(\n entry.topics as any,\n async (envelopes: any[], meta: any) => {\n await handler(envelopes, meta);\n },\n consumerOptions,\n );\n } else {\n await client.startConsumer(\n entry.topics as any,\n async (envelope: any) => {\n await handler(envelope);\n },\n consumerOptions,\n );\n }\n\n this.logger.log(\n `Registered @SubscribeTo(${entry.topics.join(\", \")})${entry.batch ? \" [batch]\" : \"\"} on ${instance.constructor.name}.${String(entry.methodName)}`,\n );\n }\n }\n }\n}\n","import { Inject } from \"@nestjs/common\";\nimport { getKafkaClientToken } from \"./kafka.constants\";\nimport { ConsumerOptions } from \"../client/kafka.client\";\nimport { TopicDescriptor, SchemaLike } from \"../client/message/topic\";\n\n/** Reflect metadata key used to store `@SubscribeTo` entries on a class constructor. */\nexport const KAFKA_SUBSCRIBER_METADATA = \"KAFKA_SUBSCRIBER_METADATA\";\n\n/** Internal shape stored per `@SubscribeTo()` decoration on a class. */\nexport interface KafkaSubscriberMetadata {\n /** Resolved topic name strings (descriptors are unwrapped to their `__topic` string). */\n topics: string[];\n /** Per-topic schema validators extracted from `TopicDescriptor` objects (if any). */\n schemas?: Map<string, SchemaLike>;\n /** Additional consumer options forwarded to `startConsumer` / `startBatchConsumer`. */\n options?: ConsumerOptions;\n /** Named client identifier — resolves to `KAFKA_CLIENT_<clientName>` in the DI container. */\n clientName?: string;\n /** When `true`, routes to `startBatchConsumer` instead of `startConsumer`. */\n batch?: boolean;\n /** Name of the decorated method on the provider class. */\n methodName?: string | symbol;\n}\n\n/** Inject a `KafkaClient` instance. Pass a name to target a specific named client. */\nexport const InjectKafkaClient = (name?: string): ParameterDecorator =>\n Inject(getKafkaClientToken(name));\n\n/**\n * Method decorator that auto-subscribes the decorated method to one or more Kafka topics\n * when the NestJS module initialises.\n *\n * The decorated method receives a fully-decoded `EventEnvelope` for each message\n * (or an array of envelopes + `BatchMeta` when `batch: true`).\n *\n * @param topics One or more topic names or `TopicDescriptor` objects. Schemas embedded in\n * descriptors are automatically extracted and forwarded to the consumer.\n * @param options Consumer and routing options:\n * - All `ConsumerOptions` fields (`groupId`, `retry`, `dlq`, `fromBeginning`, …)\n * - `clientName` — target a named `KafkaClient` (resolves `KAFKA_CLIENT_<name>` from the DI container)\n * - `batch` — use `startBatchConsumer` instead of `startConsumer`\n *\n * @example\n * ```ts\n * @SubscribeTo('orders.created', { groupId: 'orders-svc', retry: { maxRetries: 3 } })\n * async handleOrder(envelope: EventEnvelope<Order>) { ... }\n *\n * @SubscribeTo(OrdersTopic, { batch: true })\n * async handleBatch(envelopes: EventEnvelope<Order>[], meta: BatchMeta) { ... }\n * ```\n */\nexport const SubscribeTo = (\n topics:\n | string\n | string[]\n | TopicDescriptor\n | TopicDescriptor[]\n | (string | TopicDescriptor)[],\n options?: ConsumerOptions & { clientName?: string; batch?: boolean },\n): MethodDecorator => {\n const arr = Array.isArray(topics) ? topics : [topics];\n const topicsArray = arr.map((t) => (typeof t === \"string\" ? t : t.__topic));\n\n // Extract schemas from descriptors that have them\n const schemas = new Map<string, SchemaLike>();\n for (const t of arr) {\n if (typeof t !== \"string\" && t.__schema) {\n schemas.set(t.__topic, t.__schema);\n }\n }\n\n const { clientName, batch, ...consumerOptions } = options || {};\n\n return (target, propertyKey, _descriptor) => {\n const existing: KafkaSubscriberMetadata[] =\n Reflect.getMetadata(KAFKA_SUBSCRIBER_METADATA, target.constructor) || [];\n\n Reflect.defineMetadata(\n KAFKA_SUBSCRIBER_METADATA,\n [\n ...existing,\n {\n topics: topicsArray,\n schemas: schemas.size > 0 ? schemas : undefined,\n options: Object.keys(consumerOptions).length\n ? consumerOptions\n : undefined,\n clientName,\n batch,\n methodName: propertyKey,\n },\n ],\n target.constructor,\n );\n };\n};\n","import { Injectable } from \"@nestjs/common\";\nimport type {\n IKafkaClient,\n KafkaHealthResult,\n TopicMapConstraint,\n} from \"../client/types\";\nexport type { KafkaHealthResult } from \"../client/types\";\n\n/** Health check service. Call `check(client)` to verify broker connectivity. */\n@Injectable()\nexport class KafkaHealthIndicator {\n async check<T extends TopicMapConstraint<T>>(\n client: IKafkaClient<T>,\n ): Promise<KafkaHealthResult> {\n return client.checkStatus();\n }\n}\n"],"mappings":"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","names":["Logger","Inject","Inject","Logger","Injectable","Injectable"]}
|
|
1
|
+
{"version":3,"sources":["../src/nest/kafka.module.ts","../src/nest/kafka.constants.ts","../src/nest/kafka.explorer.ts","../src/nest/kafka.decorator.ts","../src/nest/kafka.health.ts"],"sourcesContent":["import { Module, DynamicModule, Provider, Logger } from \"@nestjs/common\";\nimport { DiscoveryModule } from \"@nestjs/core\";\nimport {\n KafkaClient,\n ClientId,\n GroupId,\n TopicMapConstraint,\n KafkaInstrumentation,\n KafkaClientOptions,\n} from \"../client/kafka.client\";\nimport { getKafkaClientToken } from \"./kafka.constants\";\nimport { KafkaExplorer } from \"./kafka.explorer\";\n\n/** Shared configuration fields for both `register()` and `registerAsync()`. */\ninterface KafkaModuleBaseOptions {\n /** Optional name for multi-client setups. Must match `@InjectKafkaClient(name)`. */\n name?: string;\n /** If true, makes KAFKA_CLIENT available globally without importing KafkaModule in every feature module. */\n isGlobal?: boolean;\n}\n\n/** Synchronous configuration for `KafkaModule.register()`. */\nexport interface KafkaModuleOptions extends KafkaModuleBaseOptions {\n /** Unique Kafka client identifier. */\n clientId: ClientId;\n /** Consumer group identifier. */\n groupId: GroupId;\n /** List of Kafka broker addresses. */\n brokers: string[];\n /** Auto-create topics via admin on first use (send/consume). Useful for development. */\n autoCreateTopics?: boolean;\n /** When `true`, string topic keys are validated against any schema previously registered via a TopicDescriptor. Default: `true`. */\n strictSchemas?: boolean;\n /** Number of partitions for auto-created topics. Default: `1`. */\n numPartitions?: number;\n /** Client-wide instrumentation hooks (e.g. OTel). Applied to both send and consume paths. */\n instrumentation?: KafkaInstrumentation[];\n /** Called when a message is dropped without being sent to a DLQ. @see `KafkaClientOptions.onMessageLost` */\n onMessageLost?: KafkaClientOptions[\"onMessageLost\"];\n /** Called whenever a consumer group rebalance occurs. @see `KafkaClientOptions.onRebalance` */\n onRebalance?: KafkaClientOptions[\"onRebalance\"];\n /** Transactional producer ID — must be unique per process/replica. @see `KafkaClientOptions.transactionalId` */\n transactionalId?: KafkaClientOptions[\"transactionalId\"];\n /** Recover the Lamport clock from these topics on startup. @see `KafkaClientOptions.clockRecovery` */\n clockRecovery?: KafkaClientOptions[\"clockRecovery\"];\n /** Delay producer sends when consumer lag exceeds a threshold. @see `KafkaClientOptions.lagThrottle` */\n lagThrottle?: KafkaClientOptions[\"lagThrottle\"];\n /** Client-wide TTL expiry callback. @see `KafkaClientOptions.onTtlExpired` */\n onTtlExpired?: KafkaClientOptions[\"onTtlExpired\"];\n /** Custom transport implementation (e.g. `FakeTransport` in tests). @see `KafkaClientOptions.transport` */\n transport?: KafkaClientOptions[\"transport\"];\n /** Transport security (TLS + SASL, incl. MSK IAM / GCP OAUTHBEARER). @see `KafkaClientOptions.security` */\n security?: KafkaClientOptions[\"security\"];\n}\n\n/** Async configuration for `KafkaModule.registerAsync()` with dependency injection. */\nexport interface KafkaModuleAsyncOptions extends KafkaModuleBaseOptions {\n imports?: any[];\n useFactory: (\n ...args: any[]\n ) => KafkaModuleOptions | Promise<KafkaModuleOptions>;\n inject?: any[];\n}\n\n/**\n * NestJS dynamic module for registering type-safe Kafka clients.\n * Use `register()` for static config or `registerAsync()` for DI-based config.\n */\n@Module({})\nexport class KafkaModule {\n /** Register a Kafka client with static options. */\n static register<T extends TopicMapConstraint<T>>(\n options: KafkaModuleOptions,\n ): DynamicModule {\n const token = getKafkaClientToken(options.name);\n\n const kafkaClientProvider: Provider = {\n provide: token,\n useFactory: () => KafkaModule.buildClient<T>(options),\n };\n\n return {\n global: options.isGlobal ?? false,\n module: KafkaModule,\n imports: [DiscoveryModule],\n providers: [kafkaClientProvider, KafkaExplorer],\n exports: [kafkaClientProvider],\n };\n }\n\n /** Register a Kafka client with async/factory-based options. */\n static registerAsync<T extends TopicMapConstraint<T>>(\n asyncOptions: KafkaModuleAsyncOptions,\n ): DynamicModule {\n const token = getKafkaClientToken(asyncOptions.name);\n\n const kafkaClientProvider: Provider = {\n provide: token,\n useFactory: async (...args: any[]): Promise<KafkaClient<T>> =>\n KafkaModule.buildClient<T>(await asyncOptions.useFactory(...args)),\n inject: asyncOptions.inject || [],\n };\n\n return {\n global: asyncOptions.isGlobal ?? false,\n module: KafkaModule,\n imports: [...(asyncOptions.imports || []), DiscoveryModule],\n providers: [kafkaClientProvider, KafkaExplorer],\n exports: [kafkaClientProvider],\n };\n }\n\n private static async buildClient<T extends TopicMapConstraint<T>>(\n options: KafkaModuleOptions,\n ): Promise<KafkaClient<T>> {\n const client = new KafkaClient<T>(\n options.clientId,\n options.groupId,\n options.brokers,\n {\n autoCreateTopics: options.autoCreateTopics,\n strictSchemas: options.strictSchemas,\n numPartitions: options.numPartitions,\n instrumentation: options.instrumentation,\n onMessageLost: options.onMessageLost,\n onRebalance: options.onRebalance,\n transactionalId: options.transactionalId,\n clockRecovery: options.clockRecovery,\n lagThrottle: options.lagThrottle,\n onTtlExpired: options.onTtlExpired,\n transport: options.transport,\n security: options.security,\n logger: new Logger(`KafkaClient:${options.clientId}`),\n },\n );\n await client.connectProducer();\n return client;\n }\n}\n","/** Default DI token for the Kafka client. */\nexport const KAFKA_CLIENT = \"KAFKA_CLIENT\";\n\n/** Returns the DI token for a named (or default) Kafka client instance. */\nexport const getKafkaClientToken = (name?: string): string =>\n name ? `KAFKA_CLIENT_${name}` : KAFKA_CLIENT;\n","import { Inject, Injectable, OnModuleInit, Logger } from \"@nestjs/common\";\nimport { DiscoveryService, ModuleRef } from \"@nestjs/core\";\nimport { KafkaClient } from \"../client/kafka.client\";\nimport {\n KAFKA_SUBSCRIBER_METADATA,\n KafkaSubscriberMetadata,\n} from \"./kafka.decorator\";\nimport { getKafkaClientToken } from \"./kafka.constants\";\n\ninterface SubscriberEntry extends KafkaSubscriberMetadata {\n methodName: string | symbol;\n}\n\n/**\n * Process-level registry of already-wired subscriptions, keyed by provider\n * instance. Every `KafkaModule.register()` call contributes its own\n * `KafkaExplorer`, and each explorer scans ALL providers — without this guard\n * a multi-client app would wire every `@SubscribeTo` handler once per\n * registered module (duplicate consumers / \"called twice\" startup errors).\n * Keyed by instance (not constructor) so separate Nest apps in one process\n * (e.g. tests) still wire their own instances independently.\n */\nconst wiredSubscriptions = new WeakMap<object, Set<string>>();\n\n/** Discovers `@SubscribeTo()` decorators and wires them to their Kafka clients on startup. */\n@Injectable()\nexport class KafkaExplorer implements OnModuleInit {\n private readonly logger = new Logger(KafkaExplorer.name);\n\n constructor(\n @Inject(DiscoveryService)\n private readonly discoveryService: DiscoveryService,\n @Inject(ModuleRef)\n private readonly moduleRef: ModuleRef,\n ) {}\n\n /**\n * Scan all NestJS providers for `@SubscribeTo()` metadata and wire each decorated\n * method to its Kafka client via `startConsumer` or `startBatchConsumer`.\n *\n * Called automatically by the NestJS lifecycle — do not invoke manually.\n */\n async onModuleInit() {\n const providers = this.discoveryService.getProviders();\n\n for (const wrapper of providers) {\n const { instance } = wrapper;\n if (!instance || typeof instance !== \"object\") continue;\n\n const metadata: SubscriberEntry[] | undefined = Reflect.getMetadata(\n KAFKA_SUBSCRIBER_METADATA,\n instance.constructor,\n );\n\n if (!metadata || metadata.length === 0) continue;\n\n for (const entry of metadata) {\n const token = getKafkaClientToken(entry.clientName);\n\n const entryKey = `${token}:${String(entry.methodName)}`;\n let wired = wiredSubscriptions.get(instance);\n if (!wired) {\n wired = new Set();\n wiredSubscriptions.set(instance, wired);\n }\n if (wired.has(entryKey)) continue; // already wired by another KafkaExplorer instance\n wired.add(entryKey);\n\n let client: KafkaClient<any>;\n\n try {\n client = this.moduleRef.get(token, { strict: false });\n } catch {\n this.logger.error(\n `KafkaClient \"${entry.clientName || \"default\"}\" not found for @SubscribeTo on ${instance.constructor.name}.${String(entry.methodName)}`,\n );\n continue;\n }\n\n const handler = (instance as any)[entry.methodName].bind(instance);\n\n const consumerOptions = { ...entry.options };\n if (entry.schemas) {\n consumerOptions.schemas = entry.schemas;\n }\n\n if (entry.batch) {\n await client.startBatchConsumer(\n entry.topics as any,\n async (envelopes: any[], meta: any) => {\n await handler(envelopes, meta);\n },\n consumerOptions,\n );\n } else {\n await client.startConsumer(\n entry.topics as any,\n async (envelope: any) => {\n await handler(envelope);\n },\n consumerOptions,\n );\n }\n\n this.logger.log(\n `Registered @SubscribeTo(${entry.topics.join(\", \")})${entry.batch ? \" [batch]\" : \"\"} on ${instance.constructor.name}.${String(entry.methodName)}`,\n );\n }\n }\n }\n}\n","import { Inject } from \"@nestjs/common\";\nimport { getKafkaClientToken } from \"./kafka.constants\";\nimport { ConsumerOptions } from \"../client/kafka.client\";\nimport { TopicDescriptor, SchemaLike } from \"../client/message/topic\";\n\n/** Reflect metadata key used to store `@SubscribeTo` entries on a class constructor. */\nexport const KAFKA_SUBSCRIBER_METADATA = \"KAFKA_SUBSCRIBER_METADATA\";\n\n/** Internal shape stored per `@SubscribeTo()` decoration on a class. */\nexport interface KafkaSubscriberMetadata {\n /** Resolved topic name strings (descriptors are unwrapped to their `__topic` string). */\n topics: string[];\n /** Per-topic schema validators extracted from `TopicDescriptor` objects (if any). */\n schemas?: Map<string, SchemaLike>;\n /** Additional consumer options forwarded to `startConsumer` / `startBatchConsumer`. */\n options?: ConsumerOptions;\n /** Named client identifier — resolves to `KAFKA_CLIENT_<clientName>` in the DI container. */\n clientName?: string;\n /** When `true`, routes to `startBatchConsumer` instead of `startConsumer`. */\n batch?: boolean;\n /** Name of the decorated method on the provider class. */\n methodName?: string | symbol;\n}\n\n/** Inject a `KafkaClient` instance. Pass a name to target a specific named client. */\nexport const InjectKafkaClient = (name?: string): ParameterDecorator =>\n Inject(getKafkaClientToken(name));\n\n/**\n * Method decorator that auto-subscribes the decorated method to one or more Kafka topics\n * when the NestJS module initialises.\n *\n * The decorated method receives a fully-decoded `EventEnvelope` for each message\n * (or an array of envelopes + `BatchMeta` when `batch: true`).\n *\n * @param topics One or more topic names or `TopicDescriptor` objects. Schemas embedded in\n * descriptors are automatically extracted and forwarded to the consumer.\n * @param options Consumer and routing options:\n * - All `ConsumerOptions` fields (`groupId`, `retry`, `dlq`, `fromBeginning`, …)\n * - `clientName` — target a named `KafkaClient` (resolves `KAFKA_CLIENT_<name>` from the DI container)\n * - `batch` — use `startBatchConsumer` instead of `startConsumer`\n *\n * @example\n * ```ts\n * @SubscribeTo('orders.created', { groupId: 'orders-svc', retry: { maxRetries: 3 } })\n * async handleOrder(envelope: EventEnvelope<Order>) { ... }\n *\n * @SubscribeTo(OrdersTopic, { batch: true })\n * async handleBatch(envelopes: EventEnvelope<Order>[], meta: BatchMeta) { ... }\n * ```\n */\nexport const SubscribeTo = (\n topics:\n | string\n | string[]\n | TopicDescriptor\n | TopicDescriptor[]\n | (string | TopicDescriptor)[],\n options?: ConsumerOptions & { clientName?: string; batch?: boolean },\n): MethodDecorator => {\n const arr = Array.isArray(topics) ? topics : [topics];\n const topicsArray = arr.map((t) => (typeof t === \"string\" ? t : t.__topic));\n\n // Extract schemas from descriptors that have them\n const schemas = new Map<string, SchemaLike>();\n for (const t of arr) {\n if (typeof t !== \"string\" && t.__schema) {\n schemas.set(t.__topic, t.__schema);\n }\n }\n\n const { clientName, batch, ...consumerOptions } = options || {};\n\n return (target, propertyKey, _descriptor) => {\n const existing: KafkaSubscriberMetadata[] =\n Reflect.getMetadata(KAFKA_SUBSCRIBER_METADATA, target.constructor) || [];\n\n Reflect.defineMetadata(\n KAFKA_SUBSCRIBER_METADATA,\n [\n ...existing,\n {\n topics: topicsArray,\n schemas: schemas.size > 0 ? schemas : undefined,\n options: Object.keys(consumerOptions).length\n ? consumerOptions\n : undefined,\n clientName,\n batch,\n methodName: propertyKey,\n },\n ],\n target.constructor,\n );\n };\n};\n","import { Injectable } from \"@nestjs/common\";\nimport type {\n IKafkaClient,\n KafkaHealthResult,\n TopicMapConstraint,\n} from \"../client/types\";\nexport type { KafkaHealthResult } from \"../client/types\";\n\n/** Health check service. Call `check(client)` to verify broker connectivity. */\n@Injectable()\nexport class KafkaHealthIndicator {\n async check<T extends TopicMapConstraint<T>>(\n client: IKafkaClient<T>,\n ): Promise<KafkaHealthResult> {\n return client.checkStatus();\n }\n}\n"],"mappings":"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","names":["Logger","Inject","Inject","Logger","Injectable","Injectable"]}
|
package/dist/serde.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { MessageSerde, SerdeContext } from "./client/message/serde";
|
|
2
|
+
import type { SchemaRegistryClient } from "./client/message/schema-registry";
|
|
3
|
+
export { JsonSerde } from "./client/message/serde";
|
|
4
|
+
export type { MessageSerde, SerdeContext } from "./client/message/serde";
|
|
5
|
+
export { SchemaRegistryClient } from "./client/message/schema-registry";
|
|
6
|
+
/** Injectable dynamic-import function — overridable in tests. */
|
|
7
|
+
type ImportFn = (specifier: string) => Promise<any>;
|
|
8
|
+
/** Options common to the registry-backed serdes. */
|
|
9
|
+
interface RegistrySerdeCommonOptions {
|
|
10
|
+
/** Schema Registry client used to resolve/register schema ids. */
|
|
11
|
+
registry: SchemaRegistryClient;
|
|
12
|
+
/**
|
|
13
|
+
* Subject name. Defaults to Confluent `TopicNameStrategy`
|
|
14
|
+
* (`<topic>-value` / `<topic>-key`). Provide a literal string or a
|
|
15
|
+
* function of the {@link SerdeContext} to override.
|
|
16
|
+
*/
|
|
17
|
+
subject?: string | ((ctx: SerdeContext) => string);
|
|
18
|
+
/**
|
|
19
|
+
* Register `schema` on first serialize to obtain its id (dev-friendly).
|
|
20
|
+
* Default `false` → the id is resolved via `getLatestSchema(subject)`.
|
|
21
|
+
*/
|
|
22
|
+
autoRegister?: boolean;
|
|
23
|
+
/** @internal Injectable dynamic import for tests. */
|
|
24
|
+
importFn?: ImportFn;
|
|
25
|
+
}
|
|
26
|
+
/** Options for {@link avroSerde}. */
|
|
27
|
+
export interface AvroSerdeOptions extends RegistrySerdeCommonOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Avro schema (JSON string or object) used to serialize, and as the
|
|
30
|
+
* write-schema fallback. Required to serialize; deserialize resolves the
|
|
31
|
+
* writer schema from the registry via the wire-format id.
|
|
32
|
+
*/
|
|
33
|
+
schema?: string | object;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Confluent-wire-format **Avro** serde backed by a Schema Registry.
|
|
37
|
+
*
|
|
38
|
+
* Produces/consumes the exact byte layout Java/Go clients use, so this library
|
|
39
|
+
* interoperates with them through a shared registry:
|
|
40
|
+
*
|
|
41
|
+
* ```
|
|
42
|
+
* [magic 0x00][schema id: 4-byte big-endian][avro binary]
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Uses the optional peer dependency [`avsc`](https://www.npmjs.com/package/avsc)
|
|
46
|
+
* via dynamic import — install it to enable Avro:
|
|
47
|
+
*
|
|
48
|
+
* ```bash
|
|
49
|
+
* npm install avsc
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* - **serialize**: resolves the subject from the context, obtains the schema id
|
|
53
|
+
* (`registerSchema` when `autoRegister`, else `getLatestSchema`), Avro-encodes
|
|
54
|
+
* `value` against `schema`, and frames the bytes.
|
|
55
|
+
* - **deserialize**: reads the magic byte + big-endian id, resolves the writer
|
|
56
|
+
* schema via `registry.getSchemaById(id)` (cached forever), and Avro-decodes
|
|
57
|
+
* the remainder. The reader schema equals the writer schema in v1 — full
|
|
58
|
+
* reader-schema resolution / schema evolution is a future enhancement.
|
|
59
|
+
*
|
|
60
|
+
* The parsed `avsc` type is cached per schema string so repeated messages don't
|
|
61
|
+
* re-parse the schema.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { avroSerde } from '@drarzter/kafka-client/serde';
|
|
66
|
+
* import { SchemaRegistryClient } from '@drarzter/kafka-client';
|
|
67
|
+
*
|
|
68
|
+
* const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });
|
|
69
|
+
* const orderSchema = {
|
|
70
|
+
* type: 'record',
|
|
71
|
+
* name: 'Order',
|
|
72
|
+
* fields: [{ name: 'orderId', type: 'string' }, { name: 'amount', type: 'double' }],
|
|
73
|
+
* };
|
|
74
|
+
*
|
|
75
|
+
* // Per-topic:
|
|
76
|
+
* const Orders = topic('orders')
|
|
77
|
+
* .serde(avroSerde({ registry, schema: orderSchema }))
|
|
78
|
+
* .type<Order>();
|
|
79
|
+
*
|
|
80
|
+
* // Client-wide:
|
|
81
|
+
* const kafka = new KafkaClient(id, group, brokers, {
|
|
82
|
+
* serde: avroSerde({ registry, schema: orderSchema }),
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function avroSerde(options: AvroSerdeOptions): MessageSerde;
|
|
87
|
+
/** Options for {@link protobufSerde}. */
|
|
88
|
+
export interface ProtobufSerdeOptions extends RegistrySerdeCommonOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Fully-qualified Protobuf message name to encode/decode,
|
|
91
|
+
* e.g. `"com.acme.orders.Order"`.
|
|
92
|
+
*/
|
|
93
|
+
messageType: string;
|
|
94
|
+
/**
|
|
95
|
+
* `.proto` source string defining {@link ProtobufSerdeOptions.messageType}.
|
|
96
|
+
* Required to serialize; deserialize resolves the writer `.proto` from the
|
|
97
|
+
* registry via the wire-format id.
|
|
98
|
+
*/
|
|
99
|
+
schema?: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Confluent-wire-format **Protobuf** serde backed by a Schema Registry.
|
|
103
|
+
*
|
|
104
|
+
* Produces/consumes the exact byte layout Java/Go clients use:
|
|
105
|
+
*
|
|
106
|
+
* ```
|
|
107
|
+
* [magic 0x00][schema id: 4-byte big-endian][message-index][protobuf binary]
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* The **message-index** identifies which message type within the `.proto` file
|
|
111
|
+
* was used. For the first/top-level message type (index `[0]`) Confluent writes
|
|
112
|
+
* the single byte `0x00`. This serde implements that top-level case only —
|
|
113
|
+
* multiple/nested message types are a documented v1 limitation and cause a
|
|
114
|
+
* clear error on deserialize.
|
|
115
|
+
*
|
|
116
|
+
* Uses the optional peer dependency
|
|
117
|
+
* [`protobufjs`](https://www.npmjs.com/package/protobufjs) via dynamic import —
|
|
118
|
+
* install it to enable Protobuf:
|
|
119
|
+
*
|
|
120
|
+
* ```bash
|
|
121
|
+
* npm install protobufjs
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* - **serialize**: obtains the schema id (`registerSchema` when `autoRegister`,
|
|
125
|
+
* else `getLatestSchema`), encodes `value` with the `protobufjs` `Type`, and
|
|
126
|
+
* frames it with the `0x00` message-index byte.
|
|
127
|
+
* - **deserialize**: reads the magic byte + big-endian id + message-index (which
|
|
128
|
+
* must be the single `0x00` byte), resolves the writer `.proto` via
|
|
129
|
+
* `registry.getSchemaById(id)` (cached forever), and decodes the remainder.
|
|
130
|
+
*
|
|
131
|
+
* The parsed `protobufjs` `Type` is cached per schema string so repeated
|
|
132
|
+
* messages don't re-parse the `.proto`.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { protobufSerde } from '@drarzter/kafka-client/serde';
|
|
137
|
+
* import { SchemaRegistryClient } from '@drarzter/kafka-client';
|
|
138
|
+
*
|
|
139
|
+
* const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });
|
|
140
|
+
* const proto = `
|
|
141
|
+
* syntax = "proto3";
|
|
142
|
+
* message Order { string orderId = 1; double amount = 2; }
|
|
143
|
+
* `;
|
|
144
|
+
*
|
|
145
|
+
* // Per-topic:
|
|
146
|
+
* const Orders = topic('orders')
|
|
147
|
+
* .serde(protobufSerde({ registry, schema: proto, messageType: 'Order' }))
|
|
148
|
+
* .type<Order>();
|
|
149
|
+
*
|
|
150
|
+
* // Client-wide:
|
|
151
|
+
* const kafka = new KafkaClient(id, group, brokers, {
|
|
152
|
+
* serde: protobufSerde({ registry, schema: proto, messageType: 'Order' }),
|
|
153
|
+
* });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export declare function protobufSerde(options: ProtobufSerdeOptions): MessageSerde;
|
|
157
|
+
//# sourceMappingURL=serde.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serde.d.ts","sourceRoot":"","sources":["../src/serde.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAI7E,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAKxE,iEAAiE;AACjE,KAAK,QAAQ,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAqEpD,oDAAoD;AACpD,UAAU,0BAA0B;IAClC,kEAAkE;IAClE,QAAQ,EAAE,oBAAoB,CAAC;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,YAAY,KAAK,MAAM,CAAC,CAAC;IACnD;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAiB,SAAQ,0BAA0B;IAClE;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,CA4DjE;AAED,yCAAyC;AACzC,MAAM,WAAW,oBAAqB,SAAQ,0BAA0B;IACtE;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,YAAY,CAiFzE"}
|
package/dist/serde.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/serde.ts
|
|
21
|
+
var serde_exports = {};
|
|
22
|
+
__export(serde_exports, {
|
|
23
|
+
JsonSerde: () => JsonSerde,
|
|
24
|
+
SchemaRegistryClient: () => SchemaRegistryClient,
|
|
25
|
+
avroSerde: () => avroSerde,
|
|
26
|
+
protobufSerde: () => protobufSerde
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(serde_exports);
|
|
29
|
+
var import_node_module = require("module");
|
|
30
|
+
|
|
31
|
+
// src/client/message/serde.ts
|
|
32
|
+
var JsonSerde = class {
|
|
33
|
+
/** JSON-stringify the validated payload. Returns a UTF-8 string. */
|
|
34
|
+
serialize(value) {
|
|
35
|
+
return JSON.stringify(value);
|
|
36
|
+
}
|
|
37
|
+
/** JSON-parse UTF-8 wire bytes into an object. */
|
|
38
|
+
deserialize(data) {
|
|
39
|
+
return JSON.parse(data.toString("utf8"));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/client/message/schema-registry.ts
|
|
44
|
+
var SchemaRegistryClient = class {
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
if (!options.baseUrl) {
|
|
48
|
+
throw new Error("SchemaRegistryClient: baseUrl is required");
|
|
49
|
+
}
|
|
50
|
+
this.fetchFn = options.fetchFn ?? fetch;
|
|
51
|
+
this.cacheTtlMs = options.cacheTtlMs ?? 3e5;
|
|
52
|
+
}
|
|
53
|
+
options;
|
|
54
|
+
fetchFn;
|
|
55
|
+
cacheTtlMs;
|
|
56
|
+
latestCache = /* @__PURE__ */ new Map();
|
|
57
|
+
/**
|
|
58
|
+
* `id → schema` cache. Schema ids are immutable in a Confluent-compatible
|
|
59
|
+
* registry (a given id always maps to the same schema string), so entries
|
|
60
|
+
* are cached for the lifetime of the client with no TTL.
|
|
61
|
+
*/
|
|
62
|
+
byIdCache = /* @__PURE__ */ new Map();
|
|
63
|
+
headers() {
|
|
64
|
+
const h = {
|
|
65
|
+
"Content-Type": "application/vnd.schemaregistry.v1+json"
|
|
66
|
+
};
|
|
67
|
+
if (this.options.auth) {
|
|
68
|
+
const { username, password } = this.options.auth;
|
|
69
|
+
h["Authorization"] = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
|
|
70
|
+
}
|
|
71
|
+
return h;
|
|
72
|
+
}
|
|
73
|
+
async request(method, path, body) {
|
|
74
|
+
const url = `${this.options.baseUrl.replace(/\/$/, "")}${path}`;
|
|
75
|
+
const res = await this.fetchFn(url, {
|
|
76
|
+
method,
|
|
77
|
+
headers: this.headers(),
|
|
78
|
+
...body !== void 0 && { body: JSON.stringify(body) }
|
|
79
|
+
});
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
const text = await res.text().catch(() => "");
|
|
82
|
+
throw new Error(
|
|
83
|
+
`SchemaRegistry ${method} ${path} failed: ${res.status} ${res.statusText}${text ? ` \u2014 ${text}` : ""}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return await res.json();
|
|
87
|
+
}
|
|
88
|
+
/** Fetch the latest schema registered under `subject`. Cached for `cacheTtlMs`. */
|
|
89
|
+
async getLatestSchema(subject) {
|
|
90
|
+
const cached = this.latestCache.get(subject);
|
|
91
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
92
|
+
const raw = await this.request("GET", `/subjects/${encodeURIComponent(subject)}/versions/latest`);
|
|
93
|
+
const value = {
|
|
94
|
+
id: raw.id,
|
|
95
|
+
version: raw.version,
|
|
96
|
+
schema: raw.schema
|
|
97
|
+
};
|
|
98
|
+
this.latestCache.set(subject, {
|
|
99
|
+
value,
|
|
100
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
101
|
+
});
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Fetch a schema by its globally unique registry id (`GET /schemas/ids/{id}`).
|
|
106
|
+
*
|
|
107
|
+
* Used by the Avro/Protobuf serdes on the deserialize path: the writer schema
|
|
108
|
+
* id is read from the Confluent wire-format prefix, then resolved here. Results
|
|
109
|
+
* are cached forever (schema ids are immutable), so a given id triggers exactly
|
|
110
|
+
* one registry round-trip regardless of how many messages reference it.
|
|
111
|
+
*/
|
|
112
|
+
async getSchemaById(id) {
|
|
113
|
+
const cached = this.byIdCache.get(id);
|
|
114
|
+
if (cached) return cached;
|
|
115
|
+
const raw = await this.request(
|
|
116
|
+
"GET",
|
|
117
|
+
`/schemas/ids/${id}`
|
|
118
|
+
);
|
|
119
|
+
const value = { id, schema: raw.schema, schemaType: raw.schemaType };
|
|
120
|
+
this.byIdCache.set(id, value);
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
/** Fetch a specific schema version of a subject. */
|
|
124
|
+
async getSchemaVersion(subject, version) {
|
|
125
|
+
const raw = await this.request(
|
|
126
|
+
"GET",
|
|
127
|
+
`/subjects/${encodeURIComponent(subject)}/versions/${version}`
|
|
128
|
+
);
|
|
129
|
+
return { id: raw.id, version: raw.version, schema: raw.schema };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Register a schema under `subject` (idempotent — re-registering the same
|
|
133
|
+
* schema returns the existing id). Returns the registry-assigned schema id.
|
|
134
|
+
*/
|
|
135
|
+
async registerSchema(subject, schema, schemaType = "JSON") {
|
|
136
|
+
this.latestCache.delete(subject);
|
|
137
|
+
return this.request(
|
|
138
|
+
"POST",
|
|
139
|
+
`/subjects/${encodeURIComponent(subject)}/versions`,
|
|
140
|
+
{ schema, schemaType }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Test `schema` against the subject's compatibility policy without registering.
|
|
145
|
+
* Returns `true` when the registry reports the schema as compatible.
|
|
146
|
+
*/
|
|
147
|
+
async checkCompatibility(subject, schema, schemaType = "JSON") {
|
|
148
|
+
const res = await this.request(
|
|
149
|
+
"POST",
|
|
150
|
+
`/compatibility/subjects/${encodeURIComponent(subject)}/versions/latest`,
|
|
151
|
+
{ schema, schemaType }
|
|
152
|
+
);
|
|
153
|
+
return res.is_compatible;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/serde.ts
|
|
158
|
+
var MAGIC_BYTE = 0;
|
|
159
|
+
var defaultImport = async (specifier) => {
|
|
160
|
+
const base = typeof __filename !== "undefined" ? __filename : `${process.cwd()}/index.js`;
|
|
161
|
+
const req = (0, import_node_module.createRequire)(base);
|
|
162
|
+
const mod = req(specifier);
|
|
163
|
+
return mod?.default ?? mod;
|
|
164
|
+
};
|
|
165
|
+
function resolveSubject(ctx, subject) {
|
|
166
|
+
if (typeof subject === "function") return subject(ctx);
|
|
167
|
+
if (typeof subject === "string") return subject;
|
|
168
|
+
return `${ctx.topic}-${ctx.isKey ? "key" : "value"}`;
|
|
169
|
+
}
|
|
170
|
+
function readWireHeader(data, serdeName) {
|
|
171
|
+
if (data.length < 5) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`${serdeName}: message too short to be Confluent-framed (need >= 5 bytes, got ${data.length}).`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const magic = data[0];
|
|
177
|
+
if (magic !== MAGIC_BYTE) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`${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.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const id = data.readInt32BE(1);
|
|
183
|
+
return { id, offset: 5 };
|
|
184
|
+
}
|
|
185
|
+
function frame(id, ...payloadParts) {
|
|
186
|
+
const header = Buffer.alloc(5);
|
|
187
|
+
header.writeUInt8(MAGIC_BYTE, 0);
|
|
188
|
+
header.writeInt32BE(id, 1);
|
|
189
|
+
return Buffer.concat([header, ...payloadParts]);
|
|
190
|
+
}
|
|
191
|
+
function avroSerde(options) {
|
|
192
|
+
const importFn = options.importFn ?? defaultImport;
|
|
193
|
+
const schemaString = options.schema === void 0 ? void 0 : typeof options.schema === "string" ? options.schema : JSON.stringify(options.schema);
|
|
194
|
+
const typeCache = /* @__PURE__ */ new Map();
|
|
195
|
+
let avscMod;
|
|
196
|
+
async function loadAvsc() {
|
|
197
|
+
if (avscMod) return avscMod;
|
|
198
|
+
try {
|
|
199
|
+
avscMod = await importFn("avsc");
|
|
200
|
+
} catch {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"avroSerde: package 'avsc' is not installed. Run `npm install avsc` to enable Avro serialization."
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return avscMod;
|
|
206
|
+
}
|
|
207
|
+
async function typeFor(schemaStr) {
|
|
208
|
+
const cached = typeCache.get(schemaStr);
|
|
209
|
+
if (cached) return cached;
|
|
210
|
+
const avsc = await loadAvsc();
|
|
211
|
+
const type = avsc.Type.forSchema(JSON.parse(schemaStr));
|
|
212
|
+
typeCache.set(schemaStr, type);
|
|
213
|
+
return type;
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
async serialize(value, ctx) {
|
|
217
|
+
if (schemaString === void 0) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
"avroSerde: `schema` is required to serialize \u2014 pass the Avro schema (JSON string or object) in the serde options."
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const subject = resolveSubject(ctx, options.subject);
|
|
223
|
+
const id = options.autoRegister ? (await options.registry.registerSchema(subject, schemaString, "AVRO")).id : (await options.registry.getLatestSchema(subject)).id;
|
|
224
|
+
const type = await typeFor(schemaString);
|
|
225
|
+
const payload = type.toBuffer(value);
|
|
226
|
+
return frame(id, payload);
|
|
227
|
+
},
|
|
228
|
+
async deserialize(data, _ctx) {
|
|
229
|
+
const { id, offset } = readWireHeader(data, "avroSerde");
|
|
230
|
+
const registered = await options.registry.getSchemaById(id);
|
|
231
|
+
const type = await typeFor(registered.schema);
|
|
232
|
+
return type.fromBuffer(data.subarray(offset));
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function protobufSerde(options) {
|
|
237
|
+
const importFn = options.importFn ?? defaultImport;
|
|
238
|
+
const typeCache = /* @__PURE__ */ new Map();
|
|
239
|
+
let protobufMod;
|
|
240
|
+
async function loadProtobuf() {
|
|
241
|
+
if (protobufMod) return protobufMod;
|
|
242
|
+
try {
|
|
243
|
+
protobufMod = await importFn("protobufjs");
|
|
244
|
+
} catch {
|
|
245
|
+
throw new Error(
|
|
246
|
+
"protobufSerde: package 'protobufjs' is not installed. Run `npm install protobufjs` to enable Protobuf serialization."
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return protobufMod;
|
|
250
|
+
}
|
|
251
|
+
async function typeFor(protoSource) {
|
|
252
|
+
const cacheKey = `${protoSource}::${options.messageType}`;
|
|
253
|
+
const cached = typeCache.get(cacheKey);
|
|
254
|
+
if (cached) return cached;
|
|
255
|
+
const protobuf = await loadProtobuf();
|
|
256
|
+
const parsed = protobuf.parse(protoSource);
|
|
257
|
+
const type = parsed.root.lookupType(options.messageType);
|
|
258
|
+
typeCache.set(cacheKey, type);
|
|
259
|
+
return type;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
async serialize(value, ctx) {
|
|
263
|
+
if (options.schema === void 0) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
"protobufSerde: `schema` is required to serialize \u2014 pass the .proto source string in the serde options."
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const subject = resolveSubject(ctx, options.subject);
|
|
269
|
+
const id = options.autoRegister ? (await options.registry.registerSchema(
|
|
270
|
+
subject,
|
|
271
|
+
options.schema,
|
|
272
|
+
"PROTOBUF"
|
|
273
|
+
)).id : (await options.registry.getLatestSchema(subject)).id;
|
|
274
|
+
const type = await typeFor(options.schema);
|
|
275
|
+
const payload = Buffer.from(
|
|
276
|
+
type.encode(type.create(value)).finish()
|
|
277
|
+
);
|
|
278
|
+
const messageIndex = Buffer.from([0]);
|
|
279
|
+
return frame(id, messageIndex, payload);
|
|
280
|
+
},
|
|
281
|
+
async deserialize(data, _ctx) {
|
|
282
|
+
const { id, offset } = readWireHeader(data, "protobufSerde");
|
|
283
|
+
const indexByte = data[offset];
|
|
284
|
+
if (indexByte !== 0) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`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).`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const registered = await options.registry.getSchemaById(id);
|
|
290
|
+
const type = await typeFor(registered.schema);
|
|
291
|
+
const decoded = type.decode(data.subarray(offset + 1));
|
|
292
|
+
return type.toObject(decoded, {
|
|
293
|
+
longs: String,
|
|
294
|
+
enums: String,
|
|
295
|
+
bytes: Buffer,
|
|
296
|
+
defaults: true
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
302
|
+
0 && (module.exports = {
|
|
303
|
+
JsonSerde,
|
|
304
|
+
SchemaRegistryClient,
|
|
305
|
+
avroSerde,
|
|
306
|
+
protobufSerde
|
|
307
|
+
});
|
|
308
|
+
//# sourceMappingURL=serde.js.map
|