@relaybus/kafka 0.1.0 → 0.3.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/README.md +166 -21
- package/dist/index.cjs +458 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +245 -23
- package/dist/index.d.ts +245 -23
- package/dist/index.js +446 -98
- package/dist/index.js.map +1 -1
- package/package.json +22 -9
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
|
-
import { RelayBusError, NonRetryableError, EventEnvelope, Duration, Transport, RelayBusLogger, TransportMessageHandler } from '@relaybus/core';
|
|
1
|
+
import { RelayBusError, NonRetryableError, EventEnvelope, EventHeaders, Duration, Transport, RelayBusLogger, TransportMessageHandler, TransportSubscriptionHooks } from '@relaybus/core';
|
|
2
|
+
import { JsonEnvelopeMapping } from '@relaybus/core/internal';
|
|
3
|
+
export { fromHeader, fromPayloadField } from '@relaybus/core/internal';
|
|
4
|
+
|
|
5
|
+
/** A Kafka record that cannot be turned into an {@link EventEnvelope}. Never retried. */
|
|
6
|
+
declare class InvalidKafkaMessageError extends RelayBusError {
|
|
7
|
+
name: string;
|
|
8
|
+
readonly code: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* An event that cannot be written to Kafka: its payload is not representable as
|
|
12
|
+
* JSON (`undefined`, a function, a symbol, a `BigInt`, a circular structure) or
|
|
13
|
+
* its `occurredAt` is not an RFC 3339 instant (invalid date, year outside 0..9999).
|
|
14
|
+
* Retrying cannot help, so it is a {@link NonRetryableError}.
|
|
15
|
+
*/
|
|
16
|
+
declare class UnencodableEventError extends NonRetryableError {
|
|
17
|
+
name: string;
|
|
18
|
+
readonly code: string;
|
|
19
|
+
}
|
|
20
|
+
/** `publish` did not get a broker ack within `publishTimeout`. The outbox retries it. */
|
|
21
|
+
declare class PublishTimeoutError extends RelayBusError {
|
|
22
|
+
name: string;
|
|
23
|
+
readonly code: string;
|
|
24
|
+
}
|
|
25
|
+
/** The transport was closed: it no longer publishes or subscribes. */
|
|
26
|
+
declare class TransportClosedError extends RelayBusError {
|
|
27
|
+
name: string;
|
|
28
|
+
readonly code: string;
|
|
29
|
+
}
|
|
30
|
+
/** The consumer did not join its group within `startupTimeout`. */
|
|
31
|
+
declare class GroupJoinTimeoutError extends RelayBusError {
|
|
32
|
+
name: string;
|
|
33
|
+
readonly code: string;
|
|
34
|
+
}
|
|
35
|
+
/** A handler failure re-thrown to the Kafka client, stripped of anything that could make it fatal. */
|
|
36
|
+
declare class HandlerFailedError extends RelayBusError {
|
|
37
|
+
name: string;
|
|
38
|
+
readonly code: string;
|
|
39
|
+
}
|
|
2
40
|
|
|
3
41
|
/**
|
|
4
42
|
* Structural views of the Kafka client API used by this package.
|
|
@@ -97,18 +135,6 @@ interface KafkaClientLike {
|
|
|
97
135
|
consumer(config: KafkaConsumerConfig): KafkaConsumerLike;
|
|
98
136
|
}
|
|
99
137
|
|
|
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
138
|
/** The wire form of one event: a Kafka record in CloudEvents binary content mode. */
|
|
113
139
|
interface EncodedMessage {
|
|
114
140
|
readonly key: string | null;
|
|
@@ -143,6 +169,188 @@ declare function encodeEnvelope(envelope: EventEnvelope): EncodedMessage;
|
|
|
143
169
|
*/
|
|
144
170
|
declare function decodeEnvelope(message: DecodableMessage): EventEnvelope;
|
|
145
171
|
|
|
172
|
+
/** A consumed record as handed to a {@link KafkaMessageDecoder}. */
|
|
173
|
+
interface KafkaInboundRecord extends DecodableMessage {
|
|
174
|
+
readonly topic: string;
|
|
175
|
+
readonly partition: number;
|
|
176
|
+
readonly offset: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The event a decoder extracted from a record. `id`, `type` and `payload` are
|
|
180
|
+
* required; every other field may be left out and is then filled by the
|
|
181
|
+
* transport: `source` with the topic, `key` with the record key (UTF-8),
|
|
182
|
+
* `headers` with none, `correlationId` and `causationId` with null, and
|
|
183
|
+
* `occurredAt` with the record timestamp.
|
|
184
|
+
*/
|
|
185
|
+
interface DecodedEvent {
|
|
186
|
+
readonly id: string;
|
|
187
|
+
readonly type: string;
|
|
188
|
+
readonly payload: unknown;
|
|
189
|
+
readonly source?: string;
|
|
190
|
+
readonly key?: string | null;
|
|
191
|
+
readonly headers?: EventHeaders;
|
|
192
|
+
readonly correlationId?: string | null;
|
|
193
|
+
readonly causationId?: string | null;
|
|
194
|
+
readonly occurredAt?: Date;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Turns a consumed record into an event, for topics written by producers that
|
|
198
|
+
* do not use relaybus. Must be synchronous and free of side effects: it runs
|
|
199
|
+
* again on every redelivery, so the same record must always give the same
|
|
200
|
+
* event, above all the same `id`, which is what the inbox deduplicates on.
|
|
201
|
+
*
|
|
202
|
+
* Anything it throws marks the record as malformed: it goes to the transport's
|
|
203
|
+
* `onInvalidMessage` policy (dead-letter by default) and the consumer moves on.
|
|
204
|
+
* An event the bus then rejects (empty id, invalid type or time) is
|
|
205
|
+
* dead-lettered the same way.
|
|
206
|
+
*/
|
|
207
|
+
type KafkaMessageDecoder = (record: KafkaInboundRecord) => DecodedEvent;
|
|
208
|
+
/** A plain JSON record, decoded for the extractor options of {@link plainJsonDecoder}. */
|
|
209
|
+
interface PlainJsonRecord {
|
|
210
|
+
readonly topic: string;
|
|
211
|
+
readonly partition: number;
|
|
212
|
+
readonly offset: string;
|
|
213
|
+
/**
|
|
214
|
+
* The record key as UTF-8 text, base64 when its bytes are not valid UTF-8
|
|
215
|
+
* (for example a key written by Java's `LongSerializer`), null when absent.
|
|
216
|
+
*/
|
|
217
|
+
readonly key: string | null;
|
|
218
|
+
/**
|
|
219
|
+
* Every record header as text: the last value of a repeated header; a value
|
|
220
|
+
* that is not valid UTF-8 is left out.
|
|
221
|
+
*/
|
|
222
|
+
readonly headers: EventHeaders;
|
|
223
|
+
/** The record value parsed as JSON. */
|
|
224
|
+
readonly payload: unknown;
|
|
225
|
+
/** Record timestamp in epoch milliseconds. */
|
|
226
|
+
readonly timestamp: string | undefined;
|
|
227
|
+
}
|
|
228
|
+
/** Options of {@link plainJsonDecoder}. Extractors returning undefined or `''` select the default. */
|
|
229
|
+
interface PlainJsonDecoderOptions {
|
|
230
|
+
/**
|
|
231
|
+
* Event type handlers subscribe to: a fixed name for a topic that carries one
|
|
232
|
+
* kind of event, or an extractor such as `fromHeader('event-type')` or
|
|
233
|
+
* `fromPayloadField('type')`. Required; a record without a type is invalid.
|
|
234
|
+
*/
|
|
235
|
+
readonly type: string | ((record: PlainJsonRecord) => string | undefined);
|
|
236
|
+
/**
|
|
237
|
+
* Event id the inbox deduplicates on, for example `fromHeader('message-id')`.
|
|
238
|
+
* Default, and fallback when the extractor finds none:
|
|
239
|
+
* `kafka:<topic>:<partition>:<offset>`, which is stable across redeliveries
|
|
240
|
+
* of the record. Prefer an id set by the producer when it may send the same
|
|
241
|
+
* event twice: two sends are two offsets, so only a producer id dedupes them.
|
|
242
|
+
*/
|
|
243
|
+
readonly id?: (record: PlainJsonRecord) => string | undefined;
|
|
244
|
+
/** Ordering key. Default: the record key. When given, its result is final (undefined means no key). */
|
|
245
|
+
readonly key?: (record: PlainJsonRecord) => string | null | undefined;
|
|
246
|
+
/** Logical name of the producer. Default: the topic. */
|
|
247
|
+
readonly source?: string | ((record: PlainJsonRecord) => string | undefined);
|
|
248
|
+
/** When the event happened. Default: the record timestamp. */
|
|
249
|
+
readonly occurredAt?: (record: PlainJsonRecord) => Date | undefined;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Decodes a CloudEvents 1.0 record in either content mode of the Kafka
|
|
253
|
+
* protocol binding:
|
|
254
|
+
* - structured (`content-type: application/cloudevents+json`): the value is
|
|
255
|
+
* a JSON object with `specversion`, `id`, `type`, `source` and optionally
|
|
256
|
+
* `time`, `subject` (the key, else the record key), `data` (the payload,
|
|
257
|
+
* null when absent) and a JSON `datacontenttype`. `data_base64` is refused,
|
|
258
|
+
* because relaybus payloads are JSON. Extension attributes with string
|
|
259
|
+
* values (`traceparent`, ...) become event headers, as do the record
|
|
260
|
+
* headers; `correlationid` and `causationid` fill their envelope fields.
|
|
261
|
+
* - binary (anything else): exactly {@link decodeEnvelope}, the default decoder.
|
|
262
|
+
*
|
|
263
|
+
* @throws InvalidKafkaMessageError for a record that is neither.
|
|
264
|
+
*/
|
|
265
|
+
declare function decodeCloudEvent(record: DecodableMessage): EventEnvelope;
|
|
266
|
+
/**
|
|
267
|
+
* Creates a decoder for records whose value is plain JSON, as written by a
|
|
268
|
+
* service that does not use relaybus (a plain kafkajs producer, Spring Kafka's
|
|
269
|
+
* `JsonSerializer`, a Go service): the value becomes the payload, the record
|
|
270
|
+
* headers become event headers, and the options say where the event type and
|
|
271
|
+
* id come from.
|
|
272
|
+
*
|
|
273
|
+
* A record that is a CloudEvent anyway (a `ce_specversion` header, or a
|
|
274
|
+
* structured `application/cloudevents+json` value) is decoded with
|
|
275
|
+
* {@link decodeCloudEvent} instead, so relaybus and foreign producers can share
|
|
276
|
+
* a topic. A missing or non-JSON value, a missing or invalid type, and an
|
|
277
|
+
* extractor returning anything but a string are invalid.
|
|
278
|
+
*
|
|
279
|
+
* ```ts
|
|
280
|
+
* new KafkaTransport({
|
|
281
|
+
* kafka,
|
|
282
|
+
* groupId: 'billing',
|
|
283
|
+
* decode: plainJsonDecoder({ type: fromHeader('event-type'), id: fromHeader('message-id') }),
|
|
284
|
+
* });
|
|
285
|
+
* ```
|
|
286
|
+
*
|
|
287
|
+
* @throws ConfigurationError when a fixed `type` is not a valid event type.
|
|
288
|
+
*/
|
|
289
|
+
declare function plainJsonDecoder(options: PlainJsonDecoderOptions): KafkaMessageDecoder;
|
|
290
|
+
|
|
291
|
+
/** The record a codec produces for one event. `timestamp` is epoch milliseconds in a string. */
|
|
292
|
+
type KafkaEncodedRecord = KafkaOutgoingMessage & {
|
|
293
|
+
readonly timestamp?: string;
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* The wire format of a {@link KafkaTransport}: how an event becomes a record
|
|
297
|
+
* (`encode`, on publish) and a consumed record becomes an event (`decode`).
|
|
298
|
+
*
|
|
299
|
+
* Both must be synchronous and free of side effects. What `encode` throws
|
|
300
|
+
* fails the publish: the outbox retries it, unless it is a `NonRetryableError`
|
|
301
|
+
* such as {@link UnencodableEventError}. What `decode` throws marks the record
|
|
302
|
+
* as malformed: it goes to `onInvalidMessage` (dead-letter by default) and the
|
|
303
|
+
* consumer moves on. See {@link KafkaMessageDecoder} for the decode contract.
|
|
304
|
+
*/
|
|
305
|
+
interface KafkaCodec {
|
|
306
|
+
readonly encode: (envelope: EventEnvelope) => KafkaEncodedRecord;
|
|
307
|
+
readonly decode: KafkaMessageDecoder;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* CloudEvents 1.0 binary content mode, the default: the value is the JSON
|
|
311
|
+
* payload, the attributes travel as `ce_*` headers ({@link encodeEnvelope}).
|
|
312
|
+
* Decoding is strict: any other record is malformed ({@link decodeEnvelope}).
|
|
313
|
+
*/
|
|
314
|
+
declare const cloudEventsBinary: KafkaCodec;
|
|
315
|
+
/**
|
|
316
|
+
* CloudEvents 1.0 structured content mode: the value is one
|
|
317
|
+
* `application/cloudevents+json` document with the attributes and `data`.
|
|
318
|
+
* Decoding accepts structured and binary mode ({@link decodeCloudEvent}), so a
|
|
319
|
+
* topic can move from one to the other while both are in flight.
|
|
320
|
+
*/
|
|
321
|
+
declare const cloudEventsStructured: KafkaCodec;
|
|
322
|
+
/**
|
|
323
|
+
* Where each value of a JSON message lives, for {@link jsonEnvelope}. Besides
|
|
324
|
+
* `{ body }` and `{ header }`, the id may be `'record-position'`: a consumed
|
|
325
|
+
* record gets `kafka:<topic>:<partition>:<offset>`, stable across redeliveries
|
|
326
|
+
* but different for two sends of one event. Records relaybus writes carry the
|
|
327
|
+
* event id in a `relaybus-event-id` header, which wins on read, so an outbox
|
|
328
|
+
* retry that sends an event twice is still deduplicated.
|
|
329
|
+
*/
|
|
330
|
+
type KafkaJsonEnvelopeMapping = JsonEnvelopeMapping<'record-position'>;
|
|
331
|
+
/**
|
|
332
|
+
* A JSON wire format declared by a mapping, in both directions: for topics
|
|
333
|
+
* shared with services that wrap events in their own envelope
|
|
334
|
+
* (`{ "eventName": ..., "data": {...} }`, MassTransit) or send the bare
|
|
335
|
+
* payload. The record key is always the event key; when `occurredAt` is not
|
|
336
|
+
* mapped, the record timestamp carries it. Event headers that are message
|
|
337
|
+
* headers are written as they are, so a name the mapping uses for another
|
|
338
|
+
* field is refused with {@link UnencodableEventError}, as are `content-type`
|
|
339
|
+
* and `relaybus-event-id`.
|
|
340
|
+
*
|
|
341
|
+
* ```ts
|
|
342
|
+
* codec: jsonEnvelope({
|
|
343
|
+
* type: { body: 'eventName' },
|
|
344
|
+
* id: { body: 'messageId' },
|
|
345
|
+
* payload: { body: 'data' },
|
|
346
|
+
* correlationId: { body: 'meta.correlationId' },
|
|
347
|
+
* })
|
|
348
|
+
* ```
|
|
349
|
+
*
|
|
350
|
+
* @throws ConfigurationError for an incomplete or self-overlapping mapping.
|
|
351
|
+
*/
|
|
352
|
+
declare function jsonEnvelope(mapping: KafkaJsonEnvelopeMapping): KafkaCodec;
|
|
353
|
+
|
|
146
354
|
/** A record the transport refused to hand to the bus because it is malformed. */
|
|
147
355
|
interface InvalidKafkaMessage {
|
|
148
356
|
readonly topic: string;
|
|
@@ -172,14 +380,6 @@ interface InvalidKafkaMessage {
|
|
|
172
380
|
type InvalidMessagePolicy = 'dead-letter' | 'skip' | ((message: InvalidKafkaMessage) => void | Promise<void>) | {
|
|
173
381
|
readonly deadLetterTopic: string | ((sourceTopic: string) => string);
|
|
174
382
|
};
|
|
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
383
|
/** Bounded in-process retries around the inbound handler (the bus's `receive`). */
|
|
184
384
|
interface KafkaProcessingOptions {
|
|
185
385
|
/** Attempts per delivery of a record before the error is handed to the client. Default 5. */
|
|
@@ -224,12 +424,33 @@ interface KafkaTransportOptions {
|
|
|
224
424
|
readonly restart?: KafkaRestartOptions;
|
|
225
425
|
/** Handling of records that cannot be processed for good. Default `'dead-letter'`. */
|
|
226
426
|
readonly onInvalidMessage?: InvalidMessagePolicy;
|
|
427
|
+
/**
|
|
428
|
+
* The wire format, both ways: how events become records and consumed records
|
|
429
|
+
* become events. Default {@link cloudEventsBinary}: CloudEvents binary mode,
|
|
430
|
+
* strict on read, so any other record is malformed and goes to
|
|
431
|
+
* `onInvalidMessage`. {@link cloudEventsStructured} writes structured mode
|
|
432
|
+
* and reads both, {@link jsonEnvelope} maps a JSON format of your own (an
|
|
433
|
+
* envelope with the payload inside, or the bare payload), and an object with
|
|
434
|
+
* `encode` and `decode` can do anything else. Whatever a non-default codec's
|
|
435
|
+
* `decode` throws marks the record as malformed, never crashes the consumer.
|
|
436
|
+
*/
|
|
437
|
+
readonly codec?: KafkaCodec;
|
|
438
|
+
/**
|
|
439
|
+
* How consumed records become events; events are still written in
|
|
440
|
+
* CloudEvents binary mode.
|
|
441
|
+
*
|
|
442
|
+
* @deprecated Use `codec`: `decode: fn` is `codec: { encode: encodeEnvelope, decode: fn }`.
|
|
443
|
+
* Giving both `decode` and `codec` is a configuration error.
|
|
444
|
+
*/
|
|
445
|
+
readonly decode?: KafkaMessageDecoder;
|
|
227
446
|
/** Default `console`. */
|
|
228
447
|
readonly logger?: RelayBusLogger;
|
|
229
448
|
}
|
|
230
449
|
/**
|
|
231
450
|
* Kafka {@link Transport}. Events travel as CloudEvents binary-mode records
|
|
232
451
|
* (`ce_*` headers, JSON value), so any CloudEvents-aware consumer can read them.
|
|
452
|
+
* The `codec` option changes the wire format, for example to read and write
|
|
453
|
+
* the JSON format of services that do not use relaybus.
|
|
233
454
|
*
|
|
234
455
|
* Publishing: idempotent producer, `acks=-1`, resolves after the broker ack and
|
|
235
456
|
* rejects with {@link PublishTimeoutError} after `publishTimeout`. A timed-out
|
|
@@ -251,6 +472,7 @@ interface KafkaTransportOptions {
|
|
|
251
472
|
declare class KafkaTransport implements Transport {
|
|
252
473
|
#private;
|
|
253
474
|
readonly name: string;
|
|
475
|
+
readonly system = "kafka";
|
|
254
476
|
constructor(options: KafkaTransportOptions);
|
|
255
477
|
/**
|
|
256
478
|
* True while the consumer is joined to its group, not between a crash or
|
|
@@ -275,9 +497,9 @@ declare class KafkaTransport implements Transport {
|
|
|
275
497
|
* @throws ConfigurationError when `groupId` is missing, `destinations` is empty
|
|
276
498
|
* or the transport already subscribed (create one transport per subscription).
|
|
277
499
|
*/
|
|
278
|
-
subscribe(destinations: readonly string[], onMessage: TransportMessageHandler): Promise<void>;
|
|
500
|
+
subscribe(destinations: readonly string[], onMessage: TransportMessageHandler, hooks?: TransportSubscriptionHooks): Promise<void>;
|
|
279
501
|
/** Stops consuming and disconnects the consumer and the producer. Safe to call twice. */
|
|
280
502
|
close(): Promise<void>;
|
|
281
503
|
}
|
|
282
504
|
|
|
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 };
|
|
505
|
+
export { type DecodableMessage, type DecodedEvent, type EncodedMessage, GroupJoinTimeoutError, HandlerFailedError, type InvalidKafkaMessage, InvalidKafkaMessageError, type InvalidMessagePolicy, type KafkaBytes, type KafkaClientLike, type KafkaCodec, type KafkaConsumerConfig, type KafkaConsumerEvent, type KafkaConsumerEventNames, type KafkaConsumerLike, type KafkaEachMessagePayload, type KafkaEncodedRecord, type KafkaInboundRecord, type KafkaIncomingHeaders, type KafkaIncomingMessage, type KafkaJsonEnvelopeMapping, type KafkaMessageDecoder, type KafkaOutgoingHeaders, type KafkaOutgoingMessage, type KafkaProcessingOptions, type KafkaProducerConfig, type KafkaProducerLike, type KafkaProducerRecord, type KafkaRestartOptions, type KafkaRunConfig, KafkaTransport, type KafkaTransportOptions, type PlainJsonDecoderOptions, type PlainJsonRecord, PublishTimeoutError, TransportClosedError, UnencodableEventError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, decodeEnvelope, encodeEnvelope, jsonEnvelope, plainJsonDecoder };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
|
-
import { RelayBusError, NonRetryableError, EventEnvelope, Duration, Transport, RelayBusLogger, TransportMessageHandler } from '@relaybus/core';
|
|
1
|
+
import { RelayBusError, NonRetryableError, EventEnvelope, EventHeaders, Duration, Transport, RelayBusLogger, TransportMessageHandler, TransportSubscriptionHooks } from '@relaybus/core';
|
|
2
|
+
import { JsonEnvelopeMapping } from '@relaybus/core/internal';
|
|
3
|
+
export { fromHeader, fromPayloadField } from '@relaybus/core/internal';
|
|
4
|
+
|
|
5
|
+
/** A Kafka record that cannot be turned into an {@link EventEnvelope}. Never retried. */
|
|
6
|
+
declare class InvalidKafkaMessageError extends RelayBusError {
|
|
7
|
+
name: string;
|
|
8
|
+
readonly code: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* An event that cannot be written to Kafka: its payload is not representable as
|
|
12
|
+
* JSON (`undefined`, a function, a symbol, a `BigInt`, a circular structure) or
|
|
13
|
+
* its `occurredAt` is not an RFC 3339 instant (invalid date, year outside 0..9999).
|
|
14
|
+
* Retrying cannot help, so it is a {@link NonRetryableError}.
|
|
15
|
+
*/
|
|
16
|
+
declare class UnencodableEventError extends NonRetryableError {
|
|
17
|
+
name: string;
|
|
18
|
+
readonly code: string;
|
|
19
|
+
}
|
|
20
|
+
/** `publish` did not get a broker ack within `publishTimeout`. The outbox retries it. */
|
|
21
|
+
declare class PublishTimeoutError extends RelayBusError {
|
|
22
|
+
name: string;
|
|
23
|
+
readonly code: string;
|
|
24
|
+
}
|
|
25
|
+
/** The transport was closed: it no longer publishes or subscribes. */
|
|
26
|
+
declare class TransportClosedError extends RelayBusError {
|
|
27
|
+
name: string;
|
|
28
|
+
readonly code: string;
|
|
29
|
+
}
|
|
30
|
+
/** The consumer did not join its group within `startupTimeout`. */
|
|
31
|
+
declare class GroupJoinTimeoutError extends RelayBusError {
|
|
32
|
+
name: string;
|
|
33
|
+
readonly code: string;
|
|
34
|
+
}
|
|
35
|
+
/** A handler failure re-thrown to the Kafka client, stripped of anything that could make it fatal. */
|
|
36
|
+
declare class HandlerFailedError extends RelayBusError {
|
|
37
|
+
name: string;
|
|
38
|
+
readonly code: string;
|
|
39
|
+
}
|
|
2
40
|
|
|
3
41
|
/**
|
|
4
42
|
* Structural views of the Kafka client API used by this package.
|
|
@@ -97,18 +135,6 @@ interface KafkaClientLike {
|
|
|
97
135
|
consumer(config: KafkaConsumerConfig): KafkaConsumerLike;
|
|
98
136
|
}
|
|
99
137
|
|
|
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
138
|
/** The wire form of one event: a Kafka record in CloudEvents binary content mode. */
|
|
113
139
|
interface EncodedMessage {
|
|
114
140
|
readonly key: string | null;
|
|
@@ -143,6 +169,188 @@ declare function encodeEnvelope(envelope: EventEnvelope): EncodedMessage;
|
|
|
143
169
|
*/
|
|
144
170
|
declare function decodeEnvelope(message: DecodableMessage): EventEnvelope;
|
|
145
171
|
|
|
172
|
+
/** A consumed record as handed to a {@link KafkaMessageDecoder}. */
|
|
173
|
+
interface KafkaInboundRecord extends DecodableMessage {
|
|
174
|
+
readonly topic: string;
|
|
175
|
+
readonly partition: number;
|
|
176
|
+
readonly offset: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The event a decoder extracted from a record. `id`, `type` and `payload` are
|
|
180
|
+
* required; every other field may be left out and is then filled by the
|
|
181
|
+
* transport: `source` with the topic, `key` with the record key (UTF-8),
|
|
182
|
+
* `headers` with none, `correlationId` and `causationId` with null, and
|
|
183
|
+
* `occurredAt` with the record timestamp.
|
|
184
|
+
*/
|
|
185
|
+
interface DecodedEvent {
|
|
186
|
+
readonly id: string;
|
|
187
|
+
readonly type: string;
|
|
188
|
+
readonly payload: unknown;
|
|
189
|
+
readonly source?: string;
|
|
190
|
+
readonly key?: string | null;
|
|
191
|
+
readonly headers?: EventHeaders;
|
|
192
|
+
readonly correlationId?: string | null;
|
|
193
|
+
readonly causationId?: string | null;
|
|
194
|
+
readonly occurredAt?: Date;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Turns a consumed record into an event, for topics written by producers that
|
|
198
|
+
* do not use relaybus. Must be synchronous and free of side effects: it runs
|
|
199
|
+
* again on every redelivery, so the same record must always give the same
|
|
200
|
+
* event, above all the same `id`, which is what the inbox deduplicates on.
|
|
201
|
+
*
|
|
202
|
+
* Anything it throws marks the record as malformed: it goes to the transport's
|
|
203
|
+
* `onInvalidMessage` policy (dead-letter by default) and the consumer moves on.
|
|
204
|
+
* An event the bus then rejects (empty id, invalid type or time) is
|
|
205
|
+
* dead-lettered the same way.
|
|
206
|
+
*/
|
|
207
|
+
type KafkaMessageDecoder = (record: KafkaInboundRecord) => DecodedEvent;
|
|
208
|
+
/** A plain JSON record, decoded for the extractor options of {@link plainJsonDecoder}. */
|
|
209
|
+
interface PlainJsonRecord {
|
|
210
|
+
readonly topic: string;
|
|
211
|
+
readonly partition: number;
|
|
212
|
+
readonly offset: string;
|
|
213
|
+
/**
|
|
214
|
+
* The record key as UTF-8 text, base64 when its bytes are not valid UTF-8
|
|
215
|
+
* (for example a key written by Java's `LongSerializer`), null when absent.
|
|
216
|
+
*/
|
|
217
|
+
readonly key: string | null;
|
|
218
|
+
/**
|
|
219
|
+
* Every record header as text: the last value of a repeated header; a value
|
|
220
|
+
* that is not valid UTF-8 is left out.
|
|
221
|
+
*/
|
|
222
|
+
readonly headers: EventHeaders;
|
|
223
|
+
/** The record value parsed as JSON. */
|
|
224
|
+
readonly payload: unknown;
|
|
225
|
+
/** Record timestamp in epoch milliseconds. */
|
|
226
|
+
readonly timestamp: string | undefined;
|
|
227
|
+
}
|
|
228
|
+
/** Options of {@link plainJsonDecoder}. Extractors returning undefined or `''` select the default. */
|
|
229
|
+
interface PlainJsonDecoderOptions {
|
|
230
|
+
/**
|
|
231
|
+
* Event type handlers subscribe to: a fixed name for a topic that carries one
|
|
232
|
+
* kind of event, or an extractor such as `fromHeader('event-type')` or
|
|
233
|
+
* `fromPayloadField('type')`. Required; a record without a type is invalid.
|
|
234
|
+
*/
|
|
235
|
+
readonly type: string | ((record: PlainJsonRecord) => string | undefined);
|
|
236
|
+
/**
|
|
237
|
+
* Event id the inbox deduplicates on, for example `fromHeader('message-id')`.
|
|
238
|
+
* Default, and fallback when the extractor finds none:
|
|
239
|
+
* `kafka:<topic>:<partition>:<offset>`, which is stable across redeliveries
|
|
240
|
+
* of the record. Prefer an id set by the producer when it may send the same
|
|
241
|
+
* event twice: two sends are two offsets, so only a producer id dedupes them.
|
|
242
|
+
*/
|
|
243
|
+
readonly id?: (record: PlainJsonRecord) => string | undefined;
|
|
244
|
+
/** Ordering key. Default: the record key. When given, its result is final (undefined means no key). */
|
|
245
|
+
readonly key?: (record: PlainJsonRecord) => string | null | undefined;
|
|
246
|
+
/** Logical name of the producer. Default: the topic. */
|
|
247
|
+
readonly source?: string | ((record: PlainJsonRecord) => string | undefined);
|
|
248
|
+
/** When the event happened. Default: the record timestamp. */
|
|
249
|
+
readonly occurredAt?: (record: PlainJsonRecord) => Date | undefined;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Decodes a CloudEvents 1.0 record in either content mode of the Kafka
|
|
253
|
+
* protocol binding:
|
|
254
|
+
* - structured (`content-type: application/cloudevents+json`): the value is
|
|
255
|
+
* a JSON object with `specversion`, `id`, `type`, `source` and optionally
|
|
256
|
+
* `time`, `subject` (the key, else the record key), `data` (the payload,
|
|
257
|
+
* null when absent) and a JSON `datacontenttype`. `data_base64` is refused,
|
|
258
|
+
* because relaybus payloads are JSON. Extension attributes with string
|
|
259
|
+
* values (`traceparent`, ...) become event headers, as do the record
|
|
260
|
+
* headers; `correlationid` and `causationid` fill their envelope fields.
|
|
261
|
+
* - binary (anything else): exactly {@link decodeEnvelope}, the default decoder.
|
|
262
|
+
*
|
|
263
|
+
* @throws InvalidKafkaMessageError for a record that is neither.
|
|
264
|
+
*/
|
|
265
|
+
declare function decodeCloudEvent(record: DecodableMessage): EventEnvelope;
|
|
266
|
+
/**
|
|
267
|
+
* Creates a decoder for records whose value is plain JSON, as written by a
|
|
268
|
+
* service that does not use relaybus (a plain kafkajs producer, Spring Kafka's
|
|
269
|
+
* `JsonSerializer`, a Go service): the value becomes the payload, the record
|
|
270
|
+
* headers become event headers, and the options say where the event type and
|
|
271
|
+
* id come from.
|
|
272
|
+
*
|
|
273
|
+
* A record that is a CloudEvent anyway (a `ce_specversion` header, or a
|
|
274
|
+
* structured `application/cloudevents+json` value) is decoded with
|
|
275
|
+
* {@link decodeCloudEvent} instead, so relaybus and foreign producers can share
|
|
276
|
+
* a topic. A missing or non-JSON value, a missing or invalid type, and an
|
|
277
|
+
* extractor returning anything but a string are invalid.
|
|
278
|
+
*
|
|
279
|
+
* ```ts
|
|
280
|
+
* new KafkaTransport({
|
|
281
|
+
* kafka,
|
|
282
|
+
* groupId: 'billing',
|
|
283
|
+
* decode: plainJsonDecoder({ type: fromHeader('event-type'), id: fromHeader('message-id') }),
|
|
284
|
+
* });
|
|
285
|
+
* ```
|
|
286
|
+
*
|
|
287
|
+
* @throws ConfigurationError when a fixed `type` is not a valid event type.
|
|
288
|
+
*/
|
|
289
|
+
declare function plainJsonDecoder(options: PlainJsonDecoderOptions): KafkaMessageDecoder;
|
|
290
|
+
|
|
291
|
+
/** The record a codec produces for one event. `timestamp` is epoch milliseconds in a string. */
|
|
292
|
+
type KafkaEncodedRecord = KafkaOutgoingMessage & {
|
|
293
|
+
readonly timestamp?: string;
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* The wire format of a {@link KafkaTransport}: how an event becomes a record
|
|
297
|
+
* (`encode`, on publish) and a consumed record becomes an event (`decode`).
|
|
298
|
+
*
|
|
299
|
+
* Both must be synchronous and free of side effects. What `encode` throws
|
|
300
|
+
* fails the publish: the outbox retries it, unless it is a `NonRetryableError`
|
|
301
|
+
* such as {@link UnencodableEventError}. What `decode` throws marks the record
|
|
302
|
+
* as malformed: it goes to `onInvalidMessage` (dead-letter by default) and the
|
|
303
|
+
* consumer moves on. See {@link KafkaMessageDecoder} for the decode contract.
|
|
304
|
+
*/
|
|
305
|
+
interface KafkaCodec {
|
|
306
|
+
readonly encode: (envelope: EventEnvelope) => KafkaEncodedRecord;
|
|
307
|
+
readonly decode: KafkaMessageDecoder;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* CloudEvents 1.0 binary content mode, the default: the value is the JSON
|
|
311
|
+
* payload, the attributes travel as `ce_*` headers ({@link encodeEnvelope}).
|
|
312
|
+
* Decoding is strict: any other record is malformed ({@link decodeEnvelope}).
|
|
313
|
+
*/
|
|
314
|
+
declare const cloudEventsBinary: KafkaCodec;
|
|
315
|
+
/**
|
|
316
|
+
* CloudEvents 1.0 structured content mode: the value is one
|
|
317
|
+
* `application/cloudevents+json` document with the attributes and `data`.
|
|
318
|
+
* Decoding accepts structured and binary mode ({@link decodeCloudEvent}), so a
|
|
319
|
+
* topic can move from one to the other while both are in flight.
|
|
320
|
+
*/
|
|
321
|
+
declare const cloudEventsStructured: KafkaCodec;
|
|
322
|
+
/**
|
|
323
|
+
* Where each value of a JSON message lives, for {@link jsonEnvelope}. Besides
|
|
324
|
+
* `{ body }` and `{ header }`, the id may be `'record-position'`: a consumed
|
|
325
|
+
* record gets `kafka:<topic>:<partition>:<offset>`, stable across redeliveries
|
|
326
|
+
* but different for two sends of one event. Records relaybus writes carry the
|
|
327
|
+
* event id in a `relaybus-event-id` header, which wins on read, so an outbox
|
|
328
|
+
* retry that sends an event twice is still deduplicated.
|
|
329
|
+
*/
|
|
330
|
+
type KafkaJsonEnvelopeMapping = JsonEnvelopeMapping<'record-position'>;
|
|
331
|
+
/**
|
|
332
|
+
* A JSON wire format declared by a mapping, in both directions: for topics
|
|
333
|
+
* shared with services that wrap events in their own envelope
|
|
334
|
+
* (`{ "eventName": ..., "data": {...} }`, MassTransit) or send the bare
|
|
335
|
+
* payload. The record key is always the event key; when `occurredAt` is not
|
|
336
|
+
* mapped, the record timestamp carries it. Event headers that are message
|
|
337
|
+
* headers are written as they are, so a name the mapping uses for another
|
|
338
|
+
* field is refused with {@link UnencodableEventError}, as are `content-type`
|
|
339
|
+
* and `relaybus-event-id`.
|
|
340
|
+
*
|
|
341
|
+
* ```ts
|
|
342
|
+
* codec: jsonEnvelope({
|
|
343
|
+
* type: { body: 'eventName' },
|
|
344
|
+
* id: { body: 'messageId' },
|
|
345
|
+
* payload: { body: 'data' },
|
|
346
|
+
* correlationId: { body: 'meta.correlationId' },
|
|
347
|
+
* })
|
|
348
|
+
* ```
|
|
349
|
+
*
|
|
350
|
+
* @throws ConfigurationError for an incomplete or self-overlapping mapping.
|
|
351
|
+
*/
|
|
352
|
+
declare function jsonEnvelope(mapping: KafkaJsonEnvelopeMapping): KafkaCodec;
|
|
353
|
+
|
|
146
354
|
/** A record the transport refused to hand to the bus because it is malformed. */
|
|
147
355
|
interface InvalidKafkaMessage {
|
|
148
356
|
readonly topic: string;
|
|
@@ -172,14 +380,6 @@ interface InvalidKafkaMessage {
|
|
|
172
380
|
type InvalidMessagePolicy = 'dead-letter' | 'skip' | ((message: InvalidKafkaMessage) => void | Promise<void>) | {
|
|
173
381
|
readonly deadLetterTopic: string | ((sourceTopic: string) => string);
|
|
174
382
|
};
|
|
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
383
|
/** Bounded in-process retries around the inbound handler (the bus's `receive`). */
|
|
184
384
|
interface KafkaProcessingOptions {
|
|
185
385
|
/** Attempts per delivery of a record before the error is handed to the client. Default 5. */
|
|
@@ -224,12 +424,33 @@ interface KafkaTransportOptions {
|
|
|
224
424
|
readonly restart?: KafkaRestartOptions;
|
|
225
425
|
/** Handling of records that cannot be processed for good. Default `'dead-letter'`. */
|
|
226
426
|
readonly onInvalidMessage?: InvalidMessagePolicy;
|
|
427
|
+
/**
|
|
428
|
+
* The wire format, both ways: how events become records and consumed records
|
|
429
|
+
* become events. Default {@link cloudEventsBinary}: CloudEvents binary mode,
|
|
430
|
+
* strict on read, so any other record is malformed and goes to
|
|
431
|
+
* `onInvalidMessage`. {@link cloudEventsStructured} writes structured mode
|
|
432
|
+
* and reads both, {@link jsonEnvelope} maps a JSON format of your own (an
|
|
433
|
+
* envelope with the payload inside, or the bare payload), and an object with
|
|
434
|
+
* `encode` and `decode` can do anything else. Whatever a non-default codec's
|
|
435
|
+
* `decode` throws marks the record as malformed, never crashes the consumer.
|
|
436
|
+
*/
|
|
437
|
+
readonly codec?: KafkaCodec;
|
|
438
|
+
/**
|
|
439
|
+
* How consumed records become events; events are still written in
|
|
440
|
+
* CloudEvents binary mode.
|
|
441
|
+
*
|
|
442
|
+
* @deprecated Use `codec`: `decode: fn` is `codec: { encode: encodeEnvelope, decode: fn }`.
|
|
443
|
+
* Giving both `decode` and `codec` is a configuration error.
|
|
444
|
+
*/
|
|
445
|
+
readonly decode?: KafkaMessageDecoder;
|
|
227
446
|
/** Default `console`. */
|
|
228
447
|
readonly logger?: RelayBusLogger;
|
|
229
448
|
}
|
|
230
449
|
/**
|
|
231
450
|
* Kafka {@link Transport}. Events travel as CloudEvents binary-mode records
|
|
232
451
|
* (`ce_*` headers, JSON value), so any CloudEvents-aware consumer can read them.
|
|
452
|
+
* The `codec` option changes the wire format, for example to read and write
|
|
453
|
+
* the JSON format of services that do not use relaybus.
|
|
233
454
|
*
|
|
234
455
|
* Publishing: idempotent producer, `acks=-1`, resolves after the broker ack and
|
|
235
456
|
* rejects with {@link PublishTimeoutError} after `publishTimeout`. A timed-out
|
|
@@ -251,6 +472,7 @@ interface KafkaTransportOptions {
|
|
|
251
472
|
declare class KafkaTransport implements Transport {
|
|
252
473
|
#private;
|
|
253
474
|
readonly name: string;
|
|
475
|
+
readonly system = "kafka";
|
|
254
476
|
constructor(options: KafkaTransportOptions);
|
|
255
477
|
/**
|
|
256
478
|
* True while the consumer is joined to its group, not between a crash or
|
|
@@ -275,9 +497,9 @@ declare class KafkaTransport implements Transport {
|
|
|
275
497
|
* @throws ConfigurationError when `groupId` is missing, `destinations` is empty
|
|
276
498
|
* or the transport already subscribed (create one transport per subscription).
|
|
277
499
|
*/
|
|
278
|
-
subscribe(destinations: readonly string[], onMessage: TransportMessageHandler): Promise<void>;
|
|
500
|
+
subscribe(destinations: readonly string[], onMessage: TransportMessageHandler, hooks?: TransportSubscriptionHooks): Promise<void>;
|
|
279
501
|
/** Stops consuming and disconnects the consumer and the producer. Safe to call twice. */
|
|
280
502
|
close(): Promise<void>;
|
|
281
503
|
}
|
|
282
504
|
|
|
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 };
|
|
505
|
+
export { type DecodableMessage, type DecodedEvent, type EncodedMessage, GroupJoinTimeoutError, HandlerFailedError, type InvalidKafkaMessage, InvalidKafkaMessageError, type InvalidMessagePolicy, type KafkaBytes, type KafkaClientLike, type KafkaCodec, type KafkaConsumerConfig, type KafkaConsumerEvent, type KafkaConsumerEventNames, type KafkaConsumerLike, type KafkaEachMessagePayload, type KafkaEncodedRecord, type KafkaInboundRecord, type KafkaIncomingHeaders, type KafkaIncomingMessage, type KafkaJsonEnvelopeMapping, type KafkaMessageDecoder, type KafkaOutgoingHeaders, type KafkaOutgoingMessage, type KafkaProcessingOptions, type KafkaProducerConfig, type KafkaProducerLike, type KafkaProducerRecord, type KafkaRestartOptions, type KafkaRunConfig, KafkaTransport, type KafkaTransportOptions, type PlainJsonDecoderOptions, type PlainJsonRecord, PublishTimeoutError, TransportClosedError, UnencodableEventError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, decodeEnvelope, encodeEnvelope, jsonEnvelope, plainJsonDecoder };
|