@basaltkit/queue-kafka 1.1.0 → 1.1.2

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.
Files changed (2) hide show
  1. package/README.md +67 -3
  2. package/package.json +6 -2
package/README.md CHANGED
@@ -70,7 +70,23 @@ await Job.dispatch(payload, { delay: '5m' }) // → throws UnsupportedJobOptionE
70
70
  // with onUnsupported: 'warn' (default) → warns once and runs immediately
71
71
  ```
72
72
 
73
- > If what you need is *streaming*/pub-sub (rather than jobs with retry/delay), the natural fit in Basalt is usually `@basaltkit/events`, not `@basaltkit/queue`.
73
+ **If what you need is *streaming*/pub-sub** (rather than jobs with retry/delay), the natural fit in Basalt is usually `@basaltkit/events`, not `@basaltkit/queue`.
74
+
75
+ ### Inspection: no `list()` / `queue:jobs`
76
+
77
+ This driver deliberately does **not** implement the queue's optional
78
+ `list(queue, options)` capability, so `basalt queue:jobs` reports it as
79
+ unsupported.
80
+
81
+ Here the reason is not destructiveness — reading a Kafka topic does not remove
82
+ records — but **meaning**. Kafka has no per-message state: there is no
83
+ `waiting`/`active`/`completed`/`failed` set, no broker-assigned job id, and no
84
+ way to tell a record still to be processed from one processed an hour ago. A
85
+ `list()` would return an arbitrary window of the log within retention, dressed
86
+ up in job states it invented. That would make the same API mean something
87
+ different depending on the driver, which is the coupling this API exists to
88
+ remove. Use your Kafka tooling (`kafka-console-consumer`, a UI) plus the
89
+ `<topic>.retry` / `<topic>.dead` topics instead.
74
90
 
75
91
  ## How it works
76
92
 
@@ -94,9 +110,57 @@ The worker's concurrency is passed as `partitionsConsumedConcurrently` — actua
94
110
  | `retrySuffix` | `string` | `'.retry'` | Suffix for the retry topic. |
95
111
  | `deadSuffix` | `string` | `'.dead'` | Suffix for the dead-letter topic. |
96
112
  | `client` | `KafkaClient` | kafkajs | Injectable client — used in tests without a broker. |
97
- | `onError` | `(error, { source, queue? }) => void` | contextual `console.error` | Infrastructure-fault hook (same pattern as rabbitmq/sqs): a worker's connect/subscribe/run failing at boot (`source: 'consumer'` previously an unhandled, process-fatal rejection and an invisible zero-worker app), or a retry/dead-letter re-publish failing (`source: 'producer'` — reported, then rethrown so the offset is not committed and Kafka redelivers; a producer outage cannot silently lose a failing job). |
113
+ | `onError` | `(error: unknown, info: { source: 'consumer' \| 'producer'; queue?: string }) => void` | contextual `console.error` | The driver's single fault channelsee below. |
114
+
115
+ Implements the `QueueDriver` contract from `@basaltkit/queue`. It does **not** implement the optional `stats` / `retryFailed`, so `basalt queue:stats` and `basalt queue:retry` report the operation as unsupported — use your Kafka tooling for consumer-group lag instead.
116
+
117
+ ### Failure hooks
118
+
119
+ `onError` is the only callback, and its default (`console.error` with the source and queue) is
120
+ never silent. There is no `onJobFailed`: a job that exhausts `attempts` is produced to
121
+ `<topic>.dead`, which *is* the report — monitor that topic.
122
+
123
+ | `source` | Raised when | What the driver does next |
124
+ |---|---|---|
125
+ | `'consumer'` | A worker's `connect`/`subscribe`/`run` rejected at boot (broker unreachable, missing topic, bad ACLs). | Reports and stops. Without this the rejection would float and be process-fatal, and the app would report healthy with **zero** workers. |
126
+ | `'producer'` | The retry / dead-letter **re-publish itself failed** while handling a failed job. | Reports, then **rethrows** — see below. |
127
+
128
+ ### Redelivery when the DLQ produce fails
129
+
130
+ The subtle case. A job's handler throws, so the driver tries to re-route the message — to
131
+ `<topic>.retry` if attempts remain, otherwise to `<topic>.dead`. If *that* produce also fails
132
+ (the producer lost its broker connection, the dead topic doesn't exist, the request timed out),
133
+ the failed job exists nowhere but in the message currently being consumed.
134
+
135
+ kafkajs auto-commits offsets after `eachMessage` **resolves**. So the driver:
136
+
137
+ 1. reports the publish failure through `onError({ source: 'producer', queue })`, then
138
+ 2. **rethrows** it, so `eachMessage` rejects and the offset is **not** committed.
139
+
140
+ Kafka then redelivers the same message and the driver tries the whole thing again — at-least-once
141
+ rather than a job that quietly evaporated during a producer outage. It is the Kafka equivalent of
142
+ RabbitMQ leaving a message unacked.
143
+
144
+ Two consequences worth planning for:
145
+
146
+ - **Handlers must be idempotent.** A redelivered message re-runs the handler that already failed,
147
+ and a message whose re-publish succeeded is never redelivered — but a partition stalls on the
148
+ failing message while the producer is down, so ordered downstream work backs up behind it.
149
+ - **A normal failure path does commit.** When the re-publish *succeeds*, the failure is
150
+ considered handled: the offset commits and the retry copy carries the incremented
151
+ `x-basalt-attempt` header. Redelivery only happens on the produce failure itself.
152
+
153
+ ### Exported errors
154
+
155
+ This driver throws no error classes of its own. `UnsupportedJobOptionError`
156
+ (`QUEUE_UNSUPPORTED_OPTION`) comes from `@basaltkit/queue` when a `delay`/`priority` dispatch
157
+ meets `onUnsupported: 'throw'`; everything else surfaces through `onError`.
158
+
159
+ ### Hard limits
98
160
 
99
- Implements the `QueueDriver` contract from `@basaltkit/queue`.
161
+ The attempt counters travel in message headers, which any producer on the topic could write, so
162
+ the consumer clamps the `x-basalt-attempts` it reads to at most **50**. A crafted message cannot
163
+ drive an unbounded retry loop.
100
164
 
101
165
  ## How it connects to other modules
102
166
 
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@basaltkit/queue-kafka",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
+ "engines": {
5
+ "node": ">=22.5.0"
6
+ },
4
7
  "description": "Kafka driver for @basaltkit/queue: produce/consume jobs with retry and dead-letter topics (no delayed delivery or priority — Kafka has neither).",
5
8
  "license": "MIT",
6
9
  "type": "module",
10
+ "sideEffects": false,
7
11
  "exports": {
8
12
  ".": {
9
13
  "types": "./dist/index.d.ts",
@@ -14,7 +18,7 @@
14
18
  "dist"
15
19
  ],
16
20
  "dependencies": {
17
- "@basaltkit/queue": "^1.3.1"
21
+ "@basaltkit/queue": "^1.5.0"
18
22
  },
19
23
  "peerDependencies": {
20
24
  "kafkajs": "^2.0.0"