@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 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** — configurable retries with backoff, dead letter queue for failed messages
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 between retries (multiplied by attempt number) |
667
- | `dlq` | `false` | Send to `{topic}.dlq` after all retries exhausted |
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` works:
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