@drarzter/kafka-client 0.5.6 → 0.6.3

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.
@@ -1,15 +1,44 @@
1
+ /**
2
+ * Context passed as the second argument to `SchemaLike.parse()`.
3
+ * Enables schema-registry adapters, version-aware migration, and
4
+ * header-driven parsing without coupling validators to Kafka internals.
5
+ *
6
+ * All fields are optional-friendly — validators that don't need the context
7
+ * can simply ignore the second argument.
8
+ */
9
+ interface SchemaParseContext {
10
+ /** Topic the message was produced to / consumed from. */
11
+ topic: string;
12
+ /** Decoded message headers (envelope headers included). */
13
+ headers: MessageHeaders;
14
+ /** Value of the `x-schema-version` header, defaults to `1`. */
15
+ version: number;
16
+ }
1
17
  /**
2
18
  * Any validation library with a `.parse()` method.
3
19
  * Works with Zod, Valibot, ArkType, or any custom validator.
4
20
  *
21
+ * The optional `ctx` argument carries topic/header/version metadata so
22
+ * validators can perform schema-registry lookups or version-aware migrations.
23
+ * Existing validators that only use the first argument continue to work
24
+ * unchanged — the second argument is silently ignored.
25
+ *
5
26
  * @example
6
27
  * ```ts
7
28
  * import { z } from 'zod';
8
29
  * const schema: SchemaLike<{ id: string }> = z.object({ id: z.string() });
30
+ *
31
+ * // Context-aware validator:
32
+ * const schema: SchemaLike<MyType> = {
33
+ * parse(data, ctx) {
34
+ * const version = ctx?.version ?? 1;
35
+ * return version >= 2 ? migrateV1toV2(data) : validateV1(data);
36
+ * }
37
+ * };
9
38
  * ```
10
39
  */
11
40
  interface SchemaLike<T = any> {
12
- parse(data: unknown): T | Promise<T>;
41
+ parse(data: unknown, ctx?: SchemaParseContext): T | Promise<T>;
13
42
  }
14
43
  /** Infer the output type from a SchemaLike. */
15
44
  type InferSchema<S extends SchemaLike> = S extends SchemaLike<infer T> ? T : never;
@@ -32,8 +61,8 @@ interface TopicDescriptor<N extends string = string, M extends Record<string, an
32
61
  *
33
62
  * @example
34
63
  * ```ts
35
- * // Without schema — type provided explicitly:
36
- * const OrderCreated = topic('order.created')<{ orderId: string; amount: number }>();
64
+ * // Without schema — explicit type via .type<T>():
65
+ * const OrderCreated = topic('order.created').type<{ orderId: string; amount: number }>();
37
66
  *
38
67
  * // With schema — type inferred from schema:
39
68
  * const OrderCreated = topic('order.created').schema(z.object({
@@ -50,16 +79,17 @@ interface TopicDescriptor<N extends string = string, M extends Record<string, an
50
79
  * ```
51
80
  */
52
81
  declare function topic<N extends string>(name: N): {
53
- <M extends Record<string, any>>(): TopicDescriptor<N, M>;
54
- schema<S extends SchemaLike<Record<string, any>>>(schema: S): TopicDescriptor<N, InferSchema<S>>;
82
+ /** Provide an explicit message type without a runtime schema. */
83
+ type: <M extends Record<string, any>>() => TopicDescriptor<N, M>;
84
+ schema: <S extends SchemaLike<Record<string, any>>>(schema: S) => TopicDescriptor<N, InferSchema<S>>;
55
85
  };
