@drarzter/kafka-client 0.3.0 → 0.5.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 +168 -49
- package/dist/chunk-EQQGB2QZ.mjs +17 -0
- package/dist/chunk-EQQGB2QZ.mjs.map +1 -0
- package/dist/{chunk-UDOHIMAZ.mjs → chunk-YCKN2YEC.mjs} +342 -204
- package/dist/chunk-YCKN2YEC.mjs.map +1 -0
- package/dist/core.d.mts +15 -255
- package/dist/core.d.ts +15 -255
- package/dist/core.js +351 -189
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +22 -1
- package/dist/envelope-QK1trQu4.d.mts +319 -0
- package/dist/envelope-QK1trQu4.d.ts +319 -0
- package/dist/index.d.mts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +361 -193
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +35 -7
- package/dist/index.mjs.map +1 -1
- package/dist/otel.d.mts +27 -0
- package/dist/otel.d.ts +27 -0
- package/dist/otel.js +66 -0
- package/dist/otel.js.map +1 -0
- package/dist/otel.mjs +49 -0
- package/dist/otel.mjs.map +1 -0
- package/dist/testing.d.mts +104 -0
- package/dist/testing.d.ts +104 -0
- package/dist/testing.js +157 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +133 -0
- package/dist/testing.mjs.map +1 -0
- package/package.json +44 -7
- package/dist/chunk-UDOHIMAZ.mjs.map +0 -1
package/dist/otel.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import "./chunk-EQQGB2QZ.mjs";
|
|
2
|
+
|
|
3
|
+
// src/otel.ts
|
|
4
|
+
import {
|
|
5
|
+
trace,
|
|
6
|
+
context,
|
|
7
|
+
propagation,
|
|
8
|
+
SpanKind,
|
|
9
|
+
SpanStatusCode
|
|
10
|
+
} from "@opentelemetry/api";
|
|
11
|
+
function otelInstrumentation() {
|
|
12
|
+
const tracer = trace.getTracer("@drarzter/kafka-client");
|
|
13
|
+
return {
|
|
14
|
+
beforeSend(topic, headers) {
|
|
15
|
+
propagation.inject(context.active(), headers);
|
|
16
|
+
},
|
|
17
|
+
afterSend(_topic) {
|
|
18
|
+
},
|
|
19
|
+
beforeConsume(envelope) {
|
|
20
|
+
const parentCtx = propagation.extract(context.active(), envelope.headers);
|
|
21
|
+
const span = tracer.startSpan(
|
|
22
|
+
`kafka.consume ${envelope.topic}`,
|
|
23
|
+
{
|
|
24
|
+
kind: SpanKind.CONSUMER,
|
|
25
|
+
attributes: {
|
|
26
|
+
"messaging.system": "kafka",
|
|
27
|
+
"messaging.destination.name": envelope.topic,
|
|
28
|
+
"messaging.message.id": envelope.eventId,
|
|
29
|
+
"messaging.kafka.partition": envelope.partition,
|
|
30
|
+
"messaging.kafka.offset": envelope.offset
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
parentCtx
|
|
34
|
+
);
|
|
35
|
+
return () => span.end();
|
|
36
|
+
},
|
|
37
|
+
onConsumeError(envelope, error) {
|
|
38
|
+
const span = trace.getActiveSpan();
|
|
39
|
+
if (span) {
|
|
40
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
|
41
|
+
span.recordException(error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
otelInstrumentation
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=otel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/otel.ts"],"sourcesContent":["import {\n trace,\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n} from \"@opentelemetry/api\";\nimport type { KafkaInstrumentation } from \"./client/types\";\nimport type { EventEnvelope } from \"./client/envelope\";\n\n/**\n * Create a `KafkaInstrumentation` that automatically propagates\n * W3C Trace Context via Kafka headers.\n *\n * Requires `@opentelemetry/api` as a peer dependency.\n *\n * **Send path:** injects `traceparent` into message headers from the\n * active OpenTelemetry context.\n *\n * **Consume path:** extracts `traceparent` from message headers,\n * starts a `CONSUMER` span as a child of the extracted context,\n * and ends it when the handler completes.\n *\n * @example\n * ```ts\n * import { otelInstrumentation } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation()],\n * });\n * ```\n */\nexport function otelInstrumentation(): KafkaInstrumentation {\n const tracer = trace.getTracer(\"@drarzter/kafka-client\");\n\n return {\n beforeSend(topic: string, headers: Record<string, string>) {\n propagation.inject(context.active(), headers);\n },\n\n afterSend(_topic: string) {\n // Span management for producers is left to the caller's OTel setup.\n // We only inject context — creating producer spans here would be\n // inaccurate since buildSendPayload runs synchronously per-message.\n },\n\n beforeConsume(envelope: EventEnvelope<any>) {\n const parentCtx = propagation.extract(context.active(), envelope.headers);\n const span = tracer.startSpan(\n `kafka.consume ${envelope.topic}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: {\n \"messaging.system\": \"kafka\",\n \"messaging.destination.name\": envelope.topic,\n \"messaging.message.id\": envelope.eventId,\n \"messaging.kafka.partition\": envelope.partition,\n \"messaging.kafka.offset\": envelope.offset,\n },\n },\n parentCtx,\n );\n return () => span.end();\n },\n\n onConsumeError(envelope: EventEnvelope<any>, error: Error) {\n const span = trace.getActiveSpan();\n if (span) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.recordException(error);\n }\n },\n };\n}\n"],"mappings":";;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA0BA,SAAS,sBAA4C;AAC1D,QAAM,SAAS,MAAM,UAAU,wBAAwB;AAEvD,SAAO;AAAA,IACL,WAAW,OAAe,SAAiC;AACzD,kBAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;AAAA,IAC9C;AAAA,IAEA,UAAU,QAAgB;AAAA,IAI1B;AAAA,IAEA,cAAc,UAA8B;AAC1C,YAAM,YAAY,YAAY,QAAQ,QAAQ,OAAO,GAAG,SAAS,OAAO;AACxE,YAAM,OAAO,OAAO;AAAA,QAClB,iBAAiB,SAAS,KAAK;AAAA,QAC/B;AAAA,UACE,MAAM,SAAS;AAAA,UACf,YAAY;AAAA,YACV,oBAAoB;AAAA,YACpB,8BAA8B,SAAS;AAAA,YACvC,wBAAwB,SAAS;AAAA,YACjC,6BAA6B,SAAS;AAAA,YACtC,0BAA0B,SAAS;AAAA,UACrC;AAAA,QACF;AAAA,QACA;AAAA,MACF;AACA,aAAO,MAAM,KAAK,IAAI;AAAA,IACxB;AAAA,IAEA,eAAe,UAA8B,OAAc;AACzD,YAAM,OAAO,MAAM,cAAc;AACjC,UAAI,MAAM;AACR,aAAK,UAAU,EAAE,MAAM,eAAe,OAAO,SAAS,MAAM,QAAQ,CAAC;AACrE,aAAK,gBAAgB,KAAK;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { T as TopicMapConstraint, I as IKafkaClient } from './envelope-QK1trQu4.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fully typed mock of `IKafkaClient<T>` where every method is a mock function.
|
|
5
|
+
* Compatible with Jest, Vitest, or any framework whose `fn()` returns
|
|
6
|
+
* an object with `.mock`, `.mockResolvedValue`, etc.
|
|
7
|
+
*/
|
|
8
|
+
type MockKafkaClient<T extends TopicMapConstraint<T>> = {
|
|
9
|
+
[K in keyof IKafkaClient<T>]: IKafkaClient<T>[K] & Record<string, any>;
|
|
10
|
+
};
|
|
11
|
+
/** Factory that creates a no-op mock function (e.g. `() => jest.fn()`). */
|
|
12
|
+
type MockFactory = () => (...args: any[]) => any;
|
|
13
|
+
/**
|
|
14
|
+
* Create a fully typed mock implementing every `IKafkaClient<T>` method.
|
|
15
|
+
* Useful for unit-testing services that depend on `KafkaClient` without
|
|
16
|
+
* touching a real broker.
|
|
17
|
+
*
|
|
18
|
+
* Auto-detects Jest (`jest.fn()`) or Vitest (`vi.fn()`). Pass a custom
|
|
19
|
+
* `mockFactory` for other frameworks.
|
|
20
|
+
*
|
|
21
|
+
* All methods resolve to sensible defaults:
|
|
22
|
+
* - `checkStatus()` → `{ topics: [] }`
|
|
23
|
+
* - `getClientId()` → `"mock-client"`
|
|
24
|
+
* - void methods → `undefined`
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const kafka = createMockKafkaClient<MyTopics>();
|
|
29
|
+
*
|
|
30
|
+
* const service = new OrdersService(kafka);
|
|
31
|
+
* await service.createOrder();
|
|
32
|
+
*
|
|
33
|
+
* expect(kafka.sendMessage).toHaveBeenCalledWith(
|
|
34
|
+
* 'order.created',
|
|
35
|
+
* expect.objectContaining({ orderId: '123' }),
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare function createMockKafkaClient<T extends TopicMapConstraint<T>>(mockFactory?: MockFactory): MockKafkaClient<T>;
|
|
40
|
+
|
|
41
|
+
/** Options for `KafkaTestContainer`. */
|
|
42
|
+
interface KafkaTestContainerOptions {
|
|
43
|
+
/** Docker image. Default: `"confluentinc/cp-kafka:7.7.0"`. */
|
|
44
|
+
image?: string;
|
|
45
|
+
/** Warm up the transactional coordinator on start. Default: `true`. */
|
|
46
|
+
transactionWarmup?: boolean;
|
|
47
|
+
/** Topics to pre-create. Each entry can be a string (1 partition) or `{ topic, numPartitions }`. */
|
|
48
|
+
topics?: Array<string | {
|
|
49
|
+
topic: string;
|
|
50
|
+
numPartitions?: number;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Thin wrapper around `@testcontainers/kafka` that starts a single-node
|
|
55
|
+
* KRaft Kafka container and exposes `brokers` for use with `KafkaClient`.
|
|
56
|
+
*
|
|
57
|
+
* Handles common setup pain points:
|
|
58
|
+
* - Transaction coordinator warmup (avoids transactional producer hangs)
|
|
59
|
+
* - Topic pre-creation (avoids race conditions)
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const container = new KafkaTestContainer({ topics: ['orders', 'payments'] });
|
|
64
|
+
* const brokers = await container.start();
|
|
65
|
+
*
|
|
66
|
+
* const kafka = new KafkaClient('test', 'test-group', brokers);
|
|
67
|
+
* // ... run tests ...
|
|
68
|
+
*
|
|
69
|
+
* await container.stop();
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example Jest lifecycle
|
|
73
|
+
* ```ts
|
|
74
|
+
* let container: KafkaTestContainer;
|
|
75
|
+
* let brokers: string[];
|
|
76
|
+
*
|
|
77
|
+
* beforeAll(async () => {
|
|
78
|
+
* container = new KafkaTestContainer({ topics: ['orders'] });
|
|
79
|
+
* brokers = await container.start();
|
|
80
|
+
* }, 120_000);
|
|
81
|
+
*
|
|
82
|
+
* afterAll(() => container.stop());
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare class KafkaTestContainer {
|
|
86
|
+
private container;
|
|
87
|
+
private readonly image;
|
|
88
|
+
private readonly transactionWarmup;
|
|
89
|
+
private readonly topics;
|
|
90
|
+
constructor(options?: KafkaTestContainerOptions);
|
|
91
|
+
/**
|
|
92
|
+
* Start the Kafka container, pre-create topics, and optionally warm up
|
|
93
|
+
* the transaction coordinator.
|
|
94
|
+
*
|
|
95
|
+
* @returns Broker connection strings, e.g. `["localhost:55123"]`.
|
|
96
|
+
*/
|
|
97
|
+
start(): Promise<string[]>;
|
|
98
|
+
/** Stop and remove the container. */
|
|
99
|
+
stop(): Promise<void>;
|
|
100
|
+
/** Broker connection strings. Throws if container is not started. */
|
|
101
|
+
get brokers(): string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { KafkaTestContainer, type KafkaTestContainerOptions, type MockKafkaClient, createMockKafkaClient };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { T as TopicMapConstraint, I as IKafkaClient } from './envelope-QK1trQu4.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fully typed mock of `IKafkaClient<T>` where every method is a mock function.
|
|
5
|
+
* Compatible with Jest, Vitest, or any framework whose `fn()` returns
|
|
6
|
+
* an object with `.mock`, `.mockResolvedValue`, etc.
|
|
7
|
+
*/
|
|
8
|
+
type MockKafkaClient<T extends TopicMapConstraint<T>> = {
|
|
9
|
+
[K in keyof IKafkaClient<T>]: IKafkaClient<T>[K] & Record<string, any>;
|
|
10
|
+
};
|
|
11
|
+
/** Factory that creates a no-op mock function (e.g. `() => jest.fn()`). */
|
|
12
|
+
type MockFactory = () => (...args: any[]) => any;
|
|
13
|
+
/**
|
|
14
|
+
* Create a fully typed mock implementing every `IKafkaClient<T>` method.
|
|
15
|
+
* Useful for unit-testing services that depend on `KafkaClient` without
|
|
16
|
+
* touching a real broker.
|
|
17
|
+
*
|
|
18
|
+
* Auto-detects Jest (`jest.fn()`) or Vitest (`vi.fn()`). Pass a custom
|
|
19
|
+
* `mockFactory` for other frameworks.
|
|
20
|
+
*
|
|
21
|
+
* All methods resolve to sensible defaults:
|
|
22
|
+
* - `checkStatus()` → `{ topics: [] }`
|
|
23
|
+
* - `getClientId()` → `"mock-client"`
|
|
24
|
+
* - void methods → `undefined`
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const kafka = createMockKafkaClient<MyTopics>();
|
|
29
|
+
*
|
|
30
|
+
* const service = new OrdersService(kafka);
|
|
31
|
+
* await service.createOrder();
|
|
32
|
+
*
|
|
33
|
+
* expect(kafka.sendMessage).toHaveBeenCalledWith(
|
|
34
|
+
* 'order.created',
|
|
35
|
+
* expect.objectContaining({ orderId: '123' }),
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare function createMockKafkaClient<T extends TopicMapConstraint<T>>(mockFactory?: MockFactory): MockKafkaClient<T>;
|
|
40
|
+
|
|
41
|
+
/** Options for `KafkaTestContainer`. */
|
|
42
|
+
interface KafkaTestContainerOptions {
|
|
43
|
+
/** Docker image. Default: `"confluentinc/cp-kafka:7.7.0"`. */
|
|
44
|
+
image?: string;
|
|
45
|
+
/** Warm up the transactional coordinator on start. Default: `true`. */
|
|
46
|
+
transactionWarmup?: boolean;
|
|
47
|
+
/** Topics to pre-create. Each entry can be a string (1 partition) or `{ topic, numPartitions }`. */
|
|
48
|
+
topics?: Array<string | {
|
|
49
|
+
topic: string;
|
|
50
|
+
numPartitions?: number;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Thin wrapper around `@testcontainers/kafka` that starts a single-node
|
|
55
|
+
* KRaft Kafka container and exposes `brokers` for use with `KafkaClient`.
|
|
56
|
+
*
|
|
57
|
+
* Handles common setup pain points:
|
|
58
|
+
* - Transaction coordinator warmup (avoids transactional producer hangs)
|
|
59
|
+
* - Topic pre-creation (avoids race conditions)
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const container = new KafkaTestContainer({ topics: ['orders', 'payments'] });
|
|
64
|
+
* const brokers = await container.start();
|
|
65
|
+
*
|
|
66
|
+
* const kafka = new KafkaClient('test', 'test-group', brokers);
|
|
67
|
+
* // ... run tests ...
|
|
68
|
+
*
|
|
69
|
+
* await container.stop();
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example Jest lifecycle
|
|
73
|
+
* ```ts
|
|
74
|
+
* let container: KafkaTestContainer;
|
|
75
|
+
* let brokers: string[];
|
|
76
|
+
*
|
|
77
|
+
* beforeAll(async () => {
|
|
78
|
+
* container = new KafkaTestContainer({ topics: ['orders'] });
|
|
79
|
+
* brokers = await container.start();
|
|
80
|
+
* }, 120_000);
|
|
81
|
+
*
|
|
82
|
+
* afterAll(() => container.stop());
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare class KafkaTestContainer {
|
|
86
|
+
private container;
|
|
87
|
+
private readonly image;
|
|
88
|
+
private readonly transactionWarmup;
|
|
89
|
+
private readonly topics;
|
|
90
|
+
constructor(options?: KafkaTestContainerOptions);
|
|
91
|
+
/**
|
|
92
|
+
* Start the Kafka container, pre-create topics, and optionally warm up
|
|
93
|
+
* the transaction coordinator.
|
|
94
|
+
*
|
|
95
|
+
* @returns Broker connection strings, e.g. `["localhost:55123"]`.
|
|
96
|
+
*/
|
|
97
|
+
start(): Promise<string[]>;
|
|
98
|
+
/** Stop and remove the container. */
|
|
99
|
+
stop(): Promise<void>;
|
|
100
|
+
/** Broker connection strings. Throws if container is not started. */
|
|
101
|
+
get brokers(): string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { KafkaTestContainer, type KafkaTestContainerOptions, type MockKafkaClient, createMockKafkaClient };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
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/testing.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
KafkaTestContainer: () => KafkaTestContainer,
|
|
24
|
+
createMockKafkaClient: () => createMockKafkaClient
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(testing_exports);
|
|
27
|
+
|
|
28
|
+
// src/testing/mock-client.ts
|
|
29
|
+
function detectMockFactory() {
|
|
30
|
+
try {
|
|
31
|
+
const jestFn = eval("typeof jest !== 'undefined' && jest.fn");
|
|
32
|
+
if (typeof jestFn === "function") return () => eval("jest.fn")();
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const viFn = eval("typeof vi !== 'undefined' && vi.fn");
|
|
37
|
+
if (typeof viFn === "function") return () => eval("vi.fn")();
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
"createMockKafkaClient: no mock framework detected (jest/vitest). Pass a custom mockFactory."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function createMockKafkaClient(mockFactory) {
|
|
45
|
+
const fn = mockFactory ?? detectMockFactory();
|
|
46
|
+
const mock = () => fn();
|
|
47
|
+
const resolved = (value) => mock().mockResolvedValue(value);
|
|
48
|
+
const returning = (value) => mock().mockReturnValue(value);
|
|
49
|
+
return {
|
|
50
|
+
checkStatus: resolved({ topics: [] }),
|
|
51
|
+
getClientId: returning("mock-client"),
|
|
52
|
+
sendMessage: resolved(void 0),
|
|
53
|
+
sendBatch: resolved(void 0),
|
|
54
|
+
transaction: mock().mockImplementation(async (cb) => {
|
|
55
|
+
const ctx = {
|
|
56
|
+
send: resolved(void 0),
|
|
57
|
+
sendBatch: resolved(void 0)
|
|
58
|
+
};
|
|
59
|
+
await cb(ctx);
|
|
60
|
+
}),
|
|
61
|
+
startConsumer: resolved(void 0),
|
|
62
|
+
startBatchConsumer: resolved(void 0),
|
|
63
|
+
stopConsumer: resolved(void 0),
|
|
64
|
+
disconnect: resolved(void 0)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/testing/test-container.ts
|
|
69
|
+
var import_kafka = require("@testcontainers/kafka");
|
|
70
|
+
var import_kafka_javascript = require("@confluentinc/kafka-javascript");
|
|
71
|
+
var { Kafka, logLevel: KafkaLogLevel } = import_kafka_javascript.KafkaJS;
|
|
72
|
+
var KafkaTestContainer = class {
|
|
73
|
+
container;
|
|
74
|
+
image;
|
|
75
|
+
transactionWarmup;
|
|
76
|
+
topics;
|
|
77
|
+
constructor(options) {
|
|
78
|
+
this.image = options?.image ?? "confluentinc/cp-kafka:7.7.0";
|
|
79
|
+
this.transactionWarmup = options?.transactionWarmup ?? true;
|
|
80
|
+
this.topics = options?.topics ?? [];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Start the Kafka container, pre-create topics, and optionally warm up
|
|
84
|
+
* the transaction coordinator.
|
|
85
|
+
*
|
|
86
|
+
* @returns Broker connection strings, e.g. `["localhost:55123"]`.
|
|
87
|
+
*/
|
|
88
|
+
async start() {
|
|
89
|
+
this.container = await new import_kafka.KafkaContainer(this.image).withKraft().withExposedPorts(9093).withEnvironment({
|
|
90
|
+
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: "1",
|
|
91
|
+
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: "1"
|
|
92
|
+
}).start();
|
|
93
|
+
const host = this.container.getHost();
|
|
94
|
+
const port = this.container.getMappedPort(9093);
|
|
95
|
+
const brokers = [`${host}:${port}`];
|
|
96
|
+
const kafka = new Kafka({
|
|
97
|
+
kafkaJS: {
|
|
98
|
+
clientId: "test-container-setup",
|
|
99
|
+
brokers,
|
|
100
|
+
logLevel: KafkaLogLevel.NOTHING
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
if (this.topics.length > 0) {
|
|
104
|
+
const admin = kafka.admin();
|
|
105
|
+
await admin.connect();
|
|
106
|
+
await admin.createTopics({
|
|
107
|
+
topics: this.topics.map(
|
|
108
|
+
(t) => typeof t === "string" ? { topic: t, numPartitions: 1 } : { topic: t.topic, numPartitions: t.numPartitions ?? 1 }
|
|
109
|
+
)
|
|
110
|
+
});
|
|
111
|
+
await admin.disconnect();
|
|
112
|
+
}
|
|
113
|
+
if (this.transactionWarmup) {
|
|
114
|
+
const warmupKafka = new Kafka({
|
|
115
|
+
kafkaJS: {
|
|
116
|
+
clientId: "test-container-warmup",
|
|
117
|
+
brokers,
|
|
118
|
+
logLevel: KafkaLogLevel.NOTHING
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const txProducer = warmupKafka.producer({
|
|
122
|
+
kafkaJS: {
|
|
123
|
+
transactionalId: "test-container-warmup-tx",
|
|
124
|
+
idempotent: true,
|
|
125
|
+
maxInFlightRequests: 1
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
await txProducer.connect();
|
|
129
|
+
const tx = await txProducer.transaction();
|
|
130
|
+
await tx.abort();
|
|
131
|
+
await txProducer.disconnect();
|
|
132
|
+
}
|
|
133
|
+
return brokers;
|
|
134
|
+
}
|
|
135
|
+
/** Stop and remove the container. */
|
|
136
|
+
async stop() {
|
|
137
|
+
await this.container?.stop();
|
|
138
|
+
this.container = void 0;
|
|
139
|
+
}
|
|
140
|
+
/** Broker connection strings. Throws if container is not started. */
|
|
141
|
+
get brokers() {
|
|
142
|
+
if (!this.container) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
"KafkaTestContainer is not started. Call start() first."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const host = this.container.getHost();
|
|
148
|
+
const port = this.container.getMappedPort(9093);
|
|
149
|
+
return [`${host}:${port}`];
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
153
|
+
0 && (module.exports = {
|
|
154
|
+
KafkaTestContainer,
|
|
155
|
+
createMockKafkaClient
|
|
156
|
+
});
|
|
157
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +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 \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 { 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(\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":[]}
|
package/dist/testing.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
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 { KafkaJS } from "@confluentinc/kafka-javascript";
|
|
48
|
+
var { Kafka, logLevel: KafkaLogLevel } = KafkaJS;
|
|
49
|
+
var KafkaTestContainer = class {
|
|
50
|
+
container;
|
|
51
|
+
image;
|
|
52
|
+
transactionWarmup;
|
|
53
|
+
topics;
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.image = options?.image ?? "confluentinc/cp-kafka:7.7.0";
|
|
56
|
+
this.transactionWarmup = options?.transactionWarmup ?? true;
|
|
57
|
+
this.topics = options?.topics ?? [];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Start the Kafka container, pre-create topics, and optionally warm up
|
|
61
|
+
* the transaction coordinator.
|
|
62
|
+
*
|
|
63
|
+
* @returns Broker connection strings, e.g. `["localhost:55123"]`.
|
|
64
|
+
*/
|
|
65
|
+
async start() {
|
|
66
|
+
this.container = await new KafkaContainer(this.image).withKraft().withExposedPorts(9093).withEnvironment({
|
|
67
|
+
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: "1",
|
|
68
|
+
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: "1"
|
|
69
|
+
}).start();
|
|
70
|
+
const host = this.container.getHost();
|
|
71
|
+
const port = this.container.getMappedPort(9093);
|
|
72
|
+
const brokers = [`${host}:${port}`];
|
|
73
|
+
const kafka = new Kafka({
|
|
74
|
+
kafkaJS: {
|
|
75
|
+
clientId: "test-container-setup",
|
|
76
|
+
brokers,
|
|
77
|
+
logLevel: KafkaLogLevel.NOTHING
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if (this.topics.length > 0) {
|
|
81
|
+
const admin = kafka.admin();
|
|
82
|
+
await admin.connect();
|
|
83
|
+
await admin.createTopics({
|
|
84
|
+
topics: this.topics.map(
|
|
85
|
+
(t) => typeof t === "string" ? { topic: t, numPartitions: 1 } : { topic: t.topic, numPartitions: t.numPartitions ?? 1 }
|
|
86
|
+
)
|
|
87
|
+
});
|
|
88
|
+
await admin.disconnect();
|
|
89
|
+
}
|
|
90
|
+
if (this.transactionWarmup) {
|
|
91
|
+
const warmupKafka = new Kafka({
|
|
92
|
+
kafkaJS: {
|
|
93
|
+
clientId: "test-container-warmup",
|
|
94
|
+
brokers,
|
|
95
|
+
logLevel: KafkaLogLevel.NOTHING
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
const txProducer = warmupKafka.producer({
|
|
99
|
+
kafkaJS: {
|
|
100
|
+
transactionalId: "test-container-warmup-tx",
|
|
101
|
+
idempotent: true,
|
|
102
|
+
maxInFlightRequests: 1
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
await txProducer.connect();
|
|
106
|
+
const tx = await txProducer.transaction();
|
|
107
|
+
await tx.abort();
|
|
108
|
+
await txProducer.disconnect();
|
|
109
|
+
}
|
|
110
|
+
return brokers;
|
|
111
|
+
}
|
|
112
|
+
/** Stop and remove the container. */
|
|
113
|
+
async stop() {
|
|
114
|
+
await this.container?.stop();
|
|
115
|
+
this.container = void 0;
|
|
116
|
+
}
|
|
117
|
+
/** Broker connection strings. Throws if container is not started. */
|
|
118
|
+
get brokers() {
|
|
119
|
+
if (!this.container) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"KafkaTestContainer is not started. Call start() first."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const host = this.container.getHost();
|
|
125
|
+
const port = this.container.getMappedPort(9093);
|
|
126
|
+
return [`${host}:${port}`];
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
export {
|
|
130
|
+
KafkaTestContainer,
|
|
131
|
+
createMockKafkaClient
|
|
132
|
+
};
|
|
133
|
+
//# 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 { 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(\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":[]}
|