@drarzter/kafka-client 0.2.2 → 0.3.1

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.
@@ -0,0 +1,127 @@
1
+ import "./chunk-EQQGB2QZ.mjs";
2
+
3
+ // src/testing/mock-client.ts
4
+ function detectMockFactory() {
5
+ try {
6
+ const jestFn = eval("typeof jest !== 'undefined' && jest.fn");
7
+ if (typeof jestFn === "function") return () => eval("jest.fn")();
8
+ } catch {
9
+ }
10
+ try {
11
+ const viFn = eval("typeof vi !== 'undefined' && vi.fn");
12
+ if (typeof viFn === "function") return () => eval("vi.fn")();
13
+ } catch {
14
+ }
15
+ throw new Error(
16
+ "createMockKafkaClient: no mock framework detected (jest/vitest). Pass a custom mockFactory."
17
+ );
18
+ }
19
+ function createMockKafkaClient(mockFactory) {
20
+ const fn = mockFactory ?? detectMockFactory();
21
+ const mock = () => fn();
22
+ const resolved = (value) => mock().mockResolvedValue(value);
23
+ const returning = (value) => mock().mockReturnValue(value);
24
+ return {
25
+ checkStatus: resolved({ topics: [] }),
26
+ getClientId: returning("mock-client"),
27
+ sendMessage: resolved(void 0),
28
+ sendBatch: resolved(void 0),
29
+ transaction: mock().mockImplementation(async (cb) => {
30
+ const ctx = {
31
+ send: resolved(void 0),
32
+ sendBatch: resolved(void 0)
33
+ };
34
+ await cb(ctx);
35
+ }),
36
+ startConsumer: resolved(void 0),
37
+ startBatchConsumer: resolved(void 0),
38
+ stopConsumer: resolved(void 0),
39
+ disconnect: resolved(void 0)
40
+ };
41
+ }
42
+
43
+ // src/testing/test-container.ts
44
+ import {
45
+ KafkaContainer
46
+ } from "@testcontainers/kafka";
47
+ import { Kafka, logLevel as KafkaLogLevel } from "kafkajs";
48
+ var KafkaTestContainer = class {
49
+ container;
50
+ image;
51
+ transactionWarmup;
52
+ topics;
53
+ constructor(options) {
54
+ this.image = options?.image ?? "confluentinc/cp-kafka:7.7.0";
55
+ this.transactionWarmup = options?.transactionWarmup ?? true;
56
+ this.topics = options?.topics ?? [];
57
+ }
58
+ /**
59
+ * Start the Kafka container, pre-create topics, and optionally warm up
60
+ * the transaction coordinator.
61
+ *
62
+ * @returns Broker connection strings, e.g. `["localhost:55123"]`.
63
+ */
64
+ async start() {
65
+ this.container = await new KafkaContainer(this.image).withKraft().withExposedPorts(9093).withEnvironment({
66
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: "1",
67
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: "1"
68
+ }).start();
69
+ const host = this.container.getHost();
70
+ const port = this.container.getMappedPort(9093);
71
+ const brokers = [`${host}:${port}`];
72
+ const kafka = new Kafka({
73
+ clientId: "test-container-setup",
74
+ brokers,
75
+ logLevel: KafkaLogLevel.NOTHING
76
+ });
77
+ if (this.topics.length > 0) {
78
+ const admin = kafka.admin();
79
+ await admin.connect();
80
+ await admin.createTopics({
81
+ topics: this.topics.map(
82
+ (t) => typeof t === "string" ? { topic: t, numPartitions: 1 } : { topic: t.topic, numPartitions: t.numPartitions ?? 1 }
83
+ )
84
+ });
85
+ await admin.disconnect();
86
+ }
87
+ if (this.transactionWarmup) {
88
+ const warmupKafka = new Kafka({
89
+ clientId: "test-container-warmup",
90
+ brokers,
91
+ logLevel: KafkaLogLevel.NOTHING,
92
+ retry: { retries: 30, initialRetryTime: 500, maxRetryTime: 3e4 }
93
+ });
94
+ const txProducer = warmupKafka.producer({
95
+ transactionalId: "test-container-warmup-tx",
96
+ idempotent: true,
97
+ maxInFlightRequests: 1
98
+ });
99
+ await txProducer.connect();
100
+ const tx = await txProducer.transaction();
101
+ await tx.abort();
102
+ await txProducer.disconnect();
103
+ }
104
+ return brokers;
105
+ }
106
+ /** Stop and remove the container. */
107
+ async stop() {
108
+ await this.container?.stop();
109
+ this.container = void 0;
110
+ }
111
+ /** Broker connection strings. Throws if container is not started. */
112
+ get brokers() {
113
+ if (!this.container) {
114
+ throw new Error(
115
+ "KafkaTestContainer is not started. Call start() first."
116
+ );
117
+ }
118
+ const host = this.container.getHost();
119
+ const port = this.container.getMappedPort(9093);
120
+ return [`${host}:${port}`];
121
+ }
122
+ };
123
+ export {
124
+ KafkaTestContainer,
125
+ createMockKafkaClient
126
+ };
127
+ //# sourceMappingURL=testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing/mock-client.ts","../src/testing/test-container.ts"],"sourcesContent":["import type { IKafkaClient, TopicMapConstraint } from \"../client/types\";\n\n/**\n * Fully typed mock of `IKafkaClient<T>` where every method is a mock function.\n * Compatible with Jest, Vitest, or any framework whose `fn()` returns\n * an object with `.mock`, `.mockResolvedValue`, etc.\n */\nexport type MockKafkaClient<T extends TopicMapConstraint<T>> = {\n [K in keyof IKafkaClient<T>]: IKafkaClient<T>[K] & Record<string, any>;\n};\n\n/** Factory that creates a no-op mock function (e.g. `() => jest.fn()`). */\nexport type MockFactory = () => (...args: any[]) => any;\n\nfunction detectMockFactory(): MockFactory {\n \n // Jest and Vitest inject globals into the test environment scope,\n // which may not be on `globalThis`. Use eval to check the actual scope.\n try {\n const jestFn = eval(\"typeof jest !== 'undefined' && jest.fn\");\n if (typeof jestFn === \"function\") return () => (eval(\"jest.fn\") as () => (...args: any[]) => any)();\n } catch { /* not available */ }\n try {\n const viFn = eval(\"typeof vi !== 'undefined' && vi.fn\");\n if (typeof viFn === \"function\") return () => (eval(\"vi.fn\") as () => (...args: any[]) => any)();\n } catch { /* not available */ }\n \n\n throw new Error(\n \"createMockKafkaClient: no mock framework detected (jest/vitest). \" +\n \"Pass a custom mockFactory.\",\n );\n}\n\n/**\n * Create a fully typed mock implementing every `IKafkaClient<T>` method.\n * Useful for unit-testing services that depend on `KafkaClient` without\n * touching a real broker.\n *\n * Auto-detects Jest (`jest.fn()`) or Vitest (`vi.fn()`). Pass a custom\n * `mockFactory` for other frameworks.\n *\n * All methods resolve to sensible defaults:\n * - `checkStatus()` → `{ topics: [] }`\n * - `getClientId()` → `\"mock-client\"`\n * - void methods → `undefined`\n *\n * @example\n * ```ts\n * const kafka = createMockKafkaClient<MyTopics>();\n *\n * const service = new OrdersService(kafka);\n * await service.createOrder();\n *\n * expect(kafka.sendMessage).toHaveBeenCalledWith(\n * 'order.created',\n * expect.objectContaining({ orderId: '123' }),\n * );\n * ```\n */\nexport function createMockKafkaClient<T extends TopicMapConstraint<T>>(\n mockFactory?: MockFactory,\n): MockKafkaClient<T> {\n const fn = mockFactory ?? detectMockFactory();\n\n const mock = () => fn() as any;\n const resolved = (value: unknown) => mock().mockResolvedValue(value);\n const returning = (value: unknown) => mock().mockReturnValue(value);\n\n return {\n checkStatus: resolved({ topics: [] }),\n getClientId: returning(\"mock-client\"),\n sendMessage: resolved(undefined),\n sendBatch: resolved(undefined),\n transaction: mock().mockImplementation(async (cb: (ctx: Record<string, unknown>) => Promise<void>) => {\n const ctx = {\n send: resolved(undefined),\n sendBatch: resolved(undefined),\n };\n await cb(ctx);\n }),\n startConsumer: resolved(undefined),\n startBatchConsumer: resolved(undefined),\n stopConsumer: resolved(undefined),\n disconnect: resolved(undefined),\n } as unknown as MockKafkaClient<T>;\n}\n","import {\n KafkaContainer,\n type StartedKafkaContainer,\n} from \"@testcontainers/kafka\";\nimport { Kafka, logLevel as KafkaLogLevel } from \"kafkajs\";\n\n/** Options for `KafkaTestContainer`. */\nexport interface KafkaTestContainerOptions {\n /** Docker image. Default: `\"confluentinc/cp-kafka:7.7.0\"`. */\n image?: string;\n /** Warm up the transactional coordinator on start. Default: `true`. */\n transactionWarmup?: boolean;\n /** Topics to pre-create. Each entry can be a string (1 partition) or `{ topic, numPartitions }`. */\n topics?: Array<string | { topic: string; numPartitions?: number }>;\n}\n\n/**\n * Thin wrapper around `@testcontainers/kafka` that starts a single-node\n * KRaft Kafka container and exposes `brokers` for use with `KafkaClient`.\n *\n * Handles common setup pain points:\n * - Transaction coordinator warmup (avoids transactional producer hangs)\n * - Topic pre-creation (avoids race conditions)\n *\n * @example\n * ```ts\n * const container = new KafkaTestContainer({ topics: ['orders', 'payments'] });\n * const brokers = await container.start();\n *\n * const kafka = new KafkaClient('test', 'test-group', brokers);\n * // ... run tests ...\n *\n * await container.stop();\n * ```\n *\n * @example Jest lifecycle\n * ```ts\n * let container: KafkaTestContainer;\n * let brokers: string[];\n *\n * beforeAll(async () => {\n * container = new KafkaTestContainer({ topics: ['orders'] });\n * brokers = await container.start();\n * }, 120_000);\n *\n * afterAll(() => container.stop());\n * ```\n */\nexport class KafkaTestContainer {\n private container: StartedKafkaContainer | undefined;\n private readonly image: string;\n private readonly transactionWarmup: boolean;\n private readonly topics: Array<\n string | { topic: string; numPartitions?: number }\n >;\n\n constructor(options?: KafkaTestContainerOptions) {\n this.image = options?.image ?? \"confluentinc/cp-kafka:7.7.0\";\n this.transactionWarmup = options?.transactionWarmup ?? true;\n this.topics = options?.topics ?? [];\n }\n\n /**\n * Start the Kafka container, pre-create topics, and optionally warm up\n * the transaction coordinator.\n *\n * @returns Broker connection strings, e.g. `[\"localhost:55123\"]`.\n */\n async start(): Promise<string[]> {\n this.container = await new KafkaContainer(this.image)\n .withKraft()\n .withExposedPorts(9093)\n .withEnvironment({\n KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: \"1\",\n KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: \"1\",\n })\n .start();\n\n const host = this.container.getHost();\n const port = this.container.getMappedPort(9093);\n const brokers = [`${host}:${port}`];\n\n const kafka = new Kafka({\n clientId: \"test-container-setup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n });\n\n if (this.topics.length > 0) {\n const admin = kafka.admin();\n await admin.connect();\n await admin.createTopics({\n topics: this.topics.map((t) =>\n typeof t === \"string\"\n ? { topic: t, numPartitions: 1 }\n : { topic: t.topic, numPartitions: t.numPartitions ?? 1 },\n ),\n });\n await admin.disconnect();\n }\n\n if (this.transactionWarmup) {\n const warmupKafka = new Kafka({\n clientId: \"test-container-warmup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n retry: { retries: 30, initialRetryTime: 500, maxRetryTime: 30000 },\n });\n const txProducer = warmupKafka.producer({\n transactionalId: \"test-container-warmup-tx\",\n idempotent: true,\n maxInFlightRequests: 1,\n });\n await txProducer.connect();\n const tx = await txProducer.transaction();\n await tx.abort();\n await txProducer.disconnect();\n }\n\n return brokers;\n }\n\n /** Stop and remove the container. */\n async stop(): Promise<void> {\n await this.container?.stop();\n this.container = undefined;\n }\n\n /** Broker connection strings. Throws if container is not started. */\n get brokers(): string[] {\n if (!this.container) {\n throw new Error(\n \"KafkaTestContainer is not started. Call start() first.\",\n );\n }\n const host = this.container.getHost();\n const port = this.container.getMappedPort(9093);\n return [`${host}:${port}`];\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Any validation library with a `.parse()` method.
3
+ * Works with Zod, Valibot, ArkType, or any custom validator.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { z } from 'zod';
8
+ * const schema: SchemaLike<{ id: string }> = z.object({ id: z.string() });
9
+ * ```
10
+ */
11
+ interface SchemaLike<T = any> {
12
+ parse(data: unknown): T;
13
+ }
14
+ /** Infer the output type from a SchemaLike. */
15
+ type InferSchema<S extends SchemaLike> = S extends SchemaLike<infer T> ? T : never;
16
+ /**
17
+ * A typed topic descriptor that pairs a topic name with its message type.
18
+ * Created via the `topic()` factory function.
19
+ *
20
+ * @typeParam N - The literal topic name string.
21
+ * @typeParam M - The message payload type for this topic.
22
+ */
23
+ interface TopicDescriptor<N extends string = string, M extends Record<string, any> = Record<string, any>> {
24
+ readonly __topic: N;
25
+ /** @internal Phantom type — never has a real value at runtime. */
26
+ readonly __type: M;
27
+ /** Runtime schema validator. Present only when created via `topic().schema()`. */
28
+ readonly __schema?: SchemaLike<M>;
29
+ }
30
+ /**
31
+ * Define a typed topic descriptor.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * // Without schema — type provided explicitly:
36
+ * const OrderCreated = topic('order.created')<{ orderId: string; amount: number }>();
37
+ *
38
+ * // With schema — type inferred from schema:
39
+ * const OrderCreated = topic('order.created').schema(z.object({
40
+ * orderId: z.string(),
41
+ * amount: z.number(),
42
+ * }));
43
+ *
44
+ * // Use with KafkaClient:
45
+ * await kafka.sendMessage(OrderCreated, { orderId: '123', amount: 100 });
46
+ *
47
+ * // Use with @SubscribeTo:
48
+ * @SubscribeTo(OrderCreated)
49
+ * async handleOrder(msg) { ... }
50
+ * ```
51
+ */
52
+ declare function topic<N extends string>(name: N): {
53
+ <M extends Record<string, any>>(): TopicDescriptor<N, M>;
54
+ schema<S extends SchemaLike<Record<string, any>>>(schema: S): TopicDescriptor<N, InferSchema<S>>;
55
+ };
56
+ /**
57
+ * Build a topic-message map type from a union of TopicDescriptors.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const OrderCreated = topic('order.created')<{ orderId: string }>();
62
+ * const OrderCompleted = topic('order.completed')<{ completedAt: string }>();
63
+ *
64
+ * type MyTopics = TopicsFrom<typeof OrderCreated | typeof OrderCompleted>;
65
+ * // { 'order.created': { orderId: string }; 'order.completed': { completedAt: string } }
66
+ * ```
67
+ */
68
+ type TopicsFrom<D extends TopicDescriptor<any, any>> = {
69
+ [K in D as K["__topic"]]: K["__type"];
70
+ };
71
+
72
+ /**
73
+ * Mapping of topic names to their message types.
74
+ * Define this interface to get type-safe publish/subscribe across your app.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * // with explicit extends (IDE hints for values)
79
+ * interface MyTopics extends TTopicMessageMap {
80
+ * "orders.created": { orderId: string; amount: number };
81
+ * "users.updated": { userId: string; name: string };
82
+ * }
83
+ *
84
+ * // or plain interface / type — works the same
85
+ * interface MyTopics {
86
+ * "orders.created": { orderId: string; amount: number };
87
+ * }
88
+ * ```
89
+ */
90
+ type TTopicMessageMap = {
91
+ [topic: string]: Record<string, any>;
92
+ };
93
+ /**
94
+ * Generic constraint for topic-message maps.
95
+ * Works with both `type` aliases and `interface` declarations.
96
+ */
97
+ type TopicMapConstraint<T> = {
98
+ [K in keyof T]: Record<string, any>;
99
+ };
100
+ type ClientId = string;
101
+ type GroupId = string;
102
+ type MessageHeaders = Record<string, string>;
103
+ /** Options for sending a single message. */
104
+ interface SendOptions {
105
+ /** Partition key for message routing. */
106
+ key?: string;
107
+ /** Custom headers attached to the message. */
108
+ headers?: MessageHeaders;
109
+ }
110
+ /** Metadata exposed to batch consumer handlers. */
111
+ interface BatchMeta {
112
+ /** Partition number for this batch. */
113
+ partition: number;
114
+ /** Highest offset available on the broker for this partition. */
115
+ highWatermark: string;
116
+ /** Send a heartbeat to the broker to prevent session timeout. */
117
+ heartbeat(): Promise<void>;
118
+ /** Mark an offset as processed (for manual offset management). */
119
+ resolveOffset(offset: string): void;
120
+ /** Commit offsets if the auto-commit threshold has been reached. */
121
+ commitOffsetsIfNecessary(): Promise<void>;
122
+ }
123
+ /** Options for configuring a Kafka consumer. */
124
+ interface ConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> {
125
+ /** Override the default consumer group ID from the constructor. */
126
+ groupId?: string;
127
+ /** Start reading from earliest offset. Default: `false`. */
128
+ fromBeginning?: boolean;
129
+ /** Automatically commit offsets. Default: `true`. */
130
+ autoCommit?: boolean;
131
+ /** Retry policy for failed message processing. */
132
+ retry?: RetryOptions;
133
+ /** Send failed messages to a Dead Letter Queue (`<topic>.dlq`). */
134
+ dlq?: boolean;
135
+ /** Interceptors called before/after each message. */
136
+ interceptors?: ConsumerInterceptor<T>[];
137
+ /** @internal Schema map populated by @SubscribeTo when descriptors have schemas. */
138
+ schemas?: Map<string, SchemaLike>;
139
+ /** Retry config for `consumer.subscribe()` when the topic doesn't exist yet. */
140
+ subscribeRetry?: SubscribeRetryOptions;
141
+ }
142
+ /** Configuration for consumer retry behavior. */
143
+ interface RetryOptions {
144
+ /** Maximum number of retry attempts before giving up. */
145
+ maxRetries: number;
146
+ /** Base delay between retries in ms (multiplied by attempt number). Default: `1000`. */
147
+ backoffMs?: number;
148
+ }
149
+ /**
150
+ * Interceptor hooks for consumer message processing.
151
+ * All methods are optional — implement only what you need.
152
+ */
153
+ interface ConsumerInterceptor<T extends TopicMapConstraint<T> = TTopicMessageMap> {
154
+ /** Called before the message handler. */
155
+ before?(message: T[keyof T], topic: string): Promise<void> | void;
156
+ /** Called after the message handler succeeds. */
157
+ after?(message: T[keyof T], topic: string): Promise<void> | void;
158
+ /** Called when the message handler throws. */
159
+ onError?(message: T[keyof T], topic: string, error: Error): Promise<void> | void;
160
+ }
161
+ /** Context passed to the `transaction()` callback with type-safe send methods. */
162
+ interface TransactionContext<T extends TopicMapConstraint<T>> {
163
+ send<K extends keyof T>(topic: K, message: T[K], options?: SendOptions): Promise<void>;
164
+ send<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(descriptor: D, message: D["__type"], options?: SendOptions): Promise<void>;
165
+ sendBatch<K extends keyof T>(topic: K, messages: Array<{
166
+ value: T[K];
167
+ key?: string;
168
+ headers?: MessageHeaders;
169
+ }>): Promise<void>;
170
+ sendBatch<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(descriptor: D, messages: Array<{
171
+ value: D["__type"];
172
+ key?: string;
173
+ headers?: MessageHeaders;
174
+ }>): Promise<void>;
175
+ }
176
+ /** Interface describing all public methods of the Kafka client. */
177
+ interface IKafkaClient<T extends TopicMapConstraint<T>> {
178
+ checkStatus(): Promise<{
179
+ topics: string[];
180
+ }>;
181
+ startConsumer<K extends Array<keyof T>>(topics: K, handleMessage: (message: T[K[number]], topic: K[number]) => Promise<void>, options?: ConsumerOptions<T>): Promise<void>;
182
+ startConsumer<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(topics: D[], handleMessage: (message: D["__type"], topic: D["__topic"]) => Promise<void>, options?: ConsumerOptions<T>): Promise<void>;
183
+ startBatchConsumer<K extends Array<keyof T>>(topics: K, handleBatch: (messages: T[K[number]][], topic: K[number], meta: BatchMeta) => Promise<void>, options?: ConsumerOptions<T>): Promise<void>;
184
+ startBatchConsumer<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(topics: D[], handleBatch: (messages: D["__type"][], topic: D["__topic"], meta: BatchMeta) => Promise<void>, options?: ConsumerOptions<T>): Promise<void>;
185
+ stopConsumer(): Promise<void>;
186
+ sendMessage<K extends keyof T>(topic: K, message: T[K], options?: SendOptions): Promise<void>;
187
+ sendBatch<K extends keyof T>(topic: K, messages: Array<{
188
+ value: T[K];
189
+ key?: string;
190
+ headers?: MessageHeaders;
191
+ }>): Promise<void>;
192
+ transaction(fn: (ctx: TransactionContext<T>) => Promise<void>): Promise<void>;
193
+ getClientId: () => ClientId;
194
+ disconnect(): Promise<void>;
195
+ }
196
+ /**
197
+ * Logger interface for KafkaClient.
198
+ * Compatible with NestJS Logger, console, winston, pino, or any custom logger.
199
+ */
200
+ interface KafkaLogger {
201
+ log(message: string): void;
202
+ warn(message: string, ...args: any[]): void;
203
+ error(message: string, ...args: any[]): void;
204
+ }
205
+ /** Options for `KafkaClient` constructor. */
206
+ interface KafkaClientOptions {
207
+ /** Auto-create topics via admin before the first `sendMessage`, `sendBatch`, or `transaction` for each topic. Useful for development — not recommended in production. */
208
+ autoCreateTopics?: boolean;
209
+ /** When `true`, string topic keys are validated against any schema previously registered via a TopicDescriptor. Default: `true`. */
210
+ strictSchemas?: boolean;
211
+ /** Custom logger. Defaults to console with `[KafkaClient:<clientId>]` prefix. */
212
+ logger?: KafkaLogger;
213
+ /** Number of partitions for auto-created topics. Default: `1`. */
214
+ numPartitions?: number;
215
+ }
216
+ /** Options for consumer subscribe retry when topic doesn't exist yet. */
217
+ interface SubscribeRetryOptions {
218
+ /** Maximum number of subscribe attempts. Default: `5`. */
219
+ retries?: number;
220
+ /** Delay between retries in ms. Default: `5000`. */
221
+ backoffMs?: number;
222
+ }
223
+
224
+ export { type BatchMeta as B, type ClientId as C, type GroupId as G, type IKafkaClient as I, type KafkaClientOptions as K, type MessageHeaders as M, type RetryOptions as R, type SchemaLike as S, type TopicMapConstraint as T, type ConsumerOptions as a, type TopicDescriptor as b, type ConsumerInterceptor as c, type InferSchema as d, type KafkaLogger as e, type SendOptions as f, type SubscribeRetryOptions as g, type TTopicMessageMap as h, type TopicsFrom as i, type TransactionContext as j, topic as t };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Any validation library with a `.parse()` method.
3
+ * Works with Zod, Valibot, ArkType, or any custom validator.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { z } from 'zod';
8
+ * const schema: SchemaLike<{ id: string }> = z.object({ id: z.string() });
9
+ * ```
10
+ */
11
+ interface SchemaLike<T = any> {
12
+ parse(data: unknown): T;
13
+ }
14
+ /** Infer the output type from a SchemaLike. */
15
+ type InferSchema<S extends SchemaLike> = S extends SchemaLike<infer T> ? T : never;
16
+ /**
17
+ * A typed topic descriptor that pairs a topic name with its message type.
18
+ * Created via the `topic()` factory function.
19
+ *
20
+ * @typeParam N - The literal topic name string.
21
+ * @typeParam M - The message payload type for this topic.
22
+ */
23
+ interface TopicDescriptor<N extends string = string, M extends Record<string, any> = Record<string, any>> {
24
+ readonly __topic: N;
25
+ /** @internal Phantom type — never has a real value at runtime. */
26
+ readonly __type: M;
27
+ /** Runtime schema validator. Present only when created via `topic().schema()`. */
28
+ readonly __schema?: SchemaLike<M>;
29
+ }
30
+ /**
31
+ * Define a typed topic descriptor.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * // Without schema — type provided explicitly:
36
+ * const OrderCreated = topic('order.created')<{ orderId: string; amount: number }>();
37
+ *
38
+ * // With schema — type inferred from schema:
39
+ * const OrderCreated = topic('order.created').schema(z.object({
40
+ * orderId: z.string(),
41
+ * amount: z.number(),
42
+ * }));
43
+ *
44
+ * // Use with KafkaClient:
45
+ * await kafka.sendMessage(OrderCreated, { orderId: '123', amount: 100 });
46
+ *
47
+ * // Use with @SubscribeTo:
48
+ * @SubscribeTo(OrderCreated)
49
+ * async handleOrder(msg) { ... }
50
+ * ```
51
+ */
52
+ declare function topic<N extends string>(name: N): {
53
+ <M extends Record<string, any>>(): TopicDescriptor<N, M>;
54
+ schema<S extends SchemaLike<Record<string, any>>>(schema: S): TopicDescriptor<N, InferSchema<S>>;
55
+ };
56
+ /**
57
+ * Build a topic-message map type from a union of TopicDescriptors.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const OrderCreated = topic('order.created')<{ orderId: string }>();
62
+ * const OrderCompleted = topic('order.completed')<{ completedAt: string }>();
63
+ *
64
+ * type MyTopics = TopicsFrom<typeof OrderCreated | typeof OrderCompleted>;
65
+ * // { 'order.created': { orderId: string }; 'order.completed': { completedAt: string } }
66
+ * ```
67
+ */
68
+ type TopicsFrom<D extends TopicDescriptor<any, any>> = {
69
+ [K in D as K["__topic"]]: K["__type"];
70
+ };
71
+
72
+ /**
73
+ * Mapping of topic names to their message types.
74
+ * Define this interface to get type-safe publish/subscribe across your app.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * // with explicit extends (IDE hints for values)
79
+ * interface MyTopics extends TTopicMessageMap {
80
+ * "orders.created": { orderId: string; amount: number };
81
+ * "users.updated": { userId: string; name: string };
82
+ * }
83
+ *
84
+ * // or plain interface / type — works the same
85
+ * interface MyTopics {
86
+ * "orders.created": { orderId: string; amount: number };
87
+ * }
88
+ * ```
89
+ */
90
+ type TTopicMessageMap = {
91
+ [topic: string]: Record<string, any>;
92
+ };
93
+ /**
94
+ * Generic constraint for topic-message maps.
95
+ * Works with both `type` aliases and `interface` declarations.
96
+ */
97
+ type TopicMapConstraint<T> = {
98
+ [K in keyof T]: Record<string, any>;
99
+ };
100
+ type ClientId = string;
101
+ type GroupId = string;
102
+ type MessageHeaders = Record<string, string>;
103
+ /** Options for sending a single message. */
104
+ interface SendOptions {
105
+ /** Partition key for message routing. */
106
+ key?: string;
107
+ /** Custom headers attached to the message. */
108
+ headers?: MessageHeaders;
109
+ }
110
+ /** Metadata exposed to batch consumer handlers. */
111
+ interface BatchMeta {
112
+ /** Partition number for this batch. */
113
+ partition: number;
114
+ /** Highest offset available on the broker for this partition. */
115
+ highWatermark: string;
116
+ /** Send a heartbeat to the broker to prevent session timeout. */
117
+ heartbeat(): Promise<void>;
118
+ /** Mark an offset as processed (for manual offset management). */
119
+ resolveOffset(offset: string): void;
120
+ /** Commit offsets if the auto-commit threshold has been reached. */
121
+ commitOffsetsIfNecessary(): Promise<void>;
122
+ }
123
+ /** Options for configuring a Kafka consumer. */
124
+ interface ConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> {
125
+ /** Override the default consumer group ID from the constructor. */
126
+ groupId?: string;
127
+ /** Start reading from earliest offset. Default: `false`. */
128
+ fromBeginning?: boolean;
129
+ /** Automatically commit offsets. Default: `true`. */
130
+ autoCommit?: boolean;
131
+ /** Retry policy for failed message processing. */
132
+ retry?: RetryOptions;
133
+ /** Send failed messages to a Dead Letter Queue (`<topic>.dlq`). */
134
+ dlq?: boolean;
135
+ /** Interceptors called before/after each message. */
136
+ interceptors?: ConsumerInterceptor<T>[];
137
+ /** @internal Schema map populated by @SubscribeTo when descriptors have schemas. */
138
+ schemas?: Map<string, SchemaLike>;
139
+ /** Retry config for `consumer.subscribe()` when the topic doesn't exist yet. */
140
+ subscribeRetry?: SubscribeRetryOptions;
141
+ }
142
+ /** Configuration for consumer retry behavior. */
143
+ interface RetryOptions {
144
+ /** Maximum number of retry attempts before giving up. */
145
+ maxRetries: number;
146
+ /** Base delay between retries in ms (multiplied by attempt number). Default: `1000`. */
147
+ backoffMs?: number;
148
+ }
149
+ /**
150
+ * Interceptor hooks for consumer message processing.
151
+ * All methods are optional — implement only what you need.
152
+ */
153
+ interface ConsumerInterceptor<T extends TopicMapConstraint<T> = TTopicMessageMap> {
154
+ /** Called before the message handler. */
155
+ before?(message: T[keyof T], topic: string): Promise<void> | void;
156
+ /** Called after the message handler succeeds. */
157
+ after?(message: T[keyof T], topic: string): Promise<void> | void;
158
+ /** Called when the message handler throws. */
159
+ onError?(message: T[keyof T], topic: string, error: Error): Promise<void> | void;
160
+ }
161
+ /** Context passed to the `transaction()` callback with type-safe send methods. */
162
+ interface TransactionContext<T extends TopicMapConstraint<T>> {
163
+ send<K extends keyof T>(topic: K, message: T[K], options?: SendOptions): Promise<void>;
164
+ send<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(descriptor: D, message: D["__type"], options?: SendOptions): Promise<void>;
165
+ sendBatch<K extends keyof T>(topic: K, messages: Array<{
166
+ value: T[K];
167
+ key?: string;
168
+ headers?: MessageHeaders;
169
+ }>): Promise<void>;
170
+ sendBatch<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(descriptor: D, messages: Array<{
171
+ value: D["__type"];
172
+ key?: string;
173
+ headers?: MessageHeaders;
174
+ }>): Promise<void>;
175
+ }
176
+ /** Interface describing all public methods of the Kafka client. */
177
+ interface IKafkaClient<T extends TopicMapConstraint<T>> {
178
+ checkStatus(): Promise<{
179
+ topics: string[];
180
+ }>;
181
+ startConsumer<K extends Array<keyof T>>(topics: K, handleMessage: (message: T[K[number]], topic: K[number]) => Promise<void>, options?: ConsumerOptions<T>): Promise<void>;
182
+ startConsumer<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(topics: D[], handleMessage: (message: D["__type"], topic: D["__topic"]) => Promise<void>, options?: ConsumerOptions<T>): Promise<void>;
183
+ startBatchConsumer<K extends Array<keyof T>>(topics: K, handleBatch: (messages: T[K[number]][], topic: K[number], meta: BatchMeta) => Promise<void>, options?: ConsumerOptions<T>): Promise<void>;
184
+ startBatchConsumer<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(topics: D[], handleBatch: (messages: D["__type"][], topic: D["__topic"], meta: BatchMeta) => Promise<void>, options?: ConsumerOptions<T>): Promise<void>;
185
+ stopConsumer(): Promise<void>;
186
+ sendMessage<K extends keyof T>(topic: K, message: T[K], options?: SendOptions): Promise<void>;
187
+ sendBatch<K extends keyof T>(topic: K, messages: Array<{
188
+ value: T[K];
189
+ key?: string;
190
+ headers?: MessageHeaders;
191
+ }>): Promise<void>;
192
+ transaction(fn: (ctx: TransactionContext<T>) => Promise<void>): Promise<void>;
193
+ getClientId: () => ClientId;
194
+ disconnect(): Promise<void>;
195
+ }
196
+ /**
197
+ * Logger interface for KafkaClient.
198
+ * Compatible with NestJS Logger, console, winston, pino, or any custom logger.
199
+ */
200
+ interface KafkaLogger {
201
+ log(message: string): void;
202
+ warn(message: string, ...args: any[]): void;
203
+ error(message: string, ...args: any[]): void;
204
+ }
205
+ /** Options for `KafkaClient` constructor. */
206
+ interface KafkaClientOptions {
207
+ /** Auto-create topics via admin before the first `sendMessage`, `sendBatch`, or `transaction` for each topic. Useful for development — not recommended in production. */
208
+ autoCreateTopics?: boolean;
209
+ /** When `true`, string topic keys are validated against any schema previously registered via a TopicDescriptor. Default: `true`. */
210
+ strictSchemas?: boolean;
211
+ /** Custom logger. Defaults to console with `[KafkaClient:<clientId>]` prefix. */
212
+ logger?: KafkaLogger;
213
+ /** Number of partitions for auto-created topics. Default: `1`. */
214
+ numPartitions?: number;
215
+ }
216
+ /** Options for consumer subscribe retry when topic doesn't exist yet. */
217
+ interface SubscribeRetryOptions {
218
+ /** Maximum number of subscribe attempts. Default: `5`. */
219
+ retries?: number;
220
+ /** Delay between retries in ms. Default: `5000`. */
221
+ backoffMs?: number;
222
+ }
223
+
224
+ export { type BatchMeta as B, type ClientId as C, type GroupId as G, type IKafkaClient as I, type KafkaClientOptions as K, type MessageHeaders as M, type RetryOptions as R, type SchemaLike as S, type TopicMapConstraint as T, type ConsumerOptions as a, type TopicDescriptor as b, type ConsumerInterceptor as c, type InferSchema as d, type KafkaLogger as e, type SendOptions as f, type SubscribeRetryOptions as g, type TTopicMessageMap as h, type TopicsFrom as i, type TransactionContext as j, topic as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drarzter/kafka-client",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
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",
@@ -11,6 +11,22 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.mjs",
13
13
  "require": "./dist/index.js"
14
+ },
15
+ "./core": {
16
+ "types": "./dist/core.d.ts",
17
+ "import": "./dist/core.mjs",
18
+ "require": "./dist/core.js"
19
+ },
20
+ "./testing": {
21
+ "types": "./dist/testing.d.ts",
22
+ "import": "./dist/testing.mjs",
23
+ "require": "./dist/testing.js"
24
+ }
25
+ },
26
+ "typesVersions": {
27
+ "*": {
28
+ "core": ["./dist/core.d.ts"],
29
+ "testing": ["./dist/testing.d.ts"]
14
30
  }
15
31
  },
16
32
  "files": [
@@ -45,6 +61,12 @@
45
61
  "reflect-metadata": ">=0.1.13",
46
62
  "rxjs": ">=7.0.0"
47
63
  },
64
+ "peerDependenciesMeta": {
65
+ "@nestjs/common": { "optional": true },
66
+ "@nestjs/core": { "optional": true },
67
+ "reflect-metadata": { "optional": true },
68
+ "rxjs": { "optional": true }
69
+ },
48
70
  "devDependencies": {
49
71
  "@nestjs/common": "^11.1.13",
50
72
  "@nestjs/core": "^11.1.13",