56
86
  /**
57
87
  * Build a topic-message map type from a union of TopicDescriptors.
58
88
  *
59
89
  * @example
60
90
  * ```ts
61
- * const OrderCreated = topic('order.created')<{ orderId: string }>();
62
- * const OrderCompleted = topic('order.completed')<{ completedAt: string }>();
91
+ * const OrderCreated = topic('order.created').type<{ orderId: string }>();
92
+ * const OrderCompleted = topic('order.completed').type<{ completedAt: string }>();
63
93
  *
64
94
  * type MyTopics = TopicsFrom<typeof OrderCreated | typeof OrderCompleted>;
65
95
  * // { 'order.created': { orderId: string }; 'order.completed': { completedAt: string } }
@@ -69,6 +99,77 @@ type TopicsFrom<D extends TopicDescriptor<any, any>> = {
69
99
  [K in D as K["__topic"]]: K["__type"];
70
100
  };
71
101
 
102
+ declare const HEADER_EVENT_ID = "x-event-id";
103
+ declare const HEADER_CORRELATION_ID = "x-correlation-id";
104
+ declare const HEADER_TIMESTAMP = "x-timestamp";
105
+ declare const HEADER_SCHEMA_VERSION = "x-schema-version";
106
+ declare const HEADER_TRACEPARENT = "traceparent";
107
+ /**
108
+ * Typed wrapper combining a parsed message payload with Kafka metadata
109
+ * and envelope headers.
110
+ *
111
+ * On **send**, the library auto-generates envelope headers
112
+ * (`x-event-id`, `x-correlation-id`, `x-timestamp`, `x-schema-version`).
113
+ *
114
+ * On **consume**, the library extracts those headers and assembles
115
+ * an `EventEnvelope` that is passed to the handler.
116
+ */
117
+ interface EventEnvelope<T> {
118
+ /** Deserialized + validated message body. */
119
+ payload: T;
120
+ /** Topic the message was produced to / consumed from. */
121
+ topic: string;
122
+ /** Kafka partition (consume-side only, `-1` on send). */
123
+ partition: number;
124
+ /** Kafka offset (consume-side only, empty string on send). */
125
+ offset: string;
126
+ /** ISO-8601 timestamp set by the producer. */
127
+ timestamp: string;
128
+ /** Unique ID for this event (UUID v4). */
129
+ eventId: string;
130
+ /** Correlation ID — auto-propagated via AsyncLocalStorage. */
131
+ correlationId: string;
132
+ /** Schema version of the payload. */
133
+ schemaVersion: number;
134
+ /** W3C Trace Context `traceparent` header (set by OTel instrumentation). */
135
+ traceparent?: string;
136
+ /** All decoded Kafka headers for extensibility. */
137
+ headers: MessageHeaders;
138
+ }
139
+ interface EnvelopeCtx {
140
+ correlationId: string;
141
+ traceparent?: string;
142
+ }
143
+ /** Read the current envelope context (correlationId / traceparent) from ALS. */
144
+ declare function getEnvelopeContext(): EnvelopeCtx | undefined;
145
+ /** Execute `fn` inside an envelope context so nested sends inherit correlationId. */
146
+ declare function runWithEnvelopeContext<R>(ctx: EnvelopeCtx, fn: () => R): R;
147
+ /** Options accepted by `buildEnvelopeHeaders`. */
148
+ interface EnvelopeHeaderOptions {
149
+ correlationId?: string;
150
+ schemaVersion?: number;
151
+ eventId?: string;
152
+ headers?: MessageHeaders;
153
+ }
154
+ /**
155
+ * Generate envelope headers for the send path.
156
+ *
157
+ * Priority for `correlationId`:
158
+ * explicit option → ALS context → new UUID.
159
+ */
160
+ declare function buildEnvelopeHeaders(options?: EnvelopeHeaderOptions): MessageHeaders;
161
+ /**
162
+ * Decode kafkajs headers (`Record<string, Buffer | string | undefined>`)
163
+ * into plain `Record<string, string>`.
164
+ */
165
+ declare function decodeHeaders(raw: Record<string, Buffer | string | (Buffer | string)[] | undefined> | undefined): MessageHeaders;
166
+ /**
167
+ * Build an `EventEnvelope` from a consumed kafkajs message.
168
+ * Tolerates missing envelope headers — generates defaults so messages
169
+ * from non-envelope producers still work.
170
+ */
171
+ declare function extractEnvelope<T>(payload: T, headers: MessageHeaders, topic: string, partition: number, offset: string): EventEnvelope<T>;
172
+
72
173
  /**
73
174
  * Mapping of topic names to their message types.
74
175
  * Define this interface to get type-safe publish/subscribe across your app.
@@ -165,6 +266,12 @@ interface ConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> {
165
266
  * On exhaustion, messages go to `<topic>.dlq` (if `dlq: true`) or `onMessageLost`.
166
267
  */
167
268
  retryTopics?: boolean;
269
+ /**
270
+ * Timeout (ms) for waiting until each retry level consumer receives partition
271
+ * assignments after `startConsumer` connects. Default: `10000`.
272
+ * Increase this when the broker is slow to rebalance.
273
+ */
274
+ retryTopicAssignmentTimeoutMs?: number;
168
275
  /**
169
276
  * Log a warning if the message handler has not resolved within this window (ms).
170
277
  * The handler is not cancelled — this is a diagnostic aid to surface stuck handlers
@@ -196,6 +303,21 @@ interface ConsumerInterceptor<T extends TopicMapConstraint<T> = TTopicMessageMap
196
303
  /** Called when the message handler throws. */
197
304
  onError?(envelope: EventEnvelope<T[keyof T]>, error: Error): Promise<void> | void;
198
305
  }
