@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/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 };