@drarzter/kafka-client 0.5.1 → 0.5.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 +127 -4
- package/dist/{chunk-P7GY4BLV.mjs → chunk-3QXTW66R.mjs} +304 -40
- package/dist/chunk-3QXTW66R.mjs.map +1 -0
- package/dist/core.d.mts +23 -3
- package/dist/core.d.ts +23 -3
- package/dist/core.js +303 -39
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +1 -1
- package/dist/{envelope-CPX1qudy.d.mts → envelope-BR8d1m8c.d.mts} +43 -4
- package/dist/{envelope-CPX1qudy.d.ts → envelope-BR8d1m8c.d.ts} +43 -4
- package/dist/index.d.mts +10 -7
- package/dist/index.d.ts +10 -7
- package/dist/index.js +303 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/otel.d.mts +1 -1
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/otel.mjs +1 -1
- package/dist/otel.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-P7GY4BLV.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -6,17 +6,60 @@
|
|
|
6
6
|
|
|
7
7
|
Type-safe Kafka client for Node.js. Framework-agnostic core with a first-class NestJS adapter. Built on top of [`@confluentinc/kafka-javascript`](https://github.com/confluentinc/confluent-kafka-javascript) (librdkafka).
|
|
8
8
|
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
- [What is this?](#what-is-this)
|
|
12
|
+
- [Why?](#why)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Standalone usage](#standalone-usage-no-nestjs)
|
|
15
|
+
- [Quick start (NestJS)](#quick-start-nestjs)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [1. Define your topic map](#1-define-your-topic-map)
|
|
18
|
+
- [2. Register the module](#2-register-the-module)
|
|
19
|
+
- [3. Inject and use](#3-inject-and-use)
|
|
20
|
+
- [Consuming messages](#consuming-messages)
|
|
21
|
+
- [Declarative: @SubscribeTo()](#declarative-subscribeto)
|
|
22
|
+
- [Imperative: startConsumer()](#imperative-startconsumer)
|
|
23
|
+
- [Multiple consumer groups](#multiple-consumer-groups)
|
|
24
|
+
- [Partition key](#partition-key)
|
|
25
|
+
- [Message headers](#message-headers)
|
|
26
|
+
- [Batch sending](#batch-sending)
|
|
27
|
+
- [Batch consuming](#batch-consuming)
|
|
28
|
+
- [Transactions](#transactions)
|
|
29
|
+
- [Consumer interceptors](#consumer-interceptors)
|
|
30
|
+
- [Instrumentation](#instrumentation)
|
|
31
|
+
- [Options reference](#options-reference)
|
|
32
|
+
- [Error classes](#error-classes)
|
|
33
|
+
- [Retry topic chain](#retry-topic-chain)
|
|
34
|
+
- [stopConsumer](#stopconsumer)
|
|
35
|
+
- [onMessageLost](#onmessagelost)
|
|
36
|
+
- [Schema validation](#schema-validation)
|
|
37
|
+
- [Health check](#health-check)
|
|
38
|
+
- [Testing](#testing)
|
|
39
|
+
- [Project structure](#project-structure)
|
|
40
|
+
|
|
9
41
|
## What is this?
|
|
10
42
|
|
|
11
43
|
An opinionated, type-safe abstraction over `@confluentinc/kafka-javascript` (librdkafka). Works standalone (Express, Fastify, raw Node) or as a NestJS DynamicModule. Not a full-featured framework — just a clean, typed layer for producing and consuming Kafka messages.
|
|
12
44
|
|
|
45
|
+
**This library exists so you don't have to think about:**
|
|
46
|
+
|
|
47
|
+
- rebalance edge cases
|
|
48
|
+
- retry loops and backoff scheduling
|
|
49
|
+
- dead letter queue wiring
|
|
50
|
+
- transaction coordinator warmup
|
|
51
|
+
- graceful shutdown and offset commit pitfalls
|
|
52
|
+
- silent message loss
|
|
53
|
+
|
|
54
|
+
Safe by default. Configurable when you need it. Escape hatches for when you know what you're doing.
|
|
55
|
+
|
|
13
56
|
## Why?
|
|
14
57
|
|
|
15
58
|
- **Typed topics** — you define a map of topic -> message shape, and the compiler won't let you send wrong data to wrong topic
|
|
16
59
|
- **Topic descriptors** — `topic()` DX sugar lets you define topics as standalone typed objects instead of string keys
|
|
17
60
|
- **Framework-agnostic** — use standalone or with NestJS (`register()` / `registerAsync()`, DI, lifecycle hooks)
|
|
18
61
|
- **Idempotent producer** — `acks: -1`, `idempotent: true` by default
|
|
19
|
-
- **Retry + DLQ** —
|
|
62
|
+
- **Retry + DLQ** — exponential backoff with full jitter; dead letter queue with error metadata headers (original topic, error message, stack, attempt count)
|
|
20
63
|
- **Batch sending** — send multiple messages in a single request
|
|
21
64
|
- **Batch consuming** — `startBatchConsumer()` for high-throughput `eachBatch` processing
|
|
22
65
|
- **Partition key support** — route related messages to the same partition
|
|
@@ -663,8 +706,10 @@ Options for `sendMessage()` — the third argument:
|
|
|
663
706
|
| `fromBeginning` | `false` | Read from the beginning of the topic |
|
|
664
707
|
| `autoCommit` | `true` | Auto-commit offsets |
|
|
665
708
|
| `retry.maxRetries` | — | Number of retry attempts |
|
|
666
|
-
| `retry.backoffMs` | `1000` | Base delay
|
|
667
|
-
| `
|
|
709
|
+
| `retry.backoffMs` | `1000` | Base delay for exponential backoff in ms |
|
|
710
|
+
| `retry.maxBackoffMs` | `30000` | Maximum delay cap for exponential backoff in ms |
|
|
711
|
+
| `dlq` | `false` | Send to `{topic}.dlq` after all retries exhausted — message carries `x-dlq-*` metadata headers |
|
|
712
|
+
| `retryTopics` | `false` | Route failed messages through `{topic}.retry` instead of sleeping in-process (see [Retry topic chain](#retry-topic-chain)) |
|
|
668
713
|
| `interceptors` | `[]` | Array of before/after/onError hooks |
|
|
669
714
|
| `batch` | `false` | (decorator only) Use `startBatchConsumer` instead of `startConsumer` |
|
|
670
715
|
| `subscribeRetry.retries` | `5` | Max attempts for `consumer.subscribe()` when topic doesn't exist yet |
|
|
@@ -685,6 +730,7 @@ Passed to `KafkaModule.register()` or returned from `registerAsync()` factory:
|
|
|
685
730
|
| `numPartitions` | `1` | Number of partitions for auto-created topics |
|
|
686
731
|
| `strictSchemas` | `true` | Validate string topic keys against schemas registered via TopicDescriptor |
|
|
687
732
|
| `instrumentation` | `[]` | Client-wide instrumentation hooks (e.g. OTel). Applied to both send and consume paths |
|
|
733
|
+
| `onMessageLost` | — | Called when a message is silently dropped without DLQ — use to alert, log to external systems, or trigger fallback logic |
|
|
688
734
|
|
|
689
735
|
**Module-scoped** (default) — import `KafkaModule` in each module that needs it:
|
|
690
736
|
|
|
@@ -772,6 +818,74 @@ const interceptor: ConsumerInterceptor<MyTopics> = {
|
|
|
772
818
|
};
|
|
773
819
|
```
|
|
774
820
|
|
|
821
|
+
## Retry topic chain
|
|
822
|
+
|
|
823
|
+
By default, retry is handled in-process: the consumer sleeps between attempts while holding the partition. With `retryTopics: true`, failed messages are routed to a `<topic>.retry` Kafka topic instead. A companion consumer auto-starts on `<topic>.retry` (group `<groupId>-retry`), waits until the scheduled retry time, then calls the same handler.
|
|
824
|
+
|
|
825
|
+
Benefits over in-process retry:
|
|
826
|
+
|
|
827
|
+
- **Durable** — retry messages survive a consumer restart
|
|
828
|
+
- **Non-blocking** — the original consumer is free immediately; the retry consumer pauses only the specific partition being delayed, so other partitions continue processing
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
await kafka.startConsumer(['orders.created'], handler, {
|
|
832
|
+
retry: { maxRetries: 3, backoffMs: 1000, maxBackoffMs: 30_000 },
|
|
833
|
+
dlq: true,
|
|
834
|
+
retryTopics: true, // ← opt in
|
|
835
|
+
});
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
Message flow with `maxRetries: 2`:
|
|
839
|
+
|
|
840
|
+
```text
|
|
841
|
+
orders.created → handler fails → orders.created.retry (attempt 1, delay ~1 s)
|
|
842
|
+
→ handler fails → orders.created.retry (attempt 2, delay ~2 s)
|
|
843
|
+
→ handler fails → orders.created.dlq
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
The retry topic messages carry scheduling headers (`x-retry-attempt`, `x-retry-after`, `x-retry-original-topic`, `x-retry-max-retries`) that the companion consumer reads automatically — no manual configuration needed.
|
|
847
|
+
|
|
848
|
+
> **Note:** `retryTopics` requires `retry` to be set — an error is thrown at startup if `retry` is missing. Currently only applies to `startConsumer`; batch consumers (`startBatchConsumer`) use in-process retry regardless.
|
|
849
|
+
|
|
850
|
+
## stopConsumer
|
|
851
|
+
|
|
852
|
+
Stop all consumers or a specific group:
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
// Stop a specific consumer group
|
|
856
|
+
await kafka.stopConsumer('my-group');
|
|
857
|
+
|
|
858
|
+
// Stop all consumers
|
|
859
|
+
await kafka.stopConsumer();
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
`stopConsumer(groupId)` disconnects and removes only that group's consumer, leaving other groups running. Useful when you want to pause processing for a specific topic without restarting the whole client.
|
|
863
|
+
|
|
864
|
+
## onMessageLost
|
|
865
|
+
|
|
866
|
+
By default, if a consumer handler throws and `dlq` is not enabled, the message is logged and dropped. Use `onMessageLost` to catch these silent losses:
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
import { KafkaClient, MessageLostContext } from '@drarzter/kafka-client/core';
|
|
870
|
+
|
|
871
|
+
const kafka = new KafkaClient('my-app', 'my-group', ['localhost:9092'], {
|
|
872
|
+
onMessageLost: (ctx: MessageLostContext) => {
|
|
873
|
+
// ctx.topic — topic the message came from
|
|
874
|
+
// ctx.error — what caused the failure
|
|
875
|
+
// ctx.attempt — number of attempts (0 = schema validation failed before handler ran)
|
|
876
|
+
// ctx.headers — original message headers (correlationId, traceparent, ...)
|
|
877
|
+
myAlertSystem.send(`Message lost on ${ctx.topic}: ${ctx.error.message}`);
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
`onMessageLost` fires in two cases:
|
|
883
|
+
|
|
884
|
+
1. **Handler error** — handler threw after all retries and `dlq: false`
|
|
885
|
+
2. **Validation error** — schema rejected the message and `dlq: false` (attempt is `0`)
|
|
886
|
+
|
|
887
|
+
It does NOT fire when `dlq: true` — in that case the message is preserved in `{topic}.dlq`.
|
|
888
|
+
|
|
775
889
|
## Schema validation
|
|
776
890
|
|
|
777
891
|
Add runtime message validation using any library with a `.parse()` method — Zod, Valibot, ArkType, or a custom validator. No extra dependency required.
|
|
@@ -835,11 +949,12 @@ Disable with `strictSchemas: false` in `KafkaModule.register()` options if you w
|
|
|
835
949
|
|
|
836
950
|
### Bring your own validator
|
|
837
951
|
|
|
838
|
-
Any object with `parse(data: unknown): T
|
|
952
|
+
Any object with `parse(data: unknown): T | Promise<T>` works — sync and async validators are both supported:
|
|
839
953
|
|
|
840
954
|
```typescript
|
|
841
955
|
import { SchemaLike } from '@drarzter/kafka-client';
|
|
842
956
|
|
|
957
|
+
// Sync validator
|
|
843
958
|
const customValidator: SchemaLike<{ id: string }> = {
|
|
844
959
|
parse(data: unknown) {
|
|
845
960
|
const d = data as any;
|
|
@@ -848,6 +963,14 @@ const customValidator: SchemaLike<{ id: string }> = {
|
|
|
848
963
|
},
|
|
849
964
|
};
|
|
850
965
|
|
|
966
|
+
// Async validator — e.g. remote schema registry lookup
|
|
967
|
+
const asyncValidator: SchemaLike<{ id: string }> = {
|
|
968
|
+
async parse(data: unknown) {
|
|
969
|
+
const schema = await fetchSchemaFromRegistry('my.topic');
|
|
970
|
+
return schema.validate(data);
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
|
|
851
974
|
const MyTopic = topic('my.topic').schema(customValidator);
|
|
852
975
|
```
|
|
853
976
|
|