@relaybus/kafka 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +215 -2
- package/dist/index.cjs +719 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +283 -0
- package/dist/index.d.ts +283 -0
- package/dist/index.js +711 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { RelayBusError, NonRetryableError, EventEnvelope, Duration, Transport, RelayBusLogger, TransportMessageHandler } from '@relaybus/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Structural views of the Kafka client API used by this package.
|
|
5
|
+
*
|
|
6
|
+
* They are satisfied by both `kafkajs` (`new Kafka(...)`) and
|
|
7
|
+
* `@confluentinc/kafka-javascript` (`new KafkaJS.Kafka(...)`, the KafkaJS
|
|
8
|
+
* compatible API), so neither is a hard dependency. Only the members listed
|
|
9
|
+
* here are ever called. Methods are declared with method syntax on purpose:
|
|
10
|
+
* it keeps the real client classes assignable despite their richer signatures.
|
|
11
|
+
*/
|
|
12
|
+
/** Bytes as delivered by a consumer: `Buffer` in both supported clients. */
|
|
13
|
+
type KafkaBytes = Uint8Array;
|
|
14
|
+
/** Header values a consumer may hand over: bytes, a string, a list of either, or nothing. */
|
|
15
|
+
type KafkaIncomingHeaders = Readonly<Record<string, KafkaBytes | string | readonly (KafkaBytes | string)[] | undefined>>;
|
|
16
|
+
/** Header values accepted when producing. */
|
|
17
|
+
type KafkaOutgoingHeaders = Readonly<Record<string, Buffer | string | (Buffer | string)[]>>;
|
|
18
|
+
/** One record handed to the producer. */
|
|
19
|
+
interface KafkaOutgoingMessage {
|
|
20
|
+
readonly key: string | Buffer | null;
|
|
21
|
+
readonly value: string | Buffer | null;
|
|
22
|
+
readonly headers: KafkaOutgoingHeaders;
|
|
23
|
+
}
|
|
24
|
+
/** Argument of `producer.send`. */
|
|
25
|
+
interface KafkaProducerRecord {
|
|
26
|
+
readonly topic: string;
|
|
27
|
+
readonly messages: KafkaOutgoingMessage[];
|
|
28
|
+
/** Always `-1`: wait for all in-sync replicas. */
|
|
29
|
+
readonly acks: number;
|
|
30
|
+
}
|
|
31
|
+
/** Producer settings this package decides itself; user settings are merged below them. */
|
|
32
|
+
interface KafkaProducerConfig {
|
|
33
|
+
readonly idempotent: boolean;
|
|
34
|
+
readonly maxInFlightRequests: number;
|
|
35
|
+
}
|
|
36
|
+
/** Consumer settings this package decides itself; user settings are merged below them. */
|
|
37
|
+
interface KafkaConsumerConfig {
|
|
38
|
+
readonly groupId: string;
|
|
39
|
+
}
|
|
40
|
+
/** Subset of the producer API: `connect`, `send`, `disconnect`. */
|
|
41
|
+
interface KafkaProducerLike {
|
|
42
|
+
connect(): Promise<void>;
|
|
43
|
+
send(record: KafkaProducerRecord): Promise<unknown>;
|
|
44
|
+
disconnect(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** A message as delivered to `eachMessage`. */
|
|
47
|
+
interface KafkaIncomingMessage {
|
|
48
|
+
readonly key: KafkaBytes | null;
|
|
49
|
+
readonly value: KafkaBytes | null;
|
|
50
|
+
readonly headers?: KafkaIncomingHeaders | undefined;
|
|
51
|
+
readonly offset: string;
|
|
52
|
+
/** Record timestamp as epoch milliseconds in a string (kafkajs and Confluent). */
|
|
53
|
+
readonly timestamp?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Argument of the `eachMessage` callback. `pause` and others are not used. */
|
|
56
|
+
interface KafkaEachMessagePayload {
|
|
57
|
+
readonly topic: string;
|
|
58
|
+
readonly partition: number;
|
|
59
|
+
readonly message: KafkaIncomingMessage;
|
|
60
|
+
heartbeat(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/** Argument of `consumer.run`. Offsets are committed automatically after `eachMessage` resolves. */
|
|
63
|
+
interface KafkaRunConfig {
|
|
64
|
+
eachMessage(payload: KafkaEachMessagePayload): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Instrumentation event names exposed as `consumer.events`. Optional: a client
|
|
68
|
+
* without them still works, but start-up cannot wait for the group join and
|
|
69
|
+
* `isConsuming()` cannot observe crashes.
|
|
70
|
+
*/
|
|
71
|
+
interface KafkaConsumerEventNames {
|
|
72
|
+
readonly GROUP_JOIN?: string;
|
|
73
|
+
readonly CRASH?: string;
|
|
74
|
+
readonly REBALANCING?: string;
|
|
75
|
+
readonly STOP?: string;
|
|
76
|
+
readonly DISCONNECT?: string;
|
|
77
|
+
}
|
|
78
|
+
/** Instrumentation event; `payload` carries `error` and `restart` for a crash. */
|
|
79
|
+
interface KafkaConsumerEvent {
|
|
80
|
+
readonly payload?: unknown;
|
|
81
|
+
}
|
|
82
|
+
/** Subset of the consumer API: `connect`, `subscribe`, `run`, `disconnect`, optional `on`/`events`. */
|
|
83
|
+
interface KafkaConsumerLike {
|
|
84
|
+
readonly events?: KafkaConsumerEventNames | undefined;
|
|
85
|
+
on?(eventName: string, listener: (event: KafkaConsumerEvent) => void): unknown;
|
|
86
|
+
connect(): Promise<void>;
|
|
87
|
+
subscribe(subscription: {
|
|
88
|
+
topics: string[];
|
|
89
|
+
fromBeginning: boolean;
|
|
90
|
+
}): Promise<void>;
|
|
91
|
+
run(config: KafkaRunConfig): Promise<void>;
|
|
92
|
+
disconnect(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
/** Subset of the client API: the `producer` and `consumer` factories. */
|
|
95
|
+
interface KafkaClientLike {
|
|
96
|
+
producer(config: KafkaProducerConfig): KafkaProducerLike;
|
|
97
|
+
consumer(config: KafkaConsumerConfig): KafkaConsumerLike;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A Kafka record that cannot be turned into an {@link EventEnvelope}. Never retried. */
|
|
101
|
+
declare class InvalidKafkaMessageError extends RelayBusError {
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* An event that cannot be written to Kafka: its payload is not representable as
|
|
105
|
+
* JSON (`undefined`, a function, a symbol, a `BigInt`, a circular structure) or
|
|
106
|
+
* its `occurredAt` is not an RFC 3339 instant (invalid date, year outside 0..9999).
|
|
107
|
+
* Retrying cannot help, so it is a {@link NonRetryableError}.
|
|
108
|
+
*/
|
|
109
|
+
declare class UnencodableEventError extends NonRetryableError {
|
|
110
|
+
name: string;
|
|
111
|
+
}
|
|
112
|
+
/** The wire form of one event: a Kafka record in CloudEvents binary content mode. */
|
|
113
|
+
interface EncodedMessage {
|
|
114
|
+
readonly key: string | null;
|
|
115
|
+
readonly value: string;
|
|
116
|
+
readonly headers: Record<string, string>;
|
|
117
|
+
}
|
|
118
|
+
/** The parts of an incoming Kafka record the decoder reads. */
|
|
119
|
+
interface DecodableMessage {
|
|
120
|
+
readonly key: KafkaBytes | null;
|
|
121
|
+
readonly value: KafkaBytes | null;
|
|
122
|
+
readonly headers?: KafkaIncomingHeaders | undefined;
|
|
123
|
+
/** Record timestamp in epoch milliseconds, used when `ce_time` is absent. */
|
|
124
|
+
readonly timestamp?: string | undefined;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Encodes an envelope as a CloudEvents Kafka binary-mode record: the value is
|
|
128
|
+
* the JSON payload, the event attributes travel as `ce_*` headers.
|
|
129
|
+
*
|
|
130
|
+
* User headers that would collide with protocol names (`ce_*`, `content-type`,
|
|
131
|
+
* `x-relaybus-*`) are stored under an `x-relaybus-h-` prefix and restored on
|
|
132
|
+
* decode, so every header survives a round trip unchanged.
|
|
133
|
+
*/
|
|
134
|
+
declare function encodeEnvelope(envelope: EventEnvelope): EncodedMessage;
|
|
135
|
+
/**
|
|
136
|
+
* Decodes a Kafka record strictly: the CloudEvents attributes `ce_specversion`
|
|
137
|
+
* (`1.0`), `ce_id`, `ce_type`, `ce_source`, a JSON `content-type` and a JSON
|
|
138
|
+
* value are required. `ce_time` is optional in CloudEvents: when absent, the
|
|
139
|
+
* Kafka record timestamp is used; when present it must be RFC 3339.
|
|
140
|
+
*
|
|
141
|
+
* @throws InvalidKafkaMessageError for anything else. It is the only error
|
|
142
|
+
* thrown for malformed input, so callers can tell poison messages from bugs.
|
|
143
|
+
*/
|
|
144
|
+
declare function decodeEnvelope(message: DecodableMessage): EventEnvelope;
|
|
145
|
+
|
|
146
|
+
/** A record the transport refused to hand to the bus because it is malformed. */
|
|
147
|
+
interface InvalidKafkaMessage {
|
|
148
|
+
readonly topic: string;
|
|
149
|
+
readonly partition: number;
|
|
150
|
+
readonly offset: string;
|
|
151
|
+
/** Why the record was rejected. */
|
|
152
|
+
readonly reason: string;
|
|
153
|
+
readonly key: Uint8Array | null;
|
|
154
|
+
readonly value: Uint8Array | null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* What to do with a record that cannot be processed for good (it cannot be
|
|
158
|
+
* decoded, or the bus rejected it with a `NonRetryableError`):
|
|
159
|
+
* - `'dead-letter'` (default): the original record is produced to
|
|
160
|
+
* `<source topic>.dead` with `relaybus-invalid-reason`, `relaybus-source-topic`,
|
|
161
|
+
* `relaybus-source-partition` and `relaybus-source-offset` headers added to the
|
|
162
|
+
* original ones; the offset advances only after the broker acked the copy.
|
|
163
|
+
* - `{ deadLetterTopic }`: same, with a fixed topic name or a function of the
|
|
164
|
+
* source topic.
|
|
165
|
+
* - a function: called with the record; if it rejects, the record is retried
|
|
166
|
+
* like any other failure, so nothing is dropped unless the function returns.
|
|
167
|
+
* - `'skip'`: log an error and drop the record. Explicit opt-in only.
|
|
168
|
+
*
|
|
169
|
+
* The dead-letter topic must exist or the broker must auto-create topics;
|
|
170
|
+
* otherwise the send fails and the record stays uncommitted (nothing is lost).
|
|
171
|
+
*/
|
|
172
|
+
type InvalidMessagePolicy = 'dead-letter' | 'skip' | ((message: InvalidKafkaMessage) => void | Promise<void>) | {
|
|
173
|
+
readonly deadLetterTopic: string | ((sourceTopic: string) => string);
|
|
174
|
+
};
|
|
175
|
+
/** `publish` did not get a broker ack within `publishTimeout`. The outbox retries it. */
|
|
176
|
+
declare class PublishTimeoutError extends RelayBusError {
|
|
177
|
+
name: string;
|
|
178
|
+
}
|
|
179
|
+
/** A handler failure re-thrown to the Kafka client, stripped of anything that could make it fatal. */
|
|
180
|
+
declare class HandlerFailedError extends RelayBusError {
|
|
181
|
+
name: string;
|
|
182
|
+
}
|
|
183
|
+
/** Bounded in-process retries around the inbound handler (the bus's `receive`). */
|
|
184
|
+
interface KafkaProcessingOptions {
|
|
185
|
+
/** Attempts per delivery of a record before the error is handed to the client. Default 5. */
|
|
186
|
+
readonly maxAttempts?: number;
|
|
187
|
+
/** Delay before the second attempt; doubles each attempt. Default `'200ms'`. */
|
|
188
|
+
readonly initialBackoff?: Duration;
|
|
189
|
+
/** Upper bound of the delay between attempts. Default `'5s'`. */
|
|
190
|
+
readonly maxBackoff?: Duration;
|
|
191
|
+
/** Interval of the heartbeats sent while sleeping between attempts. Default `'1s'`. */
|
|
192
|
+
readonly heartbeatInterval?: Duration;
|
|
193
|
+
}
|
|
194
|
+
/** Backoff of the consumer restart loop that runs after an unrecoverable crash. */
|
|
195
|
+
interface KafkaRestartOptions {
|
|
196
|
+
/** Delay before the second restart attempt; doubles each failure. Default `'1s'`. */
|
|
197
|
+
readonly initialBackoff?: Duration;
|
|
198
|
+
/** Upper bound of the delay between restart attempts. Default `'30s'`. */
|
|
199
|
+
readonly maxBackoff?: Duration;
|
|
200
|
+
}
|
|
201
|
+
interface KafkaTransportOptions {
|
|
202
|
+
/** A `kafkajs` `Kafka` or `@confluentinc/kafka-javascript` `KafkaJS.Kafka` instance. */
|
|
203
|
+
readonly kafka: KafkaClientLike;
|
|
204
|
+
/** Name referenced by routes and subscriptions. Default `'kafka'`. */
|
|
205
|
+
readonly name?: string;
|
|
206
|
+
/** Consumer group id. Required as soon as `subscribe` is used. */
|
|
207
|
+
readonly groupId?: string;
|
|
208
|
+
/**
|
|
209
|
+
* Where a group without committed offsets starts. Default `true` (earliest),
|
|
210
|
+
* so events published before the first start are not skipped.
|
|
211
|
+
*/
|
|
212
|
+
readonly fromBeginning?: boolean;
|
|
213
|
+
/** Producer `maxInFlightRequests`, 1 to 5. Default 1, which keeps retries from reordering a key. */
|
|
214
|
+
readonly maxInFlightRequests?: number;
|
|
215
|
+
/** Extra producer settings; `idempotent` and `maxInFlightRequests` always win. */
|
|
216
|
+
readonly producerConfig?: object;
|
|
217
|
+
/** Extra consumer settings; `groupId` always wins. */
|
|
218
|
+
readonly consumerConfig?: object;
|
|
219
|
+
readonly processing?: KafkaProcessingOptions;
|
|
220
|
+
/** How long `subscribe` waits for the consumer group join before failing. Default `'30s'`. */
|
|
221
|
+
readonly startupTimeout?: Duration;
|
|
222
|
+
/** How long `publish` waits for the broker ack before rejecting. Default `'30s'`. */
|
|
223
|
+
readonly publishTimeout?: Duration;
|
|
224
|
+
readonly restart?: KafkaRestartOptions;
|
|
225
|
+
/** Handling of records that cannot be processed for good. Default `'dead-letter'`. */
|
|
226
|
+
readonly onInvalidMessage?: InvalidMessagePolicy;
|
|
227
|
+
/** Default `console`. */
|
|
228
|
+
readonly logger?: RelayBusLogger;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Kafka {@link Transport}. Events travel as CloudEvents binary-mode records
|
|
232
|
+
* (`ce_*` headers, JSON value), so any CloudEvents-aware consumer can read them.
|
|
233
|
+
*
|
|
234
|
+
* Publishing: idempotent producer, `acks=-1`, resolves after the broker ack and
|
|
235
|
+
* rejects with {@link PublishTimeoutError} after `publishTimeout`. A timed-out
|
|
236
|
+
* send may still complete later; the outbox then retries it and the duplicate is
|
|
237
|
+
* absorbed by the consumer's inbox dedup. The record key is the envelope key, so events sharing a key land on one
|
|
238
|
+
* partition and keep their order; events without a key have no ordering.
|
|
239
|
+
*
|
|
240
|
+
* Subscribing: one consumer, one group, one `subscribe` call. `eachMessage`
|
|
241
|
+
* awaits the handler; the client commits the offset only after it resolved. A
|
|
242
|
+
* failing handler is retried in-process (bounded, with backoff) and then the
|
|
243
|
+
* error is rethrown (as a {@link HandlerFailedError}, so the client never treats
|
|
244
|
+
* it as fatal) and the client does not commit the offset and redelivers the
|
|
245
|
+
* record. A record is therefore never skipped because of a handler failure; the
|
|
246
|
+
* partition simply waits. Records that can never succeed (undecodable, or
|
|
247
|
+
* rejected with a `NonRetryableError`) are dead-lettered instead. If the client
|
|
248
|
+
* reports an unrecoverable crash anyway, the consumer run loop is restarted with
|
|
249
|
+
* bounded backoff until the transport is closed.
|
|
250
|
+
*/
|
|
251
|
+
declare class KafkaTransport implements Transport {
|
|
252
|
+
#private;
|
|
253
|
+
readonly name: string;
|
|
254
|
+
constructor(options: KafkaTransportOptions);
|
|
255
|
+
/**
|
|
256
|
+
* True while the consumer is joined to its group, not between a crash or
|
|
257
|
+
* rebalance and the next join, and no partition is stalled on a record whose
|
|
258
|
+
* handler keeps failing. Use it for readiness so a consumer that silently
|
|
259
|
+
* stopped is noticed.
|
|
260
|
+
*/
|
|
261
|
+
isConsuming(): boolean;
|
|
262
|
+
/**
|
|
263
|
+
* True once the producer is connected and, if the transport subscribed, the
|
|
264
|
+
* consumer is consuming. False before `connect`, after `close` and while a
|
|
265
|
+
* dead consumer is being restarted.
|
|
266
|
+
*/
|
|
267
|
+
isReady(): boolean;
|
|
268
|
+
/** Connects the producer. Called by the bus on start; `publish` also connects on demand. */
|
|
269
|
+
connect(): Promise<void>;
|
|
270
|
+
publish(envelope: EventEnvelope, destination: string): Promise<void>;
|
|
271
|
+
/**
|
|
272
|
+
* Starts the consumer and resolves once it joined its group, so a start-up
|
|
273
|
+
* that cannot consume fails loudly instead of looking healthy.
|
|
274
|
+
*
|
|
275
|
+
* @throws ConfigurationError when `groupId` is missing, `destinations` is empty
|
|
276
|
+
* or the transport already subscribed (create one transport per subscription).
|
|
277
|
+
*/
|
|
278
|
+
subscribe(destinations: readonly string[], onMessage: TransportMessageHandler): Promise<void>;
|
|
279
|
+
/** Stops consuming and disconnects the consumer and the producer. Safe to call twice. */
|
|
280
|
+
close(): Promise<void>;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export { type DecodableMessage, type EncodedMessage, HandlerFailedError, type InvalidKafkaMessage, InvalidKafkaMessageError, type InvalidMessagePolicy, type KafkaBytes, type KafkaClientLike, type KafkaConsumerConfig, type KafkaConsumerEvent, type KafkaConsumerEventNames, type KafkaConsumerLike, type KafkaEachMessagePayload, type KafkaIncomingHeaders, type KafkaIncomingMessage, type KafkaOutgoingHeaders, type KafkaOutgoingMessage, type KafkaProcessingOptions, type KafkaProducerConfig, type KafkaProducerLike, type KafkaProducerRecord, type KafkaRestartOptions, type KafkaRunConfig, KafkaTransport, type KafkaTransportOptions, PublishTimeoutError, UnencodableEventError, decodeEnvelope, encodeEnvelope };
|