306
+ /**
307
+ * Return value of `KafkaInstrumentation.beforeConsume`.
308
+ *
309
+ * - `() => void` — legacy form: a cleanup function called after the handler.
310
+ * - Object form:
311
+ * - `cleanup?()` — called after the handler (same as the legacy function form).
312
+ * - `wrap?(fn)` — wraps the handler execution; call `fn()` inside the desired
313
+ * async context (e.g. `context.with(spanCtx, fn)` for OpenTelemetry). Multiple
314
+ * wraps from different instrumentations are composed in declaration order,
315
+ * so the first instrumentation's wrap is the outermost.
316
+ */
317
+ type BeforeConsumeResult = (() => void) | {
318
+ cleanup?(): void;
319
+ wrap?(fn: () => Promise<void>): Promise<void>;
320
+ };
199
321
  /**
200
322
  * Client-wide instrumentation hooks for both send and consume paths.
201
323
  * Use this for cross-cutting concerns like tracing and metrics.
@@ -207,8 +329,13 @@ interface KafkaInstrumentation {
207
329
  beforeSend?(topic: string, headers: MessageHeaders): void;
208
330
  /** Called after a successful send. */
209
331
  afterSend?(topic: string): void;
210
- /** Called before the consumer handler. Return a cleanup function called after the handler. */
211
- beforeConsume?(envelope: EventEnvelope<any>): (() => void) | void;
332
+ /**
333
+ * Called before the consumer handler.
334
+ * Return a cleanup function (legacy) or a `BeforeConsumeResult` object with
335
+ * optional `cleanup` and `wrap`. Use `wrap` to run the handler inside a
336
+ * specific async context (e.g. an active OpenTelemetry span).
337
+ */
338
+ beforeConsume?(envelope: EventEnvelope<any>): BeforeConsumeResult | void;
212
339
  /** Called when the consumer handler throws. */
213
340
  onConsumeError?(envelope: EventEnvelope<any>, error: Error): void;
214
341
  }
@@ -226,13 +353,19 @@ interface ConsumerHandle {
226
353
  /** Stop this consumer. Equivalent to calling `client.stopConsumer(groupId)`. */
227
354
  stop(): Promise<void>;
228
355
  }
356
+ /** Result returned by `KafkaClient.checkStatus()`. */
357
+ type KafkaHealthResult = {
358
+ status: "up";
359
+ clientId: string;
360
+ topics: string[];
361
+ } | {
362
+ status: "down";
363
+ clientId: string;
364
+ error: string;
365
+ };
229
366
  /** Interface describing all public methods of the Kafka client. */
230
367
  interface IKafkaClient<T extends TopicMapConstraint<T>> {
231
- checkStatus(): Promise<{
232
- status: "up";
233
- clientId: string;
234
- topics: string[];
235
- }>;
368
+ checkStatus(): Promise<KafkaHealthResult>;
236
369
  /**
237
370
  * Query the consumer group lag per partition using the admin API.
238
371
  * Lag = (broker high-watermark offset) − (last committed offset).
@@ -257,8 +390,18 @@ interface IKafkaClient<T extends TopicMapConstraint<T>> {
257
390
  sendMessage<K extends keyof T>(topic: K, message: T[K], options?: SendOptions): Promise<void>;
258
391
  sendBatch<K extends keyof T>(topic: K, messages: Array<BatchMessageItem<T[K]>>): Promise<void>;
259
392
  transaction(fn: (ctx: TransactionContext<T>) => Promise<void>): Promise<void>;
260
- getClientId: () => ClientId;
261
- disconnect(): Promise<void>;
393
+ getClientId(): ClientId;
394
+ /**
395
+ * Drain in-flight handlers, then disconnect all producers, consumers, and admin.
396
+ * @param drainTimeoutMs Max ms to wait for in-flight handlers (default 30 000).
397
+ */
398
+ disconnect(drainTimeoutMs?: number): Promise<void>;
399
+ /**
400
+ * Register SIGTERM / SIGINT signal handlers that drain in-flight messages before
401
+ * disconnecting. Call once after constructing the client in non-NestJS apps.
402
+ * NestJS apps get drain automatically via `onModuleDestroy` → `disconnect()`.
403
+ */
404
+ enableGracefulShutdown(signals?: NodeJS.Signals[], drainTimeoutMs?: number): void;
262
405
  }
