@drarzter/kafka-client 0.9.2 → 0.9.4
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 +32 -7
- package/dist/{chunk-Z2DOJQRI.mjs → chunk-SM4FZKAZ.mjs} +6 -6
- package/dist/chunk-SM4FZKAZ.mjs.map +1 -0
- package/dist/client-1irhGEu0.d.mts +751 -0
- package/dist/client-BpFjkHhr.d.ts +751 -0
- package/dist/consumer.types-fFCag3VJ.d.mts +958 -0
- package/dist/consumer.types-fFCag3VJ.d.ts +958 -0
- package/dist/core.d.mts +126 -3
- package/dist/core.d.ts +126 -3
- package/dist/core.js +5 -5
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +1 -1
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/otel.d.mts +1 -1
- package/dist/otel.d.ts +1 -1
- package/dist/testing.d.mts +2 -1
- package/dist/testing.d.ts +2 -1
- package/dist/testing.js +5 -5
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +5 -5
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-Z2DOJQRI.mjs.map +0 -1
- package/dist/types-4XNxkici.d.mts +0 -1952
- package/dist/types-4XNxkici.d.ts +0 -1952
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapping of topic names to their message types.
|
|
3
|
+
* Define this interface to get type-safe publish/subscribe across your app.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* // with explicit extends (IDE hints for values)
|
|
8
|
+
* interface MyTopics extends TTopicMessageMap {
|
|
9
|
+
* "orders.created": { orderId: string; amount: number };
|
|
10
|
+
* "users.updated": { userId: string; name: string };
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* // or plain interface / type — works the same
|
|
14
|
+
* interface MyTopics {
|
|
15
|
+
* "orders.created": { orderId: string; amount: number };
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
type TTopicMessageMap = {
|
|
20
|
+
[topic: string]: Record<string, any>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Generic constraint for topic-message maps.
|
|
24
|
+
* Works with both `type` aliases and `interface` declarations.
|
|
25
|
+
*/
|
|
26
|
+
type TopicMapConstraint<T> = {
|
|
27
|
+
[K in keyof T]: Record<string, any>;
|
|
28
|
+
};
|
|
29
|
+
type ClientId = string;
|
|
30
|
+
type GroupId = string;
|
|
31
|
+
type MessageHeaders = Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* Message compression codec.
|
|
34
|
+
* Maps directly to the underlying librdkafka codec values.
|
|
35
|
+
* - `'none'` — no compression (default)
|
|
36
|
+
* - `'gzip'` — widely supported, moderate compression ratio
|
|
37
|
+
* - `'snappy'` — fast, moderate compression ratio
|
|
38
|
+
* - `'lz4'` — fastest, slightly lower ratio
|
|
39
|
+
* - `'zstd'` — best ratio, slightly slower
|
|
40
|
+
*/
|
|
41
|
+
type CompressionType = "none" | "gzip" | "snappy" | "lz4" | "zstd";
|
|
42
|
+
/**
|
|
43
|
+
* Logger interface for KafkaClient.
|
|
44
|
+
* Compatible with NestJS Logger, console, winston, pino, or any custom logger.
|
|
45
|
+
*
|
|
46
|
+
* `debug` is optional — omit it to suppress debug output in production.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* // Pass a NestJS logger:
|
|
51
|
+
* const kafka = new KafkaClient(config, groupId, { logger: this.logger });
|
|
52
|
+
*
|
|
53
|
+
* // Or a minimal pino wrapper:
|
|
54
|
+
* const kafka = new KafkaClient(config, groupId, {
|
|
55
|
+
* logger: {
|
|
56
|
+
* log: (msg) => pino.info(msg),
|
|
57
|
+
* warn: (msg, ...a) => pino.warn(msg, ...a),
|
|
58
|
+
* error: (msg, ...a) => pino.error(msg, ...a),
|
|
59
|
+
* debug: (msg, ...a) => pino.debug(msg, ...a),
|
|
60
|
+
* },
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
interface KafkaLogger {
|
|
65
|
+
log(message: string): void;
|
|
66
|
+
warn(message: string, ...args: any[]): void;
|
|
67
|
+
error(message: string, ...args: any[]): void;
|
|
68
|
+
debug?(message: string, ...args: any[]): void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Snapshot of internal event counters accumulated since client creation
|
|
72
|
+
* (or since the last `resetMetrics()` call).
|
|
73
|
+
*/
|
|
74
|
+
interface KafkaMetrics {
|
|
75
|
+
/** Total messages successfully processed by the consumer handler. */
|
|
76
|
+
processedCount: number;
|
|
77
|
+
/** Total retry attempts routed — covers both in-process retries and retry-topic hops. */
|
|
78
|
+
retryCount: number;
|
|
79
|
+
/** Total messages sent to a DLQ topic. */
|
|
80
|
+
dlqCount: number;
|
|
81
|
+
/** Total duplicate messages detected by the Lamport clock. */
|
|
82
|
+
dedupCount: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Options for sending a single message.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* await kafka.sendMessage('orders.created', { orderId: '123', amount: 99 }, {
|
|
91
|
+
* key: 'order-123',
|
|
92
|
+
* headers: { 'x-source': 'checkout-service' },
|
|
93
|
+
* compression: 'snappy',
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
interface SendOptions {
|
|
98
|
+
/** Partition key for message routing. */
|
|
99
|
+
key?: string;
|
|
100
|
+
/** Custom headers attached to the message (merged with auto-generated envelope headers). */
|
|
101
|
+
headers?: MessageHeaders;
|
|
102
|
+
/** Override the auto-propagated correlation ID (default: inherited from ALS context or new UUID). */
|
|
103
|
+
correlationId?: string;
|
|
104
|
+
/** Schema version for the payload. Default: `1`. */
|
|
105
|
+
schemaVersion?: number;
|
|
106
|
+
/** Override the auto-generated event ID (UUID v4). */
|
|
107
|
+
eventId?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Compression codec for this message.
|
|
110
|
+
* Applied at the producer record level — all messages in a single `send` call share the same codec.
|
|
111
|
+
* Default: `'none'`.
|
|
112
|
+
*/
|
|
113
|
+
compression?: CompressionType;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Shape of each item in a `sendBatch` call.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* await kafka.sendBatch('orders.created', [
|
|
121
|
+
* { value: { orderId: '1', amount: 10 }, key: 'order-1' },
|
|
122
|
+
* { value: { orderId: '2', amount: 20 }, key: 'order-2', headers: { 'x-priority': 'high' } },
|
|
123
|
+
* ]);
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
interface BatchMessageItem<V> {
|
|
127
|
+
value: V;
|
|
128
|
+
/**
|
|
129
|
+
* Kafka partition key for this message.
|
|
130
|
+
* Kafka hashes the key to deterministically route the message to a partition.
|
|
131
|
+
* Messages with the same key always land on the same partition — use this to
|
|
132
|
+
* guarantee ordering per entity (e.g. `userId`, `orderId`).
|
|
133
|
+
*/
|
|
134
|
+
key?: string;
|
|
135
|
+
headers?: MessageHeaders;
|
|
136
|
+
correlationId?: string;
|
|
137
|
+
schemaVersion?: number;
|
|
138
|
+
eventId?: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Options for a `sendBatch` call (applies to all messages in the batch).
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* await kafka.sendBatch('metrics', messages, { compression: 'zstd' });
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
interface BatchSendOptions {
|
|
149
|
+
/**
|
|
150
|
+
* Compression codec for this batch.
|
|
151
|
+
* Applied at the producer record level — all messages in the batch share the same codec.
|
|
152
|
+
* Default: `'none'`.
|
|
153
|
+
*/
|
|
154
|
+
compression?: CompressionType;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Context passed as the second argument to `SchemaLike.parse()`.
|
|
159
|
+
* Enables schema-registry adapters, version-aware migration, and
|
|
160
|
+
* header-driven parsing without coupling validators to Kafka internals.
|
|
161
|
+
*
|
|
162
|
+
* All fields are optional-friendly — validators that don't need the context
|
|
163
|
+
* can simply ignore the second argument.
|
|
164
|
+
*/
|
|
165
|
+
interface SchemaParseContext {
|
|
166
|
+
/** Topic the message was produced to / consumed from. */
|
|
167
|
+
topic: string;
|
|
168
|
+
/** Decoded message headers (envelope headers included). */
|
|
169
|
+
headers: MessageHeaders;
|
|
170
|
+
/** Value of the `x-schema-version` header, defaults to `1`. */
|
|
171
|
+
version: number;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Any validation library with a `.parse()` method.
|
|
175
|
+
* Works with Zod, Valibot, ArkType, or any custom validator.
|
|
176
|
+
*
|
|
177
|
+
* The optional `ctx` argument carries topic/header/version metadata so
|
|
178
|
+
* validators can perform schema-registry lookups or version-aware migrations.
|
|
179
|
+
* Existing validators that only use the first argument continue to work
|
|
180
|
+
* unchanged — the second argument is silently ignored.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* import { z } from 'zod';
|
|
185
|
+
* const schema: SchemaLike<{ id: string }> = z.object({ id: z.string() });
|
|
186
|
+
*
|
|
187
|
+
* // Context-aware validator:
|
|
188
|
+
* const schema: SchemaLike<MyType> = {
|
|
189
|
+
* parse(data, ctx) {
|
|
190
|
+
* const version = ctx?.version ?? 1;
|
|
191
|
+
* return version >= 2 ? migrateV1toV2(data) : validateV1(data);
|
|
192
|
+
* }
|
|
193
|
+
* };
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
interface SchemaLike<T = any> {
|
|
197
|
+
parse(data: unknown, ctx?: SchemaParseContext): T | Promise<T>;
|
|
198
|
+
}
|
|
199
|
+
/** Infer the output type from a SchemaLike. */
|
|
200
|
+
type InferSchema<S extends SchemaLike> = S extends SchemaLike<infer T> ? T : never;
|
|
201
|
+
/**
|
|
202
|
+
* A typed topic descriptor that pairs a topic name with its message type.
|
|
203
|
+
* Created via the `topic()` factory function.
|
|
204
|
+
*
|
|
205
|
+
* @typeParam N - The literal topic name string.
|
|
206
|
+
* @typeParam M - The message payload type for this topic.
|
|
207
|
+
*/
|
|
208
|
+
interface TopicDescriptor<N extends string = string, M extends Record<string, any> = Record<string, any>> {
|
|
209
|
+
readonly __topic: N;
|
|
210
|
+
/** @internal Phantom type — never has a real value at runtime. */
|
|
211
|
+
readonly __type: M;
|
|
212
|
+
/** Runtime schema validator. Present only when created via `topic().schema()`. */
|
|
213
|
+
readonly __schema?: SchemaLike<M>;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Define a typed topic descriptor.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* // Without schema — explicit type via .type<T>():
|
|
221
|
+
* const OrderCreated = topic('order.created').type<{ orderId: string; amount: number }>();
|
|
222
|
+
*
|
|
223
|
+
* // With schema — type inferred from schema:
|
|
224
|
+
* const OrderCreated = topic('order.created').schema(z.object({
|
|
225
|
+
* orderId: z.string(),
|
|
226
|
+
* amount: z.number(),
|
|
227
|
+
* }));
|
|
228
|
+
*
|
|
229
|
+
* // Use with KafkaClient:
|
|
230
|
+
* await kafka.sendMessage(OrderCreated, { orderId: '123', amount: 100 });
|
|
231
|
+
*
|
|
232
|
+
* // Use with @SubscribeTo:
|
|
233
|
+
* @SubscribeTo(OrderCreated)
|
|
234
|
+
* async handleOrder(msg) { ... }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
declare function topic<N extends string>(name: N): {
|
|
238
|
+
/** Provide an explicit message type without a runtime schema. */
|
|
239
|
+
type: <M extends Record<string, any>>() => TopicDescriptor<N, M>;
|
|
240
|
+
schema: <S extends SchemaLike<Record<string, any>>>(schema: S) => TopicDescriptor<N, InferSchema<S>>;
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Build a topic-message map type from a union of TopicDescriptors.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* const OrderCreated = topic('order.created').type<{ orderId: string }>();
|
|
248
|
+
* const OrderCompleted = topic('order.completed').type<{ completedAt: string }>();
|
|
249
|
+
*
|
|
250
|
+
* type MyTopics = TopicsFrom<typeof OrderCreated | typeof OrderCompleted>;
|
|
251
|
+
* // { 'order.created': { orderId: string }; 'order.completed': { completedAt: string } }
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
type TopicsFrom<D extends TopicDescriptor<any, any>> = {
|
|
255
|
+
[K in D as K["__topic"]]: K["__type"];
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
declare const HEADER_EVENT_ID = "x-event-id";
|
|
259
|
+
declare const HEADER_CORRELATION_ID = "x-correlation-id";
|
|
260
|
+
declare const HEADER_TIMESTAMP = "x-timestamp";
|
|
261
|
+
declare const HEADER_SCHEMA_VERSION = "x-schema-version";
|
|
262
|
+
declare const HEADER_TRACEPARENT = "traceparent";
|
|
263
|
+
/** Monotonically increasing logical clock stamped by the producer for deduplication. */
|
|
264
|
+
declare const HEADER_LAMPORT_CLOCK = "x-lamport-clock";
|
|
265
|
+
/**
|
|
266
|
+
* Typed wrapper combining a parsed message payload with Kafka metadata
|
|
267
|
+
* and envelope headers.
|
|
268
|
+
*
|
|
269
|
+
* On **send**, the library auto-generates envelope headers
|
|
270
|
+
* (`x-event-id`, `x-correlation-id`, `x-timestamp`, `x-schema-version`).
|
|
271
|
+
*
|
|
272
|
+
* On **consume**, the library extracts those headers and assembles
|
|
273
|
+
* an `EventEnvelope` that is passed to the handler.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* await kafka.startConsumer(['orders'], async (envelope: EventEnvelope<Order>) => {
|
|
278
|
+
* console.log(envelope.payload.orderId); // typed payload
|
|
279
|
+
* console.log(envelope.correlationId); // auto-propagated
|
|
280
|
+
* console.log(envelope.eventId); // unique message ID
|
|
281
|
+
* });
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
interface EventEnvelope<T> {
|
|
285
|
+
/** Deserialized + validated message body. */
|
|
286
|
+
payload: T;
|
|
287
|
+
/** Topic the message was produced to / consumed from. */
|
|
288
|
+
topic: string;
|
|
289
|
+
/** Kafka partition (consume-side only, `-1` on send). */
|
|
290
|
+
partition: number;
|
|
291
|
+
/** Kafka offset (consume-side only, empty string on send). */
|
|
292
|
+
offset: string;
|
|
293
|
+
/** ISO-8601 timestamp set by the producer. */
|
|
294
|
+
timestamp: string;
|
|
295
|
+
/** Unique ID for this event (UUID v4). */
|
|
296
|
+
eventId: string;
|
|
297
|
+
/** Correlation ID — auto-propagated via AsyncLocalStorage. */
|
|
298
|
+
correlationId: string;
|
|
299
|
+
/** Schema version of the payload. */
|
|
300
|
+
schemaVersion: number;
|
|
301
|
+
/** W3C Trace Context `traceparent` header (set by OTel instrumentation). */
|
|
302
|
+
traceparent?: string;
|
|
303
|
+
/** All decoded Kafka headers for extensibility. */
|
|
304
|
+
headers: MessageHeaders;
|
|
305
|
+
}
|
|
306
|
+
interface EnvelopeCtx {
|
|
307
|
+
correlationId: string;
|
|
308
|
+
traceparent?: string;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Read the current envelope context (correlationId / traceparent) from ALS.
|
|
312
|
+
* Returns `undefined` outside of a Kafka consumer handler.
|
|
313
|
+
* @example
|
|
314
|
+
* ```ts
|
|
315
|
+
* const ctx = getEnvelopeContext();
|
|
316
|
+
* if (ctx) console.log('correlationId:', ctx.correlationId);
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
declare function getEnvelopeContext(): EnvelopeCtx | undefined;
|
|
320
|
+
/**
|
|
321
|
+
* Execute `fn` inside an envelope context so nested sends inherit correlationId.
|
|
322
|
+
* Automatically called by the consumer pipeline — use this in tests or manual flows.
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* await runWithEnvelopeContext({ correlationId: 'abc-123' }, async () => {
|
|
326
|
+
* await kafka.sendMessage('orders.created', payload); // inherits correlationId
|
|
327
|
+
* });
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
declare function runWithEnvelopeContext<R>(ctx: EnvelopeCtx, fn: () => R): R;
|
|
331
|
+
/** Options accepted by `buildEnvelopeHeaders`. */
|
|
332
|
+
interface EnvelopeHeaderOptions {
|
|
333
|
+
correlationId?: string;
|
|
334
|
+
schemaVersion?: number;
|
|
335
|
+
eventId?: string;
|
|
336
|
+
headers?: MessageHeaders;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Generate envelope headers for the send path.
|
|
340
|
+
*
|
|
341
|
+
* Priority for `correlationId`:
|
|
342
|
+
* explicit option → ALS context → new UUID.
|
|
343
|
+
*/
|
|
344
|
+
declare function buildEnvelopeHeaders(options?: EnvelopeHeaderOptions): MessageHeaders;
|
|
345
|
+
/**
|
|
346
|
+
* Decode kafkajs headers (`Record<string, Buffer | string | undefined>`)
|
|
347
|
+
* into plain `Record<string, string>`.
|
|
348
|
+
*/
|
|
349
|
+
declare function decodeHeaders(raw: Record<string, Buffer | string | (Buffer | string)[] | undefined> | undefined): MessageHeaders;
|
|
350
|
+
/**
|
|
351
|
+
* Build an `EventEnvelope` from a consumed kafkajs message.
|
|
352
|
+
* Tolerates missing envelope headers — generates defaults so messages
|
|
353
|
+
* from non-envelope producers still work.
|
|
354
|
+
*/
|
|
355
|
+
declare function extractEnvelope<T>(payload: T, headers: MessageHeaders, topic: string, partition: number, offset: string): EventEnvelope<T>;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Metadata exposed to batch consumer handlers.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```ts
|
|
362
|
+
* await kafka.startBatchConsumer(['events'], async (envelopes, meta) => {
|
|
363
|
+
* console.log(`Partition ${meta.partition}, HWM ${meta.highWatermark}`);
|
|
364
|
+
* await db.insertMany(envelopes.map(e => e.payload));
|
|
365
|
+
* meta.resolveOffset(envelopes.at(-1)!.offset);
|
|
366
|
+
* await meta.commitOffsetsIfNecessary();
|
|
367
|
+
* }, { autoCommit: false });
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
interface BatchMeta {
|
|
371
|
+
/** Partition number for this batch. */
|
|
372
|
+
partition: number;
|
|
373
|
+
/**
|
|
374
|
+
* Highest offset available on the broker for this partition.
|
|
375
|
+
* `null` when the message is being replayed via a retry topic consumer —
|
|
376
|
+
* in that path the broker high-watermark is not accessible without an admin
|
|
377
|
+
* call. Do not use this for lag calculation in the retry path.
|
|
378
|
+
*/
|
|
379
|
+
highWatermark: string | null;
|
|
380
|
+
/** Send a heartbeat to the broker to prevent session timeout. */
|
|
381
|
+
heartbeat(): Promise<void>;
|
|
382
|
+
/** Mark an offset as processed (for manual offset management). */
|
|
383
|
+
resolveOffset(offset: string): void;
|
|
384
|
+
/** Commit offsets if the auto-commit threshold has been reached. */
|
|
385
|
+
commitOffsetsIfNecessary(): Promise<void>;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Configuration for consumer retry behavior.
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```ts
|
|
392
|
+
* await kafka.startConsumer(['orders'], handler, {
|
|
393
|
+
* retry: { maxRetries: 5, backoffMs: 500, maxBackoffMs: 10_000 },
|
|
394
|
+
* });
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
397
|
+
interface RetryOptions {
|
|
398
|
+
/** Maximum number of retry attempts before giving up. */
|
|
399
|
+
maxRetries: number;
|
|
400
|
+
/** Base delay for exponential backoff in ms. Default: `1000`. */
|
|
401
|
+
backoffMs?: number;
|
|
402
|
+
/** Maximum delay cap for exponential backoff in ms. Default: `30000`. */
|
|
403
|
+
maxBackoffMs?: number;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Options for Lamport Clock-based message deduplication.
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```ts
|
|
410
|
+
* await kafka.startConsumer(['payments'], handler, {
|
|
411
|
+
* deduplication: { strategy: 'dlq' },
|
|
412
|
+
* dlq: true,
|
|
413
|
+
* });
|
|
414
|
+
* ```
|
|
415
|
+
*
|
|
416
|
+
* The producer stamps every outgoing message with a monotonically increasing
|
|
417
|
+
* `x-lamport-clock` header. The consumer tracks the last processed value per
|
|
418
|
+
* `topic:partition` and skips any message whose clock is not strictly greater
|
|
419
|
+
* than that value.
|
|
420
|
+
*
|
|
421
|
+
* Messages that arrive without the `x-lamport-clock` header are passed through
|
|
422
|
+
* unchanged (backwards-compatible with producers that don't use this library).
|
|
423
|
+
*
|
|
424
|
+
* **In-session limitation**: deduplication state lives in memory and is reset
|
|
425
|
+
* whenever the process restarts or `stopConsumer` is called. After a restart,
|
|
426
|
+
* previously processed messages with `clock ≤ N` will be re-processed until
|
|
427
|
+
* their offsets catch up to the high-watermark again. The same applies after a
|
|
428
|
+
* rebalance: the instance that receives the partition begins with empty state.
|
|
429
|
+
* This is a fundamental limitation of the in-memory Lamport clock approach —
|
|
430
|
+
* it provides deduplication only within a single process session.
|
|
431
|
+
*/
|
|
432
|
+
interface DeduplicationOptions {
|
|
433
|
+
/**
|
|
434
|
+
* What to do with detected duplicate messages:
|
|
435
|
+
* - `'drop'` — silently discard. No routing, no callback. **(default)**
|
|
436
|
+
* - `'dlq'` — forward to `<topic>.dlq` with reason metadata headers.
|
|
437
|
+
* Requires `dlq: true` on the consumer options.
|
|
438
|
+
* - `'topic'` — forward to `<topic>.duplicates` (or `duplicatesTopic` if set).
|
|
439
|
+
*/
|
|
440
|
+
strategy?: "dlq" | "topic" | "drop";
|
|
441
|
+
/**
|
|
442
|
+
* Custom destination topic for `strategy: 'topic'`.
|
|
443
|
+
* Defaults to `<consumedTopic>.duplicates`.
|
|
444
|
+
*/
|
|
445
|
+
duplicatesTopic?: string;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Options for the per-partition circuit breaker.
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```ts
|
|
452
|
+
* await kafka.startConsumer(['payments'], handler, {
|
|
453
|
+
* circuitBreaker: {
|
|
454
|
+
* threshold: 5,
|
|
455
|
+
* recoveryMs: 30_000,
|
|
456
|
+
* windowSize: 20,
|
|
457
|
+
* halfOpenSuccesses: 2,
|
|
458
|
+
* },
|
|
459
|
+
* dlq: true,
|
|
460
|
+
* });
|
|
461
|
+
* ```
|
|
462
|
+
*
|
|
463
|
+
* The circuit breaker tracks recent message outcomes in a **sliding window** and
|
|
464
|
+
* opens (pauses the partition) when too many failures accumulate:
|
|
465
|
+
*
|
|
466
|
+
* - **CLOSED** — normal operation. Each DLQ route or successful delivery is recorded
|
|
467
|
+
* in the window. When `failuresInWindow >= threshold` the circuit opens.
|
|
468
|
+
* - **OPEN** — the partition is paused. After `recoveryMs` the circuit moves to HALF-OPEN.
|
|
469
|
+
* - **HALF-OPEN** — the partition is resumed. If the next `halfOpenSuccesses` messages
|
|
470
|
+
* succeed the circuit closes; a new failure immediately re-opens it.
|
|
471
|
+
*/
|
|
472
|
+
interface CircuitBreakerOptions {
|
|
473
|
+
/**
|
|
474
|
+
* Number of failures within the sliding window required to open the circuit.
|
|
475
|
+
* A failure is any message that ends up in the DLQ.
|
|
476
|
+
* Default: `5`.
|
|
477
|
+
*/
|
|
478
|
+
threshold?: number;
|
|
479
|
+
/**
|
|
480
|
+
* Time (ms) to keep the circuit OPEN before attempting recovery (HALF_OPEN).
|
|
481
|
+
* Default: `30_000` (30 s).
|
|
482
|
+
*/
|
|
483
|
+
recoveryMs?: number;
|
|
484
|
+
/**
|
|
485
|
+
* Number of outcomes (successes + failures) to keep in the sliding window.
|
|
486
|
+
* Default: `threshold * 2` (minimum `10`).
|
|
487
|
+
*/
|
|
488
|
+
windowSize?: number;
|
|
489
|
+
/**
|
|
490
|
+
* Number of consecutive successes in HALF-OPEN state required to close the
|
|
491
|
+
* circuit. Default: `1`.
|
|
492
|
+
*/
|
|
493
|
+
halfOpenSuccesses?: number;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Interceptor hooks for consumer message processing.
|
|
497
|
+
* All methods are optional — implement only what you need.
|
|
498
|
+
*
|
|
499
|
+
* Interceptors are per-consumer. For client-wide hooks (e.g. OTel),
|
|
500
|
+
* use `KafkaInstrumentation` instead.
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* ```ts
|
|
504
|
+
* const logger: ConsumerInterceptor = {
|
|
505
|
+
* async before(envelope) { console.log('Processing', envelope.eventId); },
|
|
506
|
+
* async after(envelope) { console.log('Done', envelope.eventId); },
|
|
507
|
+
* async onError(envelope, err) { console.error('Failed', err.message); },
|
|
508
|
+
* };
|
|
509
|
+
*
|
|
510
|
+
* await kafka.startConsumer(['orders'], handler, { interceptors: [logger] });
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
interface ConsumerInterceptor<T extends TopicMapConstraint<T> = TTopicMessageMap> {
|
|
514
|
+
/** Called before the message handler. */
|
|
515
|
+
before?(envelope: EventEnvelope<T[keyof T]>): Promise<void> | void;
|
|
516
|
+
/** Called after the message handler succeeds. */
|
|
517
|
+
after?(envelope: EventEnvelope<T[keyof T]>): Promise<void> | void;
|
|
518
|
+
/** Called when the message handler throws. */
|
|
519
|
+
onError?(envelope: EventEnvelope<T[keyof T]>, error: Error): Promise<void> | void;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Return value of `KafkaInstrumentation.beforeConsume`.
|
|
523
|
+
*
|
|
524
|
+
* - `() => void` — legacy form: a cleanup function called after the handler.
|
|
525
|
+
* - Object form:
|
|
526
|
+
* - `cleanup?()` — called after the handler (same as the legacy function form).
|
|
527
|
+
* - `wrap?(fn)` — wraps the handler execution; call `fn()` inside the desired
|
|
528
|
+
* async context (e.g. `context.with(spanCtx, fn)` for OpenTelemetry). Multiple
|
|
529
|
+
* wraps from different instrumentations are composed in declaration order,
|
|
530
|
+
* so the first instrumentation's wrap is the outermost.
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* ```ts
|
|
534
|
+
* // Cleanup-only (legacy form):
|
|
535
|
+
* beforeConsume(envelope) {
|
|
536
|
+
* const timer = startTimer();
|
|
537
|
+
* return () => timer.end();
|
|
538
|
+
* }
|
|
539
|
+
*
|
|
540
|
+
* // Wrap form — run handler inside an OTel span context:
|
|
541
|
+
* beforeConsume(envelope) {
|
|
542
|
+
* const ctx = propagator.extract(ROOT_CONTEXT, envelope.headers);
|
|
543
|
+
* const span = tracer.startSpan(envelope.topic, {}, ctx);
|
|
544
|
+
* return {
|
|
545
|
+
* wrap: (fn) => context.with(trace.setSpan(ctx, span), fn),
|
|
546
|
+
* cleanup: () => span.end(),
|
|
547
|
+
* };
|
|
548
|
+
* }
|
|
549
|
+
* ```
|
|
550
|
+
*/
|
|
551
|
+
type BeforeConsumeResult = (() => void) | {
|
|
552
|
+
cleanup?(): void;
|
|
553
|
+
wrap?(fn: () => Promise<void>): Promise<void>;
|
|
554
|
+
};
|
|
555
|
+
/**
|
|
556
|
+
* Reason a message was sent to the DLQ.
|
|
557
|
+
* - `'handler-error'` — the consumer handler threw after all retry attempts.
|
|
558
|
+
* - `'validation-error'` — schema validation failed before the handler ran.
|
|
559
|
+
* - `'lamport-clock-duplicate'` — message was identified as a Lamport-clock duplicate
|
|
560
|
+
* and `deduplication.strategy` is `'dlq'`.
|
|
561
|
+
* - `'ttl-expired'` — message age exceeded `messageTtlMs` before the handler ran.
|
|
562
|
+
*/
|
|
563
|
+
type DlqReason = "handler-error" | "validation-error" | "lamport-clock-duplicate" | "ttl-expired";
|
|
564
|
+
/**
|
|
565
|
+
* Client-wide instrumentation hooks for both send and consume paths.
|
|
566
|
+
* Use this for cross-cutting concerns like tracing and metrics.
|
|
567
|
+
*
|
|
568
|
+
* @see `otelInstrumentation()` from `@drarzter/kafka-client/otel`
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* ```ts
|
|
572
|
+
* const tracing: KafkaInstrumentation = {
|
|
573
|
+
* beforeSend(topic, headers) {
|
|
574
|
+
* headers['traceparent'] = getCurrentTraceId();
|
|
575
|
+
* },
|
|
576
|
+
* beforeConsume(envelope) {
|
|
577
|
+
* const span = tracer.startSpan(envelope.topic);
|
|
578
|
+
* return { cleanup: () => span.end() };
|
|
579
|
+
* },
|
|
580
|
+
* onDlq(envelope, reason) {
|
|
581
|
+
* metrics.increment('kafka.dlq', { topic: envelope.topic, reason });
|
|
582
|
+
* },
|
|
583
|
+
* };
|
|
584
|
+
*
|
|
585
|
+
* const kafka = new KafkaClient(config, groupId, { instrumentation: [tracing] });
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
588
|
+
interface KafkaInstrumentation {
|
|
589
|
+
/** Called before sending — can mutate `headers` (e.g. inject `traceparent`). */
|
|
590
|
+
beforeSend?(topic: string, headers: MessageHeaders): void;
|
|
591
|
+
/** Called after a successful send. */
|
|
592
|
+
afterSend?(topic: string): void;
|
|
593
|
+
/**
|
|
594
|
+
* Called before the consumer handler.
|
|
595
|
+
* Return a cleanup function (legacy) or a `BeforeConsumeResult` object with
|
|
596
|
+
* optional `cleanup` and `wrap`. Use `wrap` to run the handler inside a
|
|
597
|
+
* specific async context (e.g. an active OpenTelemetry span).
|
|
598
|
+
*/
|
|
599
|
+
beforeConsume?(envelope: EventEnvelope<any>): BeforeConsumeResult | void;
|
|
600
|
+
/** Called when the consumer handler throws. */
|
|
601
|
+
onConsumeError?(envelope: EventEnvelope<any>, error: Error): void;
|
|
602
|
+
/**
|
|
603
|
+
* Called when a message is queued for retry.
|
|
604
|
+
* Fires for both in-process retries (before the backoff sleep) and
|
|
605
|
+
* retry-topic routing (EOS and non-EOS paths).
|
|
606
|
+
*/
|
|
607
|
+
onRetry?(envelope: EventEnvelope<any>, attempt: number, maxRetries: number): void;
|
|
608
|
+
/** Called when a message is routed to a DLQ topic. */
|
|
609
|
+
onDlq?(envelope: EventEnvelope<any>, reason: DlqReason): void;
|
|
610
|
+
/**
|
|
611
|
+
* Called when a duplicate message is detected via the Lamport clock.
|
|
612
|
+
* Fires regardless of the configured `deduplication.strategy`.
|
|
613
|
+
*/
|
|
614
|
+
onDuplicate?(envelope: EventEnvelope<any>, strategy: "drop" | "dlq" | "topic"): void;
|
|
615
|
+
/**
|
|
616
|
+
* Called after the consumer handler successfully processes a message.
|
|
617
|
+
* Use this as a success counter for error-rate calculations.
|
|
618
|
+
* Fires for both single-message and batch consumers (once per envelope).
|
|
619
|
+
*/
|
|
620
|
+
onMessage?(envelope: EventEnvelope<any>): void;
|
|
621
|
+
/** Called when a partition circuit opens (consumer paused due to DLQ failures). */
|
|
622
|
+
onCircuitOpen?(topic: string, partition: number): void;
|
|
623
|
+
/** Called when the circuit moves to half-open (partition resumed for a probe). */
|
|
624
|
+
onCircuitHalfOpen?(topic: string, partition: number): void;
|
|
625
|
+
/** Called when the circuit closes (normal operation restored). */
|
|
626
|
+
onCircuitClose?(topic: string, partition: number): void;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Context passed to `onTtlExpired` when a message is dropped because it
|
|
630
|
+
* exceeded `messageTtlMs` and `dlq` is not enabled.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```ts
|
|
634
|
+
* const kafka = new KafkaClient(config, groupId, {
|
|
635
|
+
* onTtlExpired: ({ topic, ageMs, messageTtlMs }) => {
|
|
636
|
+
* console.warn(`Message on ${topic} expired: age=${ageMs}ms, ttl=${messageTtlMs}ms`);
|
|
637
|
+
* },
|
|
638
|
+
* });
|
|
639
|
+
* ```
|
|
640
|
+
*/
|
|
641
|
+
interface TtlExpiredContext {
|
|
642
|
+
/** Topic the message was consumed from. */
|
|
643
|
+
topic: string;
|
|
644
|
+
/** Actual age of the message in ms at the time it was dropped. */
|
|
645
|
+
ageMs: number;
|
|
646
|
+
/** The configured TTL threshold (`messageTtlMs`). */
|
|
647
|
+
messageTtlMs: number;
|
|
648
|
+
/** Original Kafka message headers (correlationId, traceparent, etc.). */
|
|
649
|
+
headers: MessageHeaders;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Context passed to `onMessageLost` when a message is silently dropped
|
|
653
|
+
* (handler threw and `dlq` is not enabled).
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* ```ts
|
|
657
|
+
* const kafka = new KafkaClient(config, groupId, {
|
|
658
|
+
* onMessageLost: ({ topic, error, attempt }) => {
|
|
659
|
+
* alerting.fire('kafka.message-lost', { topic, error: error.message, attempt });
|
|
660
|
+
* },
|
|
661
|
+
* });
|
|
662
|
+
* ```
|
|
663
|
+
*/
|
|
664
|
+
interface MessageLostContext {
|
|
665
|
+
/** Topic the message was consumed from. */
|
|
666
|
+
topic: string;
|
|
667
|
+
/** Error that caused the message to be dropped. */
|
|
668
|
+
error: Error;
|
|
669
|
+
/** Number of processing attempts (0 = validation failure, before handler ran). */
|
|
670
|
+
attempt: number;
|
|
671
|
+
/** Original Kafka message headers (correlationId, traceparent, etc.). */
|
|
672
|
+
headers: MessageHeaders;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Options for consumer subscribe retry when topic doesn't exist yet.
|
|
676
|
+
*
|
|
677
|
+
* @example
|
|
678
|
+
* ```ts
|
|
679
|
+
* await kafka.startConsumer(['orders.created'], handler, {
|
|
680
|
+
* subscribeRetry: { retries: 10, backoffMs: 2_000 },
|
|
681
|
+
* });
|
|
682
|
+
* ```
|
|
683
|
+
*/
|
|
684
|
+
interface SubscribeRetryOptions {
|
|
685
|
+
/** Maximum number of subscribe attempts. Default: `5`. */
|
|
686
|
+
retries?: number;
|
|
687
|
+
/** Delay between retries in ms. Default: `5000`. */
|
|
688
|
+
backoffMs?: number;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Options for configuring a Kafka consumer.
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* ```ts
|
|
695
|
+
* await kafka.startConsumer(['orders.created'], handler, {
|
|
696
|
+
* groupId: 'billing-service',
|
|
697
|
+
* retry: { maxRetries: 5, backoffMs: 1000 },
|
|
698
|
+
* dlq: true,
|
|
699
|
+
* deduplication: { strategy: 'drop' },
|
|
700
|
+
* circuitBreaker: { threshold: 3, recoveryMs: 60_000 },
|
|
701
|
+
* interceptors: [loggingInterceptor],
|
|
702
|
+
* });
|
|
703
|
+
* ```
|
|
704
|
+
*/
|
|
705
|
+
interface ConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> {
|
|
706
|
+
/** Override the default consumer group ID from the constructor. */
|
|
707
|
+
groupId?: string;
|
|
708
|
+
/** Start reading from earliest offset. Default: `false`. */
|
|
709
|
+
fromBeginning?: boolean;
|
|
710
|
+
/** Automatically commit offsets. Default: `true`. */
|
|
711
|
+
autoCommit?: boolean;
|
|
712
|
+
/** Retry policy for failed message processing. */
|
|
713
|
+
retry?: RetryOptions;
|
|
714
|
+
/** Send failed messages to a Dead Letter Queue (`<topic>.dlq`). */
|
|
715
|
+
dlq?: boolean;
|
|
716
|
+
/** Interceptors called before/after each message. */
|
|
717
|
+
interceptors?: ConsumerInterceptor<T>[];
|
|
718
|
+
/** @internal Schema map populated by @SubscribeTo when descriptors have schemas. */
|
|
719
|
+
schemas?: Map<string, SchemaLike>;
|
|
720
|
+
/** Retry config for `consumer.subscribe()` when the topic doesn't exist yet. */
|
|
721
|
+
subscribeRetry?: SubscribeRetryOptions;
|
|
722
|
+
/**
|
|
723
|
+
* Route failed messages through a Kafka retry topic (`<topic>.retry`) instead of sleeping
|
|
724
|
+
* in-process between retry attempts. Requires `retry` to be set.
|
|
725
|
+
*
|
|
726
|
+
* Benefits over in-process retry:
|
|
727
|
+
* - Retry messages survive consumer restarts (durable)
|
|
728
|
+
* - Original consumer is not blocked during retry delay
|
|
729
|
+
*
|
|
730
|
+
* A companion consumer on `<topic>.retry` is auto-started with group `<groupId>-retry`.
|
|
731
|
+
* On exhaustion, messages go to `<topic>.dlq` (if `dlq: true`) or `onMessageLost`.
|
|
732
|
+
*/
|
|
733
|
+
retryTopics?: boolean;
|
|
734
|
+
/**
|
|
735
|
+
* Timeout (ms) for waiting until each retry level consumer receives partition
|
|
736
|
+
* assignments after `startConsumer` connects. Default: `10000`.
|
|
737
|
+
* Increase this when the broker is slow to rebalance.
|
|
738
|
+
*/
|
|
739
|
+
retryTopicAssignmentTimeoutMs?: number;
|
|
740
|
+
/**
|
|
741
|
+
* Log a warning if the message handler has not resolved within this window (ms).
|
|
742
|
+
* The handler is not cancelled — this is a diagnostic aid to surface stuck handlers
|
|
743
|
+
* before they starve a partition.
|
|
744
|
+
*/
|
|
745
|
+
handlerTimeoutMs?: number;
|
|
746
|
+
/**
|
|
747
|
+
* Enable Lamport Clock deduplication.
|
|
748
|
+
* Requires the producer to stamp messages with `x-lamport-clock` headers
|
|
749
|
+
* (done automatically when using this library's send methods).
|
|
750
|
+
* Messages without the header are passed through unchanged.
|
|
751
|
+
*/
|
|
752
|
+
deduplication?: DeduplicationOptions;
|
|
753
|
+
/**
|
|
754
|
+
* Drop messages older than this threshold, measured in milliseconds from
|
|
755
|
+
* the `x-timestamp` header set by the producer.
|
|
756
|
+
*
|
|
757
|
+
* Expired messages are routed to `{topic}.dlq` when `dlq: true`, otherwise
|
|
758
|
+
* `onTtlExpired` is called. The handler is never invoked for an expired message.
|
|
759
|
+
*/
|
|
760
|
+
messageTtlMs?: number;
|
|
761
|
+
/**
|
|
762
|
+
* Automatically pause a partition when it accumulates too many consecutive
|
|
763
|
+
* failures, then resume after a recovery window.
|
|
764
|
+
*
|
|
765
|
+
* See `CircuitBreakerOptions` for the sliding-window semantics.
|
|
766
|
+
*/
|
|
767
|
+
circuitBreaker?: CircuitBreakerOptions;
|
|
768
|
+
/**
|
|
769
|
+
* Max number of messages buffered in the `consume()` iterator queue before
|
|
770
|
+
* the partition is paused. The partition resumes when the queue drains below 50%.
|
|
771
|
+
* Only applies to `consume()`. Default: unbounded.
|
|
772
|
+
*/
|
|
773
|
+
queueHighWaterMark?: number;
|
|
774
|
+
/**
|
|
775
|
+
* Kafka partition assignment strategy for this consumer group.
|
|
776
|
+
* - `'cooperative-sticky'` — **(default)** partitions move as little as possible on rebalance.
|
|
777
|
+
* Best for horizontal scaling: only the partitions that need to be reassigned are moved.
|
|
778
|
+
* - `'roundrobin'` — distributes partitions as evenly as possible across consumers.
|
|
779
|
+
* - `'range'` — assigns contiguous partition ranges; can cause uneven distribution with multiple topics.
|
|
780
|
+
*/
|
|
781
|
+
partitionAssigner?: "roundrobin" | "range" | "cooperative-sticky";
|
|
782
|
+
/**
|
|
783
|
+
* Called when a message is dropped due to TTL expiration (`messageTtlMs`).
|
|
784
|
+
* Fires instead of `onMessageLost` for expired messages when `dlq` is not enabled.
|
|
785
|
+
* When `dlq: true`, expired messages go to the DLQ and this callback is NOT called.
|
|
786
|
+
*
|
|
787
|
+
* **Per-consumer override**: takes precedence over `KafkaClientOptions.onTtlExpired`
|
|
788
|
+
* when both are set. Use this to handle TTL expiry differently per consumer group.
|
|
789
|
+
*/
|
|
790
|
+
onTtlExpired?: (ctx: TtlExpiredContext) => void | Promise<void>;
|
|
791
|
+
/**
|
|
792
|
+
* Called when a message is silently dropped after all retries are exhausted
|
|
793
|
+
* and `dlq` is not enabled.
|
|
794
|
+
*
|
|
795
|
+
* **Per-consumer override**: takes precedence over `KafkaClientOptions.onMessageLost`
|
|
796
|
+
* when both are set. Use this for consumer-specific alerting or dead-message logging.
|
|
797
|
+
*/
|
|
798
|
+
onMessageLost?: (ctx: MessageLostContext) => void | Promise<void>;
|
|
799
|
+
/**
|
|
800
|
+
* Called before each retry attempt (both in-process and retry-topic paths).
|
|
801
|
+
*
|
|
802
|
+
* **Per-consumer override**: fires in addition to any global instrumentation hooks.
|
|
803
|
+
* Use this for per-consumer retry metrics or structured logging.
|
|
804
|
+
*/
|
|
805
|
+
onRetry?: (envelope: EventEnvelope<any>, attempt: number, maxRetries: number) => void | Promise<void>;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Handle returned by `startConsumer` / `startBatchConsumer`.
|
|
809
|
+
*
|
|
810
|
+
* @example
|
|
811
|
+
* ```ts
|
|
812
|
+
* const handle = await kafka.startConsumer(['orders'], handler);
|
|
813
|
+
* // later, on shutdown:
|
|
814
|
+
* await handle.stop();
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
817
|
+
interface ConsumerHandle {
|
|
818
|
+
/** The consumer group ID this consumer is running under. */
|
|
819
|
+
groupId: string;
|
|
820
|
+
/** Stop this consumer. Equivalent to calling `client.stopConsumer(groupId)`. */
|
|
821
|
+
stop(): Promise<void>;
|
|
822
|
+
/**
|
|
823
|
+
* Resolves once the consumer has received its first partition assignment from
|
|
824
|
+
* the broker (i.e. it has joined the group and is ready to receive messages).
|
|
825
|
+
*
|
|
826
|
+
* Use this in tests and startup probes instead of a fixed `setTimeout` delay:
|
|
827
|
+
*
|
|
828
|
+
* @example
|
|
829
|
+
* ```ts
|
|
830
|
+
* const handle = await kafka.startConsumer(['orders'], handler);
|
|
831
|
+
* await handle.ready(); // wait for partition assignment — no fixed sleep needed
|
|
832
|
+
* await kafka.sendMessage('orders', payload);
|
|
833
|
+
* ```
|
|
834
|
+
*/
|
|
835
|
+
ready(): Promise<void>;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Context passed to the `transaction()` callback with type-safe send methods.
|
|
839
|
+
*
|
|
840
|
+
* @example
|
|
841
|
+
* ```ts
|
|
842
|
+
* await kafka.transaction(async (tx) => {
|
|
843
|
+
* await tx.send('inventory.reserved', { itemId: 'a', qty: 1 });
|
|
844
|
+
* await tx.sendBatch('audit.log', [
|
|
845
|
+
* { value: { action: 'reserve', itemId: 'a' } },
|
|
846
|
+
* ]);
|
|
847
|
+
* });
|
|
848
|
+
* ```
|
|
849
|
+
*/
|
|
850
|
+
interface TransactionContext<T extends TopicMapConstraint<T>> {
|
|
851
|
+
send<K extends keyof T>(topic: K, message: T[K], options?: SendOptions): Promise<void>;
|
|
852
|
+
send<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(descriptor: D, message: D["__type"], options?: SendOptions): Promise<void>;
|
|
853
|
+
sendBatch<K extends keyof T>(topic: K, messages: Array<BatchMessageItem<T[K]>>, options?: BatchSendOptions): Promise<void>;
|
|
854
|
+
sendBatch<D extends TopicDescriptor<string & keyof T, T[string & keyof T]>>(descriptor: D, messages: Array<BatchMessageItem<D["__type"]>>, options?: BatchSendOptions): Promise<void>;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Transactional context passed to each `startTransactionalConsumer` handler invocation.
|
|
858
|
+
*
|
|
859
|
+
* Every call to `send` or `sendBatch` on this object is part of the same Kafka
|
|
860
|
+
* transaction as the source message offset commit. Either all sends and the offset
|
|
861
|
+
* commit succeed atomically, or the transaction is aborted and the source message
|
|
862
|
+
* is redelivered.
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* ```ts
|
|
866
|
+
* await kafka.startTransactionalConsumer(['orders.created'], async (envelope, tx) => {
|
|
867
|
+
* await tx.send('inventory.reserved', { orderId: envelope.payload.orderId, qty: 1 });
|
|
868
|
+
* await tx.sendBatch('audit.log', [{ value: { action: 'reserve', orderId: envelope.payload.orderId } }]);
|
|
869
|
+
* });
|
|
870
|
+
* ```
|
|
871
|
+
*/
|
|
872
|
+
interface TransactionalHandlerContext<T extends TopicMapConstraint<T>> {
|
|
873
|
+
/**
|
|
874
|
+
* Send a message as part of this message's transaction.
|
|
875
|
+
* The send is staged — it only becomes visible to consumers after the transaction commits.
|
|
876
|
+
*/
|
|
877
|
+
send<K extends keyof T>(topic: K, message: T[K], options?: SendOptions): Promise<void>;
|
|
878
|
+
/**
|
|
879
|
+
* Send multiple messages as part of this message's transaction.
|
|
880
|
+
* All messages are staged together and become visible only after commit.
|
|
881
|
+
*/
|
|
882
|
+
sendBatch<K extends keyof T>(topic: K, messages: Array<BatchMessageItem<T[K]>>, options?: BatchSendOptions): Promise<void>;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Routing configuration for `startRoutedConsumer`.
|
|
886
|
+
*
|
|
887
|
+
* Messages are dispatched to the handler whose key matches the value of `header`.
|
|
888
|
+
* Unmatched messages are forwarded to `fallback` if provided, or silently skipped.
|
|
889
|
+
*
|
|
890
|
+
* @example
|
|
891
|
+
* ```ts
|
|
892
|
+
* await kafka.startRoutedConsumer(['events'], {
|
|
893
|
+
* header: 'x-event-type',
|
|
894
|
+
* routes: {
|
|
895
|
+
* 'order.created': async (e) => handleOrderCreated(e.payload),
|
|
896
|
+
* 'order.cancelled': async (e) => handleOrderCancelled(e.payload),
|
|
897
|
+
* },
|
|
898
|
+
* fallback: async (e) => logger.warn('Unknown event type', e.headers),
|
|
899
|
+
* });
|
|
900
|
+
* ```
|
|
901
|
+
*/
|
|
902
|
+
interface RoutingOptions<E> {
|
|
903
|
+
/** Header whose value determines which handler is invoked. */
|
|
904
|
+
header: string;
|
|
905
|
+
/**
|
|
906
|
+
* Map of header value → handler.
|
|
907
|
+
* The handler with the matching key is called for each message.
|
|
908
|
+
*/
|
|
909
|
+
routes: Record<string, (envelope: EventEnvelope<E>) => Promise<void>>;
|
|
910
|
+
/**
|
|
911
|
+
* Called when no route matches the header value (or the header is absent).
|
|
912
|
+
* If omitted, unmatched messages are silently skipped.
|
|
913
|
+
*/
|
|
914
|
+
fallback?: (envelope: EventEnvelope<E>) => Promise<void>;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Metadata passed to the `startWindowConsumer` handler on each flush.
|
|
918
|
+
*
|
|
919
|
+
* @example
|
|
920
|
+
* ```ts
|
|
921
|
+
* await kafka.startWindowConsumer('events', async (batch, meta) => {
|
|
922
|
+
* console.log(`Flush: ${batch.length} events, trigger=${meta.trigger}`);
|
|
923
|
+
* console.log(`Window: ${meta.windowEnd - meta.windowStart} ms`);
|
|
924
|
+
* await db.insertMany(batch.map(e => e.payload));
|
|
925
|
+
* }, { maxMessages: 100, maxMs: 5_000 });
|
|
926
|
+
* ```
|
|
927
|
+
*/
|
|
928
|
+
interface WindowMeta {
|
|
929
|
+
/** What triggered this flush: accumulated `maxMessages` messages, or the `maxMs` timer. */
|
|
930
|
+
trigger: "size" | "time";
|
|
931
|
+
/** Unix timestamp (ms) of the first message that entered the current window. */
|
|
932
|
+
windowStart: number;
|
|
933
|
+
/** Unix timestamp (ms) when the flush was initiated. */
|
|
934
|
+
windowEnd: number;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Options for `startWindowConsumer`.
|
|
938
|
+
*
|
|
939
|
+
* Extends `ConsumerOptions` — all standard consumer settings apply.
|
|
940
|
+
* `retryTopics`, `retryTopicAssignmentTimeoutMs`, and `queueHighWaterMark`
|
|
941
|
+
* are excluded: they are incompatible with windowed accumulation.
|
|
942
|
+
*/
|
|
943
|
+
interface WindowConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> extends Omit<ConsumerOptions<T>, "retryTopics" | "retryTopicAssignmentTimeoutMs" | "queueHighWaterMark"> {
|
|
944
|
+
/**
|
|
945
|
+
* Maximum number of messages to accumulate before flushing.
|
|
946
|
+
* When the buffer reaches this size the handler is called immediately,
|
|
947
|
+
* regardless of how much time has elapsed since the first message.
|
|
948
|
+
*/
|
|
949
|
+
maxMessages: number;
|
|
950
|
+
/**
|
|
951
|
+
* Maximum time (ms) to wait after the first message before flushing.
|
|
952
|
+
* When this timer expires the handler is called with whatever messages
|
|
953
|
+
* have accumulated so far — even if the buffer is not full.
|
|
954
|
+
*/
|
|
955
|
+
maxMs: number;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export { type TransactionalHandlerContext 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 InferSchema as I, type WindowMeta as J, type KafkaInstrumentation as K, buildEnvelopeHeaders as L, type MessageHeaders as M, decodeHeaders as N, extractEnvelope as O, getEnvelopeContext as P, runWithEnvelopeContext as Q, type RetryOptions as R, type SchemaLike as S, type TopicMapConstraint as T, topic as U, type WindowConsumerOptions as W, type ConsumerOptions as a, type TopicDescriptor as b, type BatchMeta as c, type BatchSendOptions as d, type BeforeConsumeResult as e, type CircuitBreakerOptions as f, type CompressionType as g, type ConsumerHandle as h, type ConsumerInterceptor as i, type DlqReason as j, type EventEnvelope as k, HEADER_EVENT_ID as l, HEADER_LAMPORT_CLOCK as m, HEADER_SCHEMA_VERSION as n, HEADER_TIMESTAMP as o, HEADER_TRACEPARENT as p, type KafkaLogger as q, type KafkaMetrics as r, type MessageLostContext as s, type RoutingOptions as t, type SchemaParseContext as u, type SendOptions as v, type SubscribeRetryOptions as w, type TTopicMessageMap as x, type TopicsFrom as y, type TransactionContext as z };
|