@drarzter/kafka-client 0.7.0 → 0.7.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.
package/dist/index.mjs CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  getEnvelopeContext,
16
16
  runWithEnvelopeContext,
17
17
  topic
18
- } from "./chunk-MJ342P4R.mjs";
18
+ } from "./chunk-AMEGMOZH.mjs";
19
19
  import {
20
20
  __decorateClass,
21
21
  __decorateParam
package/dist/otel.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { K as KafkaInstrumentation } from './types-DqQ7IXZr.mjs';
1
+ import { K as KafkaInstrumentation } from './types-BEIGjmV6.mjs';
2
2
 
3
3
  /**
4
4
  * Create a `KafkaInstrumentation` that automatically propagates
package/dist/otel.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { K as KafkaInstrumentation } from './types-DqQ7IXZr.js';
1
+ import { K as KafkaInstrumentation } from './types-BEIGjmV6.js';
2
2
 
3
3
  /**
4
4
  * Create a `KafkaInstrumentation` that automatically propagates
@@ -1,4 +1,4 @@
1
- import { T as TopicMapConstraint, I as IKafkaClient } from './types-DqQ7IXZr.mjs';
1
+ import { T as TopicMapConstraint, I as IKafkaClient } from './types-BEIGjmV6.mjs';
2
2
 
3
3
  /**
4
4
  * Fully typed mock of `IKafkaClient<T>` where every method is a mock function.
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as TopicMapConstraint, I as IKafkaClient } from './types-DqQ7IXZr.js';
1
+ import { T as TopicMapConstraint, I as IKafkaClient } from './types-BEIGjmV6.js';
2
2
 
3
3
  /**
4
4
  * Fully typed mock of `IKafkaClient<T>` where every method is a mock function.
package/dist/testing.js CHANGED
@@ -83,6 +83,8 @@ function createMockKafkaClient(mockFactory) {
83
83
  replayDlq: resolved({ replayed: 0, skipped: 0 }),
84
84
  resetOffsets: resolved(void 0),
85
85
  seekToOffset: resolved(void 0),
86
+ seekToTimestamp: resolved(void 0),
87
+ getCircuitState: returning(void 0),
86
88
  pauseConsumer: mock(),
87
89
  resumeConsumer: mock(),
88
90
  getMetrics: returning({
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing.ts","../src/testing/mock-client.ts","../src/testing/test-container.ts"],"sourcesContent":["export * from \"./testing/index\";\n","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 // Jest and Vitest inject their globals (`jest` / `vi`) as module-scope\n // bindings, not as properties of `globalThis`. The only reliable way to\n // detect them without a hard import is via `eval`, which evaluates in the\n // current module scope where those bindings are available.\n try {\n if (eval(\"typeof jest === 'object' && typeof jest.fn === 'function'\")) {\n return () => eval(\"jest.fn()\");\n }\n } catch {\n /* not available */\n }\n try {\n if (eval(\"typeof vi === 'object' && typeof vi.fn === 'function'\")) {\n return () => eval(\"vi.fn()\");\n }\n } catch {\n /* not available */\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()` → `{ status: 'up', clientId: 'mock-client', 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({\n status: \"up\",\n clientId: \"mock-client\",\n topics: [],\n }),\n getConsumerLag: resolved([]),\n getClientId: returning(\"mock-client\"),\n sendMessage: resolved(undefined),\n sendBatch: resolved(undefined),\n transaction: mock().mockImplementation(\n 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 ),\n startConsumer: resolved({\n groupId: \"mock-group\",\n stop: mock().mockResolvedValue(undefined),\n }),\n startBatchConsumer: resolved({\n groupId: \"mock-group\",\n stop: mock().mockResolvedValue(undefined),\n }),\n stopConsumer: resolved(undefined),\n consume: returning(\n (function* () {})() as unknown as AsyncIterableIterator<any>,\n ),\n replayDlq: resolved({ replayed: 0, skipped: 0 }),\n resetOffsets: resolved(undefined),\n seekToOffset: resolved(undefined),\n pauseConsumer: mock(),\n resumeConsumer: mock(),\n getMetrics: returning({\n processedCount: 0,\n retryCount: 0,\n dlqCount: 0,\n dedupCount: 0,\n }),\n resetMetrics: mock(),\n disconnect: resolved(undefined),\n enableGracefulShutdown: mock(),\n } as unknown as MockKafkaClient<T>;\n}\n","import {\n KafkaContainer,\n type StartedKafkaContainer,\n} from \"@testcontainers/kafka\";\nimport { KafkaJS } from \"@confluentinc/kafka-javascript\";\nconst { Kafka, logLevel: KafkaLogLevel } = 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 kafkaJS: {\n clientId: \"test-container-setup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n },\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 kafkaJS: {\n clientId: \"test-container-warmup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n },\n });\n const txProducer = warmupKafka.producer({\n kafkaJS: {\n transactionalId: \"test-container-warmup-tx\",\n idempotent: true,\n maxInFlightRequests: 1,\n },\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(\"KafkaTestContainer is not started. Call start() first.\");\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":[]}
1
+ {"version":3,"sources":["../src/testing.ts","../src/testing/mock-client.ts","../src/testing/test-container.ts"],"sourcesContent":["export * from \"./testing/index\";\n","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 // Jest and Vitest inject their globals (`jest` / `vi`) as module-scope\n // bindings, not as properties of `globalThis`. The only reliable way to\n // detect them without a hard import is via `eval`, which evaluates in the\n // current module scope where those bindings are available.\n try {\n if (eval(\"typeof jest === 'object' && typeof jest.fn === 'function'\")) {\n return () => eval(\"jest.fn()\");\n }\n } catch {\n /* not available */\n }\n try {\n if (eval(\"typeof vi === 'object' && typeof vi.fn === 'function'\")) {\n return () => eval(\"vi.fn()\");\n }\n } catch {\n /* not available */\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()` → `{ status: 'up', clientId: 'mock-client', 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({\n status: \"up\",\n clientId: \"mock-client\",\n topics: [],\n }),\n getConsumerLag: resolved([]),\n getClientId: returning(\"mock-client\"),\n sendMessage: resolved(undefined),\n sendBatch: resolved(undefined),\n transaction: mock().mockImplementation(\n 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 ),\n startConsumer: resolved({\n groupId: \"mock-group\",\n stop: mock().mockResolvedValue(undefined),\n }),\n startBatchConsumer: resolved({\n groupId: \"mock-group\",\n stop: mock().mockResolvedValue(undefined),\n }),\n stopConsumer: resolved(undefined),\n consume: returning(\n (function* () {})() as unknown as AsyncIterableIterator<any>,\n ),\n replayDlq: resolved({ replayed: 0, skipped: 0 }),\n resetOffsets: resolved(undefined),\n seekToOffset: resolved(undefined),\n seekToTimestamp: resolved(undefined),\n getCircuitState: returning(undefined),\n pauseConsumer: mock(),\n resumeConsumer: mock(),\n getMetrics: returning({\n processedCount: 0,\n retryCount: 0,\n dlqCount: 0,\n dedupCount: 0,\n }),\n resetMetrics: mock(),\n disconnect: resolved(undefined),\n enableGracefulShutdown: mock(),\n } as unknown as MockKafkaClient<T>;\n}\n","import {\n KafkaContainer,\n type StartedKafkaContainer,\n} from \"@testcontainers/kafka\";\nimport { KafkaJS } from \"@confluentinc/kafka-javascript\";\nconst { Kafka, logLevel: KafkaLogLevel } = 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 kafkaJS: {\n clientId: \"test-container-setup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n },\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 kafkaJS: {\n clientId: \"test-container-warmup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n },\n });\n const txProducer = warmupKafka.producer({\n kafkaJS: {\n transactionalId: \"test-container-warmup-tx\",\n idempotent: true,\n maxInFlightRequests: 1,\n },\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(\"KafkaTestContainer is not started. Call start() first.\");\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":[]}
package/dist/testing.mjs CHANGED
@@ -58,6 +58,8 @@ function createMockKafkaClient(mockFactory) {
58
58
  replayDlq: resolved({ replayed: 0, skipped: 0 }),
59
59
  resetOffsets: resolved(void 0),
60
60
  seekToOffset: resolved(void 0),
61
+ seekToTimestamp: resolved(void 0),
62
+ getCircuitState: returning(void 0),
61
63
  pauseConsumer: mock(),
62
64
  resumeConsumer: mock(),
63
65
  getMetrics: returning({
@@ -1 +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 // Jest and Vitest inject their globals (`jest` / `vi`) as module-scope\n // bindings, not as properties of `globalThis`. The only reliable way to\n // detect them without a hard import is via `eval`, which evaluates in the\n // current module scope where those bindings are available.\n try {\n if (eval(\"typeof jest === 'object' && typeof jest.fn === 'function'\")) {\n return () => eval(\"jest.fn()\");\n }\n } catch {\n /* not available */\n }\n try {\n if (eval(\"typeof vi === 'object' && typeof vi.fn === 'function'\")) {\n return () => eval(\"vi.fn()\");\n }\n } catch {\n /* not available */\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()` → `{ status: 'up', clientId: 'mock-client', 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({\n status: \"up\",\n clientId: \"mock-client\",\n topics: [],\n }),\n getConsumerLag: resolved([]),\n getClientId: returning(\"mock-client\"),\n sendMessage: resolved(undefined),\n sendBatch: resolved(undefined),\n transaction: mock().mockImplementation(\n 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 ),\n startConsumer: resolved({\n groupId: \"mock-group\",\n stop: mock().mockResolvedValue(undefined),\n }),\n startBatchConsumer: resolved({\n groupId: \"mock-group\",\n stop: mock().mockResolvedValue(undefined),\n }),\n stopConsumer: resolved(undefined),\n consume: returning(\n (function* () {})() as unknown as AsyncIterableIterator<any>,\n ),\n replayDlq: resolved({ replayed: 0, skipped: 0 }),\n resetOffsets: resolved(undefined),\n seekToOffset: resolved(undefined),\n pauseConsumer: mock(),\n resumeConsumer: mock(),\n getMetrics: returning({\n processedCount: 0,\n retryCount: 0,\n dlqCount: 0,\n dedupCount: 0,\n }),\n resetMetrics: mock(),\n disconnect: resolved(undefined),\n enableGracefulShutdown: mock(),\n } as unknown as MockKafkaClient<T>;\n}\n","import {\n KafkaContainer,\n type StartedKafkaContainer,\n} from \"@testcontainers/kafka\";\nimport { KafkaJS } from \"@confluentinc/kafka-javascript\";\nconst { Kafka, logLevel: KafkaLogLevel } = 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 kafkaJS: {\n clientId: \"test-container-setup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n },\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 kafkaJS: {\n clientId: \"test-container-warmup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n },\n });\n const txProducer = warmupKafka.producer({\n kafkaJS: {\n transactionalId: \"test-container-warmup-tx\",\n idempotent: true,\n maxInFlightRequests: 1,\n },\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(\"KafkaTestContainer is not started. Call start() first.\");\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":[]}
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 // Jest and Vitest inject their globals (`jest` / `vi`) as module-scope\n // bindings, not as properties of `globalThis`. The only reliable way to\n // detect them without a hard import is via `eval`, which evaluates in the\n // current module scope where those bindings are available.\n try {\n if (eval(\"typeof jest === 'object' && typeof jest.fn === 'function'\")) {\n return () => eval(\"jest.fn()\");\n }\n } catch {\n /* not available */\n }\n try {\n if (eval(\"typeof vi === 'object' && typeof vi.fn === 'function'\")) {\n return () => eval(\"vi.fn()\");\n }\n } catch {\n /* not available */\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()` → `{ status: 'up', clientId: 'mock-client', 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({\n status: \"up\",\n clientId: \"mock-client\",\n topics: [],\n }),\n getConsumerLag: resolved([]),\n getClientId: returning(\"mock-client\"),\n sendMessage: resolved(undefined),\n sendBatch: resolved(undefined),\n transaction: mock().mockImplementation(\n 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 ),\n startConsumer: resolved({\n groupId: \"mock-group\",\n stop: mock().mockResolvedValue(undefined),\n }),\n startBatchConsumer: resolved({\n groupId: \"mock-group\",\n stop: mock().mockResolvedValue(undefined),\n }),\n stopConsumer: resolved(undefined),\n consume: returning(\n (function* () {})() as unknown as AsyncIterableIterator<any>,\n ),\n replayDlq: resolved({ replayed: 0, skipped: 0 }),\n resetOffsets: resolved(undefined),\n seekToOffset: resolved(undefined),\n seekToTimestamp: resolved(undefined),\n getCircuitState: returning(undefined),\n pauseConsumer: mock(),\n resumeConsumer: mock(),\n getMetrics: returning({\n processedCount: 0,\n retryCount: 0,\n dlqCount: 0,\n dedupCount: 0,\n }),\n resetMetrics: mock(),\n disconnect: resolved(undefined),\n enableGracefulShutdown: mock(),\n } as unknown as MockKafkaClient<T>;\n}\n","import {\n KafkaContainer,\n type StartedKafkaContainer,\n} from \"@testcontainers/kafka\";\nimport { KafkaJS } from \"@confluentinc/kafka-javascript\";\nconst { Kafka, logLevel: KafkaLogLevel } = 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 kafkaJS: {\n clientId: \"test-container-setup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n },\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 kafkaJS: {\n clientId: \"test-container-warmup\",\n brokers,\n logLevel: KafkaLogLevel.NOTHING,\n },\n });\n const txProducer = warmupKafka.producer({\n kafkaJS: {\n transactionalId: \"test-container-warmup-tx\",\n idempotent: true,\n maxInFlightRequests: 1,\n },\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(\"KafkaTestContainer is not started. Call start() first.\");\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":[]}
@@ -366,7 +366,7 @@ interface ConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> {
366
366
  * the `x-timestamp` header set by the producer.
367
367
  *
368
368
  * Expired messages are routed to `{topic}.dlq` when `dlq: true`, otherwise
369
- * `onMessageLost` is called. The handler is never invoked for an expired message.
369
+ * `onTtlExpired` is called. The handler is never invoked for an expired message.
370
370
  */
371
371
  messageTtlMs?: number;
372
372
  /**
@@ -376,6 +376,12 @@ interface ConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> {
376
376
  * See `CircuitBreakerOptions` for the sliding-window semantics.
377
377
  */
378
378
  circuitBreaker?: CircuitBreakerOptions;
379
+ /**
380
+ * Max number of messages buffered in the `consume()` iterator queue before
381
+ * the partition is paused. The partition resumes when the queue drains below 50%.
382
+ * Only applies to `consume()`. Default: unbounded.
383
+ */
384
+ queueHighWaterMark?: number;
379
385
  }
380
386
  /** Configuration for consumer retry behavior. */
381
387
  interface RetryOptions {
@@ -497,6 +503,12 @@ interface KafkaInstrumentation {
497
503
  * Fires for both single-message and batch consumers (once per envelope).
498
504
  */
499
505
  onMessage?(envelope: EventEnvelope<any>): void;
506
+ /** Called when a partition circuit opens (consumer paused due to DLQ failures). */
507
+ onCircuitOpen?(topic: string, partition: number): void;
508
+ /** Called when the circuit moves to half-open (partition resumed for a probe). */
509
+ onCircuitHalfOpen?(topic: string, partition: number): void;
510
+ /** Called when the circuit closes (normal operation restored). */
511
+ onCircuitClose?(topic: string, partition: number): void;
500
512
  }
501
513
  /** Context passed to the `transaction()` callback with type-safe send methods. */
502
514
  interface TransactionContext<T extends TopicMapConstraint<T>> {
@@ -606,6 +618,36 @@ interface IKafkaClient<T extends TopicMapConstraint<T>> {
606
618
  partition: number;
607
619
  offset: string;
608
620
  }>): Promise<void>;
621
+ /**
622
+ * Seek specific partitions to the offset nearest to a given Unix timestamp (ms).
623
+ * Uses `admin.fetchTopicOffsetsByTime` under the hood. Falls back to `-1` (end of
624
+ * topic) when no offset exists at the requested timestamp (e.g. empty partition or
625
+ * future timestamp).
626
+ *
627
+ * The consumer group must be inactive. Call `stopConsumer(groupId)` first.
628
+ *
629
+ * @param groupId Consumer group to seek. Defaults to the client's default groupId.
630
+ * @param assignments Array of `{ topic, partition, timestamp }` tuples (Unix ms).
631
+ */
632
+ seekToTimestamp(groupId: string | undefined, assignments: Array<{
633
+ topic: string;
634
+ partition: number;
635
+ timestamp: number;
636
+ }>): Promise<void>;
637
+ /**
638
+ * Returns the current circuit breaker state for a specific topic partition.
639
+ * Returns `undefined` when no circuit state exists — either `circuitBreaker` is not
640
+ * configured for the group, or the circuit has never been tripped.
641
+ *
642
+ * @param topic Topic name.
643
+ * @param partition Partition index.
644
+ * @param groupId Consumer group. Defaults to the client's default groupId.
645
+ */
646
+ getCircuitState(topic: string, partition: number, groupId?: string): {
647
+ status: "closed" | "open" | "half-open";
648
+ failures: number;
649
+ windowSize: number;
650
+ } | undefined;
609
651
  /**
610
652
  * Consume messages as an async iterator. Useful for scripts, migrations, and
611
653
  * one-off processing where the full `startConsumer` lifecycle is unnecessary.
@@ -667,6 +709,20 @@ interface KafkaLogger {
667
709
  error(message: string, ...args: any[]): void;
668
710
  debug?(message: string, ...args: any[]): void;
669
711
  }
712
+ /**
713
+ * Context passed to `onTtlExpired` when a message is dropped because it
714
+ * exceeded `messageTtlMs` and `dlq` is not enabled.
715
+ */
716
+ interface TtlExpiredContext {
717
+ /** Topic the message was consumed from. */
718
+ topic: string;
719
+ /** Actual age of the message in ms at the time it was dropped. */
720
+ ageMs: number;
721
+ /** The configured TTL threshold (`messageTtlMs`). */
722
+ messageTtlMs: number;
723
+ /** Original Kafka message headers (correlationId, traceparent, etc.). */
724
+ headers: MessageHeaders;
725
+ }
670
726
  /**
671
727
  * Context passed to `onMessageLost` when a message is silently dropped
672
728
  * (handler threw and `dlq` is not enabled).
@@ -710,6 +766,12 @@ interface KafkaClientOptions {
710
766
  * Use this to alert, log to external systems, or trigger fallback logic.
711
767
  */
712
768
  onMessageLost?: (ctx: MessageLostContext) => void | Promise<void>;
769
+ /**
770
+ * Called when a message is dropped due to TTL expiration (`messageTtlMs`).
771
+ * Fires instead of `onMessageLost` for expired messages when `dlq` is not enabled.
772
+ * When `dlq: true`, expired messages go to the DLQ and this callback is NOT called.
773
+ */
774
+ onTtlExpired?: (ctx: TtlExpiredContext) => void | Promise<void>;
713
775
  /**
714
776
  * Called whenever a consumer group rebalance occurs.
715
777
  * - `'assign'` — new partitions were granted to this instance.
@@ -731,4 +793,4 @@ interface SubscribeRetryOptions {
731
793
  backoffMs?: number;
732
794
  }
733
795
 
734
- export { type TransactionContext as A, type BatchMessageItem as B, type ClientId as C, type DeduplicationOptions as D, type EnvelopeHeaderOptions as E, buildEnvelopeHeaders as F, type GroupId as G, HEADER_CORRELATION_ID as H, type IKafkaClient as I, decodeHeaders as J, type KafkaInstrumentation as K, extractEnvelope as L, type MessageHeaders as M, getEnvelopeContext as N, runWithEnvelopeContext as O, topic as P, type RetryOptions as R, type SchemaLike as S, type TopicMapConstraint as T, type KafkaClientOptions as a, type ConsumerOptions as b, type TopicDescriptor as c, type KafkaHealthResult as d, type BatchMeta as e, type BeforeConsumeResult as f, type CircuitBreakerOptions as g, type ConsumerHandle as h, type ConsumerInterceptor as i, type DlqReason as j, type DlqReplayOptions as k, type EventEnvelope as l, HEADER_EVENT_ID as m, HEADER_LAMPORT_CLOCK as n, HEADER_SCHEMA_VERSION as o, HEADER_TIMESTAMP as p, HEADER_TRACEPARENT as q, type InferSchema as r, type KafkaLogger as s, type KafkaMetrics as t, type MessageLostContext as u, type SchemaParseContext as v, type SendOptions as w, type SubscribeRetryOptions as x, type TTopicMessageMap as y, type TopicsFrom as z };
796
+ export { type TransactionContext as A, type BatchMessageItem as B, type ClientId as C, type DeduplicationOptions as D, type EnvelopeHeaderOptions as E, type TtlExpiredContext as F, type GroupId as G, HEADER_CORRELATION_ID as H, type IKafkaClient as I, buildEnvelopeHeaders as J, type KafkaInstrumentation as K, decodeHeaders as L, type MessageHeaders as M, extractEnvelope as N, getEnvelopeContext as O, runWithEnvelopeContext as P, topic as Q, type RetryOptions as R, type SchemaLike as S, type TopicMapConstraint as T, type KafkaClientOptions as a, type ConsumerOptions as b, type TopicDescriptor as c, type KafkaHealthResult as d, type BatchMeta as e, type BeforeConsumeResult as f, type CircuitBreakerOptions as g, type ConsumerHandle as h, type ConsumerInterceptor as i, type DlqReason as j, type DlqReplayOptions as k, type EventEnvelope as l, HEADER_EVENT_ID as m, HEADER_LAMPORT_CLOCK as n, HEADER_SCHEMA_VERSION as o, HEADER_TIMESTAMP as p, HEADER_TRACEPARENT as q, type InferSchema as r, type KafkaLogger as s, type KafkaMetrics as t, type MessageLostContext as u, type SchemaParseContext as v, type SendOptions as w, type SubscribeRetryOptions as x, type TTopicMessageMap as y, type TopicsFrom as z };
@@ -366,7 +366,7 @@ interface ConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> {
366
366
  * the `x-timestamp` header set by the producer.
367
367
  *
368
368
  * Expired messages are routed to `{topic}.dlq` when `dlq: true`, otherwise
369
- * `onMessageLost` is called. The handler is never invoked for an expired message.
369
+ * `onTtlExpired` is called. The handler is never invoked for an expired message.
370
370
  */
371
371
  messageTtlMs?: number;
372
372
  /**
@@ -376,6 +376,12 @@ interface ConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> {
376
376
  * See `CircuitBreakerOptions` for the sliding-window semantics.
377
377
  */
378
378
  circuitBreaker?: CircuitBreakerOptions;
379
+ /**
380
+ * Max number of messages buffered in the `consume()` iterator queue before
381
+ * the partition is paused. The partition resumes when the queue drains below 50%.
382
+ * Only applies to `consume()`. Default: unbounded.
383
+ */
384
+ queueHighWaterMark?: number;
379
385
  }
380
386
  /** Configuration for consumer retry behavior. */
381
387
  interface RetryOptions {
@@ -497,6 +503,12 @@ interface KafkaInstrumentation {
497
503
  * Fires for both single-message and batch consumers (once per envelope).
498
504
  */
499
505
  onMessage?(envelope: EventEnvelope<any>): void;
506
+ /** Called when a partition circuit opens (consumer paused due to DLQ failures). */
507
+ onCircuitOpen?(topic: string, partition: number): void;
508
+ /** Called when the circuit moves to half-open (partition resumed for a probe). */
509
+ onCircuitHalfOpen?(topic: string, partition: number): void;
510
+ /** Called when the circuit closes (normal operation restored). */
511
+ onCircuitClose?(topic: string, partition: number): void;
500
512
  }
501
513
  /** Context passed to the `transaction()` callback with type-safe send methods. */
502
514
  interface TransactionContext<T extends TopicMapConstraint<T>> {
@@ -606,6 +618,36 @@ interface IKafkaClient<T extends TopicMapConstraint<T>> {
606
618
  partition: number;
607
619
  offset: string;
608
620
  }>): Promise<void>;
621
+ /**
622
+ * Seek specific partitions to the offset nearest to a given Unix timestamp (ms).
623
+ * Uses `admin.fetchTopicOffsetsByTime` under the hood. Falls back to `-1` (end of
624
+ * topic) when no offset exists at the requested timestamp (e.g. empty partition or
625
+ * future timestamp).
626
+ *
627
+ * The consumer group must be inactive. Call `stopConsumer(groupId)` first.
628
+ *
629
+ * @param groupId Consumer group to seek. Defaults to the client's default groupId.
630
+ * @param assignments Array of `{ topic, partition, timestamp }` tuples (Unix ms).
631
+ */
632
+ seekToTimestamp(groupId: string | undefined, assignments: Array<{
633
+ topic: string;
634
+ partition: number;
635
+ timestamp: number;
636
+ }>): Promise<void>;
637
+ /**
638
+ * Returns the current circuit breaker state for a specific topic partition.
639
+ * Returns `undefined` when no circuit state exists — either `circuitBreaker` is not
640
+ * configured for the group, or the circuit has never been tripped.
641
+ *
642
+ * @param topic Topic name.
643
+ * @param partition Partition index.
644
+ * @param groupId Consumer group. Defaults to the client's default groupId.
645
+ */
646
+ getCircuitState(topic: string, partition: number, groupId?: string): {
647
+ status: "closed" | "open" | "half-open";
648
+ failures: number;
649
+ windowSize: number;
650
+ } | undefined;
609
651
  /**
610
652
  * Consume messages as an async iterator. Useful for scripts, migrations, and
611
653
  * one-off processing where the full `startConsumer` lifecycle is unnecessary.
@@ -667,6 +709,20 @@ interface KafkaLogger {
667
709
  error(message: string, ...args: any[]): void;
668
710
  debug?(message: string, ...args: any[]): void;
669
711
  }
712
+ /**
713
+ * Context passed to `onTtlExpired` when a message is dropped because it
714
+ * exceeded `messageTtlMs` and `dlq` is not enabled.
715
+ */
716
+ interface TtlExpiredContext {
717
+ /** Topic the message was consumed from. */
718
+ topic: string;
719
+ /** Actual age of the message in ms at the time it was dropped. */
720
+ ageMs: number;
721
+ /** The configured TTL threshold (`messageTtlMs`). */
722
+ messageTtlMs: number;
723
+ /** Original Kafka message headers (correlationId, traceparent, etc.). */
724
+ headers: MessageHeaders;
725
+ }
670
726
  /**
671
727
  * Context passed to `onMessageLost` when a message is silently dropped
672
728
  * (handler threw and `dlq` is not enabled).
@@ -710,6 +766,12 @@ interface KafkaClientOptions {
710
766
  * Use this to alert, log to external systems, or trigger fallback logic.
711
767
  */
712
768
  onMessageLost?: (ctx: MessageLostContext) => void | Promise<void>;
769
+ /**
770
+ * Called when a message is dropped due to TTL expiration (`messageTtlMs`).
771
+ * Fires instead of `onMessageLost` for expired messages when `dlq` is not enabled.
772
+ * When `dlq: true`, expired messages go to the DLQ and this callback is NOT called.
773
+ */
774
+ onTtlExpired?: (ctx: TtlExpiredContext) => void | Promise<void>;
713
775
  /**
714
776
  * Called whenever a consumer group rebalance occurs.
715
777
  * - `'assign'` — new partitions were granted to this instance.
@@ -731,4 +793,4 @@ interface SubscribeRetryOptions {
731
793
  backoffMs?: number;
732
794
  }
733
795
 
734
- export { type TransactionContext as A, type BatchMessageItem as B, type ClientId as C, type DeduplicationOptions as D, type EnvelopeHeaderOptions as E, buildEnvelopeHeaders as F, type GroupId as G, HEADER_CORRELATION_ID as H, type IKafkaClient as I, decodeHeaders as J, type KafkaInstrumentation as K, extractEnvelope as L, type MessageHeaders as M, getEnvelopeContext as N, runWithEnvelopeContext as O, topic as P, type RetryOptions as R, type SchemaLike as S, type TopicMapConstraint as T, type KafkaClientOptions as a, type ConsumerOptions as b, type TopicDescriptor as c, type KafkaHealthResult as d, type BatchMeta as e, type BeforeConsumeResult as f, type CircuitBreakerOptions as g, type ConsumerHandle as h, type ConsumerInterceptor as i, type DlqReason as j, type DlqReplayOptions as k, type EventEnvelope as l, HEADER_EVENT_ID as m, HEADER_LAMPORT_CLOCK as n, HEADER_SCHEMA_VERSION as o, HEADER_TIMESTAMP as p, HEADER_TRACEPARENT as q, type InferSchema as r, type KafkaLogger as s, type KafkaMetrics as t, type MessageLostContext as u, type SchemaParseContext as v, type SendOptions as w, type SubscribeRetryOptions as x, type TTopicMessageMap as y, type TopicsFrom as z };
796
+ export { type TransactionContext as A, type BatchMessageItem as B, type ClientId as C, type DeduplicationOptions as D, type EnvelopeHeaderOptions as E, type TtlExpiredContext as F, type GroupId as G, HEADER_CORRELATION_ID as H, type IKafkaClient as I, buildEnvelopeHeaders as J, type KafkaInstrumentation as K, decodeHeaders as L, type MessageHeaders as M, extractEnvelope as N, getEnvelopeContext as O, runWithEnvelopeContext as P, topic as Q, type RetryOptions as R, type SchemaLike as S, type TopicMapConstraint as T, type KafkaClientOptions as a, type ConsumerOptions as b, type TopicDescriptor as c, type KafkaHealthResult as d, type BatchMeta as e, type BeforeConsumeResult as f, type CircuitBreakerOptions as g, type ConsumerHandle as h, type ConsumerInterceptor as i, type DlqReason as j, type DlqReplayOptions as k, type EventEnvelope as l, HEADER_EVENT_ID as m, HEADER_LAMPORT_CLOCK as n, HEADER_SCHEMA_VERSION as o, HEADER_TIMESTAMP as p, HEADER_TRACEPARENT as q, type InferSchema as r, type KafkaLogger as s, type KafkaMetrics as t, type MessageLostContext as u, type SchemaParseContext as v, type SendOptions as w, type SubscribeRetryOptions as x, type TTopicMessageMap as y, type TopicsFrom as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drarzter/kafka-client",
3
- "version": "0.7.0",
3
+ "version": "0.7.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",