263
406
  /**
264
407
  * Logger interface for KafkaClient.
@@ -325,75 +468,4 @@ interface SubscribeRetryOptions {
325
468
  backoffMs?: number;
326
469
  }
327
470
 
328
- declare const HEADER_EVENT_ID = "x-event-id";
329
- declare const HEADER_CORRELATION_ID = "x-correlation-id";
330
- declare const HEADER_TIMESTAMP = "x-timestamp";
331
- declare const HEADER_SCHEMA_VERSION = "x-schema-version";
332
- declare const HEADER_TRACEPARENT = "traceparent";
333
- /**
334
- * Typed wrapper combining a parsed message payload with Kafka metadata
335
- * and envelope headers.
336
- *
337
- * On **send**, the library auto-generates envelope headers
338
- * (`x-event-id`, `x-correlation-id`, `x-timestamp`, `x-schema-version`).
339
- *
340
- * On **consume**, the library extracts those headers and assembles
341
- * an `EventEnvelope` that is passed to the handler.
342
- */
343
- interface EventEnvelope<T> {
344
- /** Deserialized + validated message body. */
345
- payload: T;
346
- /** Topic the message was produced to / consumed from. */
347
- topic: string;
348
- /** Kafka partition (consume-side only, `-1` on send). */
349
- partition: number;
350
- /** Kafka offset (consume-side only, empty string on send). */
351
- offset: string;
352
- /** ISO-8601 timestamp set by the producer. */
353
- timestamp: string;
354
- /** Unique ID for this event (UUID v4). */
355
- eventId: string;
356
- /** Correlation ID — auto-propagated via AsyncLocalStorage. */
357
- correlationId: string;
358
- /** Schema version of the payload. */
359
- schemaVersion: number;
360
- /** W3C Trace Context `traceparent` header (set by OTel instrumentation). */
361
- traceparent?: string;
362
- /** All decoded Kafka headers for extensibility. */
363
- headers: MessageHeaders;
364
- }
365
- interface EnvelopeCtx {
366
- correlationId: string;
367
- traceparent?: string;
368
- }
369
- /** Read the current envelope context (correlationId / traceparent) from ALS. */
370
- declare function getEnvelopeContext(): EnvelopeCtx | undefined;
371
- /** Execute `fn` inside an envelope context so nested sends inherit correlationId. */
372
- declare function runWithEnvelopeContext<R>(ctx: EnvelopeCtx, fn: () => R): R;
373
- /** Options accepted by `buildEnvelopeHeaders`. */
374
- interface EnvelopeHeaderOptions {
375
- correlationId?: string;
376
- schemaVersion?: number;
377
- eventId?: string;
378
- headers?: MessageHeaders;
379
- }
380
- /**
381
- * Generate envelope headers for the send path.
382
- *
383
- * Priority for `correlationId`:
384
- * explicit option → ALS context → new UUID.
385
- */
386
- declare function buildEnvelopeHeaders(options?: EnvelopeHeaderOptions): MessageHeaders;
387
- /**
388
- * Decode kafkajs headers (`Record<string, Buffer | string | undefined>`)
389
- * into plain `Record<string, string>`.
390
- */
391
- declare function decodeHeaders(raw: Record<string, Buffer | string | (Buffer | string)[] | undefined> | undefined): MessageHeaders;
392
- /**
393
- * Build an `EventEnvelope` from a consumed kafkajs message.
394
- * Tolerates missing envelope headers — generates defaults so messages
395
- * from non-envelope producers still work.
396
- */
397
- declare function extractEnvelope<T>(payload: T, headers: MessageHeaders, topic: string, partition: number, offset: string): EventEnvelope<T>;
398
-
399
- export { type BatchMessageItem as B, type ClientId as C, type EnvelopeHeaderOptions as E, type GroupId as G, HEADER_CORRELATION_ID as H, type IKafkaClient as I, type KafkaInstrumentation as K, type MessageHeaders as M, 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 BatchMeta as d, type ConsumerHandle as e, type ConsumerInterceptor as f, type EventEnvelope as g, HEADER_EVENT_ID as h, HEADER_SCHEMA_VERSION as i, HEADER_TIMESTAMP as j, HEADER_TRACEPARENT as k, type InferSchema as l, type KafkaLogger as m, type MessageLostContext as n, type SendOptions as o, type SubscribeRetryOptions as p, type TTopicMessageMap as q, type TopicsFrom as r, type TransactionContext as s, buildEnvelopeHeaders as t, decodeHeaders as u, extractEnvelope as v, getEnvelopeContext as w, runWithEnvelopeContext as x, topic as y };
471
+ export { runWithEnvelopeContext as A, type BatchMessageItem as B, type ClientId as C, topic as D, type EnvelopeHeaderOptions as E, type GroupId as G, HEADER_CORRELATION_ID as H, type IKafkaClient as I, type KafkaInstrumentation as K, type MessageHeaders as M, 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 ConsumerHandle as g, type ConsumerInterceptor as h, type EventEnvelope as i, HEADER_EVENT_ID as j, HEADER_SCHEMA_VERSION as k, HEADER_TIMESTAMP as l, HEADER_TRACEPARENT as m, type InferSchema as n, type KafkaLogger as o, type MessageLostContext as p, type SchemaParseContext as q, type SendOptions as r, type SubscribeRetryOptions as s, type TTopicMessageMap as t, type TopicsFrom as u, type TransactionContext as v, buildEnvelopeHeaders as w, decodeHeaders as x, extractEnvelope as y, getEnvelopeContext as z };
@@ -1,15 +1,44 @@
1
+ /**
2
+ * Context passed as the second argument to `SchemaLike.parse()`.
3
+ * Enables schema-registry adapters, version-aware migration, and
4
+ * header-driven parsing without coupling validators to Kafka internals.
5
+ *
6
+ * All fields are optional-friendly — validators that don't need the context
7
+ * can simply ignore the second argument.
8
+ */
9
+ interface SchemaParseContext {
10
+ /** Topic the message was produced to / consumed from. */
11
+ topic: string;
12
+ /** Decoded message headers (envelope headers included). */
13
+ headers: MessageHeaders;
14
+ /** Value of the `x-schema-version` header, defaults to `1`. */
15
+ version: number;
16
+ }
1
17
  /**
2
18
  * Any validation library with a `.parse()` method.
3
19
  * Works with Zod, Valibot, ArkType, or any custom validator.
4
20
  *
21
+ * The optional `ctx` argument carries topic/header/version metadata so
22
+ * validators can perform schema-registry lookups or version-aware migrations.
23
+ * Existing validators that only use the first argument continue to work
24
+ * unchanged — the second argument is silently ignored.
25
+ *
5
26
  * @example
6
27
  * ```ts
7
28
  * import { z } from 'zod';
8
29
  * const schema: SchemaLike<{ id: string }> = z.object({ id: z.string() });
30
+ *
31
+ * // Context-aware validator:
32
+ * const schema: SchemaLike<MyType> = {
33
+ * parse(data, ctx) {
34
+ * const version = ctx?.version ?? 1;
35
+ * return version >= 2 ? migrateV1toV2(data) : validateV1(data);
36
+ * }
37
+ * };
9
38
  * ```
10
39
  */
11
40
  interface SchemaLike<T = any> {
12
- parse(data: unknown): T | Promise<T>;
41
+ parse(data: unknown, ctx?: SchemaParseContext): T | Promise<T>;
13
42
  }
14
43
  /** Infer the output type from a SchemaLike. */
15
44
  type InferSchema<S extends SchemaLike> = S extends SchemaLike<infer T> ? T : never;
@@ -32,8 +61,8 @@ interface TopicDescriptor<N extends string = string, M extends Record<string, an
32
61
  *
33
62
  * @example
34
63
  * ```ts
35
- * // Without schema — type provided explicitly:
36
- * const OrderCreated = topic('order.created')<{ orderId: string; amount: number }>();
64
+ * // Without schema — explicit type via .type<T>():
65
+ * const OrderCreated = topic('order.created').type<{ orderId: string; amount: number }>();
37
66
  *
38
67
  * // With schema — type inferred from schema:
39
68
  * const OrderCreated = topic('order.created').schema(z.object({
@@ -50,16 +79,17 @@ interface TopicDescriptor<N extends string = string, M extends Record<string, an
50
79
  * ```
51
80
  */
52
81
  declare function topic<N extends string>(name: N): {
53
- <M extends Record<string, any>>(): TopicDescriptor<N, M>;
54
- schema<S extends SchemaLike<Record<string, any>>>(schema: S): TopicDescriptor<N, InferSchema<S>>;
82
+ /** Provide an explicit message type without a runtime schema. */
83
+ type: <M extends Record<string, any>>() => TopicDescriptor<N, M>;
84
+ schema: <S extends SchemaLike<Record<string, any>>>(schema: S) => TopicDescriptor<N, InferSchema<S>>;
55
85
  };
56
86
  /**
57
87
  * Build a topic-message map type from a union of TopicDescriptors.
58
88
  *
59
89
  * @example
60
90
  * ```ts
61
- * const OrderCreated = topic('order.created')<{ orderId: string }>();
62
- * const OrderCompleted = topic('order.completed')<{ completedAt: string }>();
91
+ * const OrderCreated = topic('order.created').type<{ orderId: string }>();
92
+ * const OrderCompleted = topic('order.completed').type<{ completedAt: string }>();
63
93
  *
64
94
  * type MyTopics = TopicsFrom<typeof OrderCreated | typeof OrderCompleted>;
65
95
  * // { 'order.created': { orderId: string }; 'order.completed': { completedAt: string } }
@@ -69,6 +99,77 @@ type TopicsFrom<D extends TopicDescriptor<any, any>> = {
69
99
  [K in D as K["__topic"]]: K["__type"];
70
100
  };
71
101
 
102
+ declare const HEADER_EVENT_ID = "x-event-id";
103
+ declare const HEADER_CORRELATION_ID = "x-correlation-id";
104
+ declare const HEADER_TIMESTAMP = "x-timestamp";
105
+ declare const HEADER_SCHEMA_VERSION = "x-schema-version";
106
+ declare const HEADER_TRACEPARENT = "traceparent";
107
+ /**
108
+ * Typed wrapper combining a parsed message payload with Kafka metadata
109
+ * and envelope headers.
110
+ *
111
+ * On **send**, the library auto-generates envelope headers
112
+ * (`x-event-id`, `x-correlation-id`, `x-timestamp`, `x-schema-version`).
113
+ *
114
+ * On **consume**, the library extracts those headers and assembles
115
+ * an `EventEnvelope` that is passed to the handler.
116
+ */
117
+ interface EventEnvelope<T> {
118
+ /** Deserialized + validated message body. */
119
+ payload: T;
120
+ /** Topic the message was produced to / consumed from. */
121
+ topic: string;
122
+ /** Kafka partition (consume-side only, `-1` on send). */
123
+ partition: number;
124
+ /** Kafka offset (consume-side only, empty string on send). */
125
+ offset: string;
126
+ /** ISO-8601 timestamp set by the producer. */
127
+ timestamp: string;
128
+ /** Unique ID for this event (UUID v4). */
129
+ eventId: string;
130
+ /** Correlation ID — auto-propagated via AsyncLocalStorage. */
131
+ correlationId: string;
132
+ /** Schema version of the payload. */
133
+ schemaVersion: number;
134
+ /** W3C Trace Context `traceparent` header (set by OTel instrumentation). */
135
+ traceparent?: string;
136
+ /** All decoded Kafka headers for extensibility. */
137
+ headers: MessageHeaders;
138
+ }
139
+ interface EnvelopeCtx {
140
+ correlationId: string;
141
+ traceparent?: string;
142
+ }
143
+ /** Read the current envelope context (correlationId / traceparent) from ALS. */
144
+ declare function getEnvelopeContext(): EnvelopeCtx | undefined;
145
+ /** Execute `fn` inside an envelope context so nested sends inherit correlationId. */
146
+ declare function runWithEnvelopeContext<R>(ctx: EnvelopeCtx, fn: () => R): R;
147
+ /** Options accepted by `buildEnvelopeHeaders`. */
148
+ interface EnvelopeHeaderOptions {
149
+ correlationId?: string;
150
+ schemaVersion?: number;
151
+ eventId?: string;
152
+ headers?: MessageHeaders;
153
+ }
154
+ /**
155
+ * Generate envelope headers for the send path.
156
+ *
157
+ * Priority for `correlationId`:
158
+ * explicit option → ALS context → new UUID.
159
+ */
160
+ declare function buildEnvelopeHeaders(options?: EnvelopeHeaderOptions): MessageHeaders;
161
+ /**
162
+ * Decode kafkajs headers (`Record<string, Buffer | string | undefined>`)
163
+ * into plain `Record<string, string>`.
164
+ */
165
+ declare function decodeHeaders(raw: Record<string, Buffer | string | (Buffer | string)[] | undefined> | undefined): MessageHeaders;
166
+ /**
167
+ * Build an `EventEnvelope` from a consumed kafkajs message.
168
+ * Tolerates missing envelope headers — generates defaults so messages
169
+ * from non-envelope producers still work.
170
+ */
171
+ declare function extractEnvelope<T>(payload: T, headers: MessageHeaders, topic: string, partition: number, offset: string): EventEnvelope<T>;
172
+
72
173
  /**
73
174
  * Mapping of topic names to their message types.
74
175
  * Define this interface to get type-safe publish/subscribe across your app.
@@ -165,6 +266,12 @@ interface ConsumerOptions<T extends TopicMapConstraint<T> = TTopicMessageMap> {
165
266
  * On exhaustion, messages go to `<topic>.dlq` (if `dlq: true`) or `onMessageLost`.
166
267
  */
167
268
  retryTopics?: boolean;
269
+ /**
270
+ * Timeout (ms) for waiting until each retry level consumer receives partition
271
+ * assignments after `startConsumer` connects. Default: `10000`.
272
+ * Increase this when the broker is slow to rebalance.
273
+ */
274
+ retryTopicAssignmentTimeoutMs?: number;
168
275
  /**
169
276
  * Log a warning if the message handler has not resolved within this window (ms).
170
277
  * The handler is not cancelled — this is a diagnostic aid to surface stuck handlers
@@ -196,6 +303,21 @@ interface ConsumerInterceptor<T extends TopicMapConstraint<T> = TTopicMessageMap
196
303
  /** Called when the message handler throws. */
197
304
  onError?(envelope: EventEnvelope<T[keyof T]>, error: Error): Promise<void> | void;
198
305
  }
306
+ /**
307
+ * Return value of `KafkaInstrumentation.beforeConsume`.
308
+ *
309
+ * - `() => void` — legacy form: a cleanup function called after the handler.
310
+ * - Object form:
311
+ * - `cleanup?()` — called after the handler (same as the legacy function form).
312
+ * - `wrap?(fn)` — wraps the handler execution; call `fn()` inside the desired
313
+ * async context (e.g. `context.with(spanCtx, fn)` for OpenTelemetry). Multiple
314
+ * wraps from different instrumentations are composed in declaration order,
315
+ * so the first instrumentation's wrap is the outermost.
316
+ */
317
+ type BeforeConsumeResult = (() => void) | {
318
+ cleanup?(): void;
319
+ wrap?(fn: () => Promise<void>): Promise<void>;
320
+ };
199
321
  /**
200
322
  * Client-wide instrumentation hooks for both send and consume paths.
201
323
  * Use this for cross-cutting concerns like tracing and metrics.
@@ -207,8 +329,13 @@ interface KafkaInstrumentation {
207
329
  beforeSend?(topic: string, headers: MessageHeaders): void;
208
330
  /** Called after a successful send. */
209
331
  afterSend?(topic: string): void;
210
- /** Called before the consumer handler. Return a cleanup function called after the handler. */
211
- beforeConsume?(envelope: EventEnvelope<any>): (() => void) | void;
332
+ /**
333
+ * Called before the consumer handler.
334
+ * Return a cleanup function (legacy) or a `BeforeConsumeResult` object with
335
+ * optional `cleanup` and `wrap`. Use `wrap` to run the handler inside a
336
+ * specific async context (e.g. an active OpenTelemetry span).
337
+ */
338
+ beforeConsume?(envelope: EventEnvelope<any>): BeforeConsumeResult | void;
212
339
  /** Called when the consumer handler throws. */
213
340
  onConsumeError?(envelope: EventEnvelope<any>, error: Error): void;
214
341
  }
@@ -226,13 +353,19 @@ interface ConsumerHandle {
226
353
  /** Stop this consumer. Equivalent to calling `client.stopConsumer(groupId)`. */
227
354
  stop(): Promise<void>;
228
355
  }
356
+ /** Result returned by `KafkaClient.checkStatus()`. */
357
+ type KafkaHealthResult = {
358
+ status: "up";
359
+ clientId: string;
360
+ topics: string[];
361
+ } | {
362
+ status: "down";
363
+ clientId: string;
364
+ error: string;
365
+ };
229
366
  /** Interface describing all public methods of the Kafka client. */
230
367
  interface IKafkaClient<T extends TopicMapConstraint<T>> {
231
- checkStatus(): Promise<{
232
- status: "up";
233
- clientId: string;
234
- topics: string[];
235
- }>;
368
+ checkStatus(): Promise<KafkaHealthResult>;
236
369
  /**
237
370
  * Query the consumer group lag per partition using the admin API.
238
371
  * Lag = (broker high-watermark offset) − (last committed offset).
@@ -257,8 +390,18 @@ interface IKafkaClient<T extends TopicMapConstraint<T>> {
257
390
  sendMessage<K extends keyof T>(topic: K, message: T[K], options?: SendOptions): Promise<void>;
258
391
  sendBatch<K extends keyof T>(topic: K, messages: Array<BatchMessageItem<T[K]>>): Promise<void>;
259
392
  transaction(fn: (ctx: TransactionContext<T>) => Promise<void>): Promise<void>;
260
- getClientId: () => ClientId;
261
- disconnect(): Promise<void>;
393
+ getClientId(): ClientId;
394
+ /**
395
+ * Drain in-flight handlers, then disconnect all producers, consumers, and admin.
396
+ * @param drainTimeoutMs Max ms to wait for in-flight handlers (default 30 000).
397
+ */
398
+ disconnect(drainTimeoutMs?: number): Promise<void>;
399
+ /**
400
+ * Register SIGTERM / SIGINT signal handlers that drain in-flight messages before
401
+ * disconnecting. Call once after constructing the client in non-NestJS apps.
402
+ * NestJS apps get drain automatically via `onModuleDestroy` → `disconnect()`.
403
+ */
404
+ enableGracefulShutdown(signals?: NodeJS.Signals[], drainTimeoutMs?: number): void;
262
405
  }
263
406
  /**
264
407
  * Logger interface for KafkaClient.
@@ -325,75 +468,4 @@ interface SubscribeRetryOptions {
325
468
  backoffMs?: number;
326
469
  }
327
470
 
328
- declare const HEADER_EVENT_ID = "x-event-id";
329
- declare const HEADER_CORRELATION_ID = "x-correlation-id";
330
- declare const HEADER_TIMESTAMP = "x-timestamp";
331
- declare const HEADER_SCHEMA_VERSION = "x-schema-version";
332
- declare const HEADER_TRACEPARENT = "traceparent";
333
- /**
334
- * Typed wrapper combining a parsed message payload with Kafka metadata
335
- * and envelope headers.
336
- *
337
- * On **send**, the library auto-generates envelope headers
338
- * (`x-event-id`, `x-correlation-id`, `x-timestamp`, `x-schema-version`).
339
- *
340
- * On **consume**, the library extracts those headers and assembles
341
- * an `EventEnvelope` that is passed to the handler.
342
- */
343
- interface EventEnvelope<T> {
344
- /** Deserialized + validated message body. */
345
- payload: T;
346
- /** Topic the message was produced to / consumed from. */
347
- topic: string;
348
- /** Kafka partition (consume-side only, `-1` on send). */
349
- partition: number;
350
- /** Kafka offset (consume-side only, empty string on send). */
351
- offset: string;
352
- /** ISO-8601 timestamp set by the producer. */
353
- timestamp: string;
354
- /** Unique ID for this event (UUID v4). */
355
- eventId: string;
356
- /** Correlation ID — auto-propagated via AsyncLocalStorage. */
357
- correlationId: string;
358
- /** Schema version of the payload. */
359
- schemaVersion: number;
360
- /** W3C Trace Context `traceparent` header (set by OTel instrumentation). */
361
- traceparent?: string;
362
- /** All decoded Kafka headers for extensibility. */
363
- headers: MessageHeaders;
364
- }
365
- interface EnvelopeCtx {
366
- correlationId: string;
367
- traceparent?: string;
368
- }
369
- /** Read the current envelope context (correlationId / traceparent) from ALS. */
370
- declare function getEnvelopeContext(): EnvelopeCtx | undefined;
371
- /** Execute `fn` inside an envelope context so nested sends inherit correlationId. */
372
- declare function runWithEnvelopeContext<R>(ctx: EnvelopeCtx, fn: () => R): R;
373
- /** Options accepted by `buildEnvelopeHeaders`. */
374
- interface EnvelopeHeaderOptions {
375
- correlationId?: string;
376
- schemaVersion?: number;
377
- eventId?: string;
378
- headers?: MessageHeaders;
379
- }
380
- /**
381
- * Generate envelope headers for the send path.
382
- *
383
- * Priority for `correlationId`:
384
- * explicit option → ALS context → new UUID.
385
- */
386
- declare function buildEnvelopeHeaders(options?: EnvelopeHeaderOptions): MessageHeaders;
387
- /**
388
- * Decode kafkajs headers (`Record<string, Buffer | string | undefined>`)
389
- * into plain `Record<string, string>`.
390
- */
391
- declare function decodeHeaders(raw: Record<string, Buffer | string | (Buffer | string)[] | undefined> | undefined): MessageHeaders;
392
- /**
393
- * Build an `EventEnvelope` from a consumed kafkajs message.
394
- * Tolerates missing envelope headers — generates defaults so messages
395
- * from non-envelope producers still work.
396
- */
397
- declare function extractEnvelope<T>(payload: T, headers: MessageHeaders, topic: string, partition: number, offset: string): EventEnvelope<T>;
398
-
399
- export { type BatchMessageItem as B, type ClientId as C, type EnvelopeHeaderOptions as E, type GroupId as G, HEADER_CORRELATION_ID as H, type IKafkaClient as I, type KafkaInstrumentation as K, type MessageHeaders as M, 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 BatchMeta as d, type ConsumerHandle as e, type ConsumerInterceptor as f, type EventEnvelope as g, HEADER_EVENT_ID as h, HEADER_SCHEMA_VERSION as i, HEADER_TIMESTAMP as j, HEADER_TRACEPARENT as k, type InferSchema as l, type KafkaLogger as m, type MessageLostContext as n, type SendOptions as o, type SubscribeRetryOptions as p, type TTopicMessageMap as q, type TopicsFrom as r, type TransactionContext as s, buildEnvelopeHeaders as t, decodeHeaders as u, extractEnvelope as v, getEnvelopeContext as w, runWithEnvelopeContext as x, topic as y };
471
+ export { runWithEnvelopeContext as A, type BatchMessageItem as B, type ClientId as C, topic as D, type EnvelopeHeaderOptions as E, type GroupId as G, HEADER_CORRELATION_ID as H, type IKafkaClient as I, type KafkaInstrumentation as K, type MessageHeaders as M, 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 ConsumerHandle as g, type ConsumerInterceptor as h, type EventEnvelope as i, HEADER_EVENT_ID as j, HEADER_SCHEMA_VERSION as k, HEADER_TIMESTAMP as l, HEADER_TRACEPARENT as m, type InferSchema as n, type KafkaLogger as o, type MessageLostContext as p, type SchemaParseContext as q, type SendOptions as r, type SubscribeRetryOptions as s, type TTopicMessageMap as t, type TopicsFrom as u, type TransactionContext as v, buildEnvelopeHeaders as w, decodeHeaders as x, extractEnvelope as y, getEnvelopeContext as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drarzter/kafka-client",
3
- "version": "0.5.6",
3
+ "version": "0.6.3",
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",