@relaybus/rabbitmq 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,5 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import { NonRetryableError, RelayBusError, RetryPolicyOptions, Duration, RelayBusLogger, Transport, EventEnvelope, TransportMessageHandler } from '@relaybus/core';
2
+ import { NonRetryableError, RelayBusError, EventHeaders, EventEnvelope, RetryPolicyOptions, Duration, RelayBusLogger, Transport, TransportMessageHandler, TransportSubscriptionHooks, CloseOptions } from '@relaybus/core';
3
+ import { JsonEnvelopeMapping } from '@relaybus/core/internal';
4
+ export { fromHeader, fromPayloadField } from '@relaybus/core/internal';
3
5
 
4
6
  /**
5
7
  * The slice of amqplib this package relies on. Declaring it here keeps the
@@ -74,18 +76,26 @@ interface AmqpConnection extends EventEmitter {
74
76
  * Always retryable; the outbox keeps the delivery and tries again.
75
77
  */
76
78
  declare class RabbitMqUnavailableError extends RelayBusError {
79
+ name: string;
80
+ readonly code: string;
77
81
  }
78
82
  /** The broker negatively acknowledged a published message. Retryable. */
79
83
  declare class RabbitMqPublishRejectedError extends RelayBusError {
84
+ name: string;
85
+ readonly code: string;
80
86
  }
81
87
  /**
82
88
  * The broker accepted the message but no queue is bound to its routing key, so
83
89
  * it would have been discarded. Retryable: it succeeds once a consumer binds.
84
90
  */
85
91
  declare class UnroutableMessageError extends RelayBusError {
92
+ name: string;
93
+ readonly code: string;
86
94
  }
87
95
  /** A received message is not a valid relaybus CloudEvent. It is dead-lettered. */
88
96
  declare class MalformedMessageError extends RelayBusError {
97
+ name: string;
98
+ readonly code: string;
89
99
  }
90
100
  /**
91
101
  * The payload of an event cannot be represented as JSON without losing or
@@ -94,6 +104,7 @@ declare class MalformedMessageError extends RelayBusError {
94
104
  */
95
105
  declare class UnserializablePayloadError extends NonRetryableError {
96
106
  name: string;
107
+ readonly code: string;
97
108
  }
98
109
  /**
99
110
  * A value exceeds a protocol limit of AMQP 0-9-1, for example an event id or a
@@ -101,8 +112,191 @@ declare class UnserializablePayloadError extends NonRetryableError {
101
112
  */
102
113
  declare class AmqpLimitExceededError extends NonRetryableError {
103
114
  name: string;
115
+ readonly code: string;
116
+ }
117
+ /**
118
+ * An event the configured codec cannot write: a header name the format
119
+ * reserves, or a type a fixed-type route does not carry. Never retried.
120
+ */
121
+ declare class RabbitMqUnencodableEventError extends NonRetryableError {
122
+ name: string;
123
+ readonly code: string;
104
124
  }
105
125
 
126
+ /**
127
+ * The event a decoder extracted from a message. `id`, `type` and `payload` are
128
+ * required; every other field may be left out and is then filled by the
129
+ * transport: `source` with the `appId` property (else the routing key), `key`
130
+ * with null, `headers` with none, `correlationId` and `causationId` with null,
131
+ * and `occurredAt` with the `timestamp` property (else the time of receipt).
132
+ */
133
+ interface DecodedEvent {
134
+ readonly id: string;
135
+ readonly type: string;
136
+ readonly payload: unknown;
137
+ readonly source?: string;
138
+ readonly key?: string | null;
139
+ readonly headers?: EventHeaders;
140
+ readonly correlationId?: string | null;
141
+ readonly causationId?: string | null;
142
+ readonly occurredAt?: Date;
143
+ }
144
+ /**
145
+ * Turns a consumed message into an event, for queues fed by producers that do
146
+ * not use relaybus. Must be synchronous and free of side effects: it runs again
147
+ * on every redelivery, so the same message must always give the same event,
148
+ * above all the same `id`, which is what the inbox deduplicates on.
149
+ *
150
+ * Anything it throws marks the message as malformed: it is dead-lettered and
151
+ * the consumer moves on. An event the bus then rejects (empty id, invalid type
152
+ * or time) is dead-lettered the same way.
153
+ */
154
+ type RabbitMqMessageDecoder = (message: AmqpMessage) => DecodedEvent;
155
+ /** A plain JSON message, decoded for the extractor options of {@link plainJsonDecoder}. */
156
+ interface PlainJsonMessage {
157
+ readonly routingKey: string;
158
+ /** The AMQP properties as delivered (`messageId`, `type`, `appId`, `timestamp`, ...). */
159
+ readonly properties: AmqpMessageProperties;
160
+ /**
161
+ * The message headers with string values, without the broker's and the
162
+ * transport's own (`x-death`, `x-first-death-*`, `x-last-death-*`, `x-relaybus-*`).
163
+ */
164
+ readonly headers: EventHeaders;
165
+ /** The body parsed as JSON. */
166
+ readonly payload: unknown;
167
+ }
168
+ /** Options of {@link plainJsonDecoder}. Extractors returning undefined or `''` select the default. */
169
+ interface PlainJsonDecoderOptions {
170
+ /**
171
+ * Event type handlers subscribe to: a fixed name for a queue that carries one
172
+ * kind of event, or an extractor such as `fromHeader('event-type')`,
173
+ * `fromPayloadField('type')` or `(message) => message.properties.type`.
174
+ * Required; a message without a type is malformed.
175
+ */
176
+ readonly type: string | ((message: PlainJsonMessage) => string | undefined);
177
+ /**
178
+ * Event id the inbox deduplicates on. Default, and fallback when the
179
+ * extractor finds none: the `messageId` property, which survives
180
+ * redeliveries. A message with neither is malformed, because AMQP has no
181
+ * other identity that is stable across redeliveries; give producers a
182
+ * `messageId` (Spring AMQP can generate one with `setCreateMessageIds(true)`).
183
+ */
184
+ readonly id?: (message: PlainJsonMessage) => string | undefined;
185
+ /** Ordering key. Default: none. */
186
+ readonly key?: (message: PlainJsonMessage) => string | null | undefined;
187
+ /** Logical name of the producer. Default: the `appId` property, else the routing key. */
188
+ readonly source?: string | ((message: PlainJsonMessage) => string | undefined);
189
+ /** When the event happened. Default: the `timestamp` property, else the time of receipt. */
190
+ readonly occurredAt?: (message: PlainJsonMessage) => Date | undefined;
191
+ }
192
+ /**
193
+ * Decodes a CloudEvents 1.0 message in either content mode:
194
+ * - structured (`contentType` property `application/cloudevents+json`): the
195
+ * body is a JSON object with `specversion`, `id`, `type`, `source` and
196
+ * optionally `time` (else the `timestamp` property, else the time of
197
+ * receipt), `subject` (the key), `data` (the payload, null when absent) and
198
+ * a JSON `datacontenttype`. `data_base64` is refused, because relaybus
199
+ * payloads are JSON. Extension attributes with string values (`traceparent`,
200
+ * ...) become event headers, as do the string message headers;
201
+ * `correlationid` and `causationid` fill their envelope fields.
202
+ * - binary (anything else): exactly the default decoder, `cloudEvents:*`
203
+ * headers with the AMQP properties as fallback.
204
+ *
205
+ * @throws MalformedMessageError for a message that is neither.
206
+ */
207
+ declare function decodeCloudEvent(message: AmqpMessage): EventEnvelope;
208
+ /**
209
+ * Creates a decoder for messages whose body is plain JSON, as published by a
210
+ * service that does not use relaybus (plain amqplib, Spring AMQP's
211
+ * `Jackson2JsonMessageConverter`, a Go or Python service): the body becomes the
212
+ * payload, the string headers become event headers, and the options say where
213
+ * the event type and id come from.
214
+ *
215
+ * A message that is a CloudEvent anyway (a `cloudEvents:specversion` header,
216
+ * or a structured `application/cloudevents+json` body) is decoded with
217
+ * {@link decodeCloudEvent} instead, so relaybus and foreign producers can feed
218
+ * one queue. A body that is not JSON, a missing or invalid type, a missing id
219
+ * and an extractor returning anything but a string are malformed.
220
+ *
221
+ * ```ts
222
+ * new RabbitMqTransport({
223
+ * url,
224
+ * consumerGroup: 'billing',
225
+ * decode: plainJsonDecoder({ type: (message) => message.properties.type as string }),
226
+ * });
227
+ * ```
228
+ *
229
+ * @throws ConfigurationError when a fixed `type` is not a valid event type.
230
+ */
231
+ declare function plainJsonDecoder(options: PlainJsonDecoderOptions): RabbitMqMessageDecoder;
232
+
233
+ /** A serialized event, ready for `channel.publish`. */
234
+ interface EncodedMessage {
235
+ readonly content: Buffer;
236
+ readonly options: AmqpPublishOptions;
237
+ }
238
+
239
+ /**
240
+ * The wire format of a {@link RabbitMqTransport}: how an event becomes a
241
+ * message (`encode`, on publish) and a consumed message becomes an event
242
+ * (`decode`).
243
+ *
244
+ * Both must be synchronous and free of side effects. What `encode` throws
245
+ * fails the publish: the outbox retries it, unless it is a `NonRetryableError`
246
+ * such as {@link RabbitMqUnencodableEventError}. Give every message a
247
+ * `messageId`: the transport matches broker returns of unroutable messages by
248
+ * it. What `decode` throws marks the message as malformed: it is
249
+ * dead-lettered and the consumer moves on. See {@link RabbitMqMessageDecoder}
250
+ * for the decode contract.
251
+ */
252
+ interface RabbitMqCodec {
253
+ readonly encode: (envelope: EventEnvelope) => EncodedMessage;
254
+ readonly decode: RabbitMqMessageDecoder;
255
+ }
256
+ /**
257
+ * CloudEvents 1.0 binary content mode, the default: the body is the JSON
258
+ * payload, the attributes travel as `cloudEvents:*` headers mirrored in the
259
+ * AMQP properties. Decoding is strict: any other message is malformed.
260
+ */
261
+ declare const cloudEventsBinary: RabbitMqCodec;
262
+ /**
263
+ * CloudEvents 1.0 structured content mode: the body is one
264
+ * `application/cloudevents+json` document with the attributes and `data`.
265
+ * Decoding accepts structured and binary mode ({@link decodeCloudEvent}).
266
+ * Event headers travel under their own names; names starting with `x-death`,
267
+ * `x-first-death-`, `x-last-death-` or `x-relaybus-` are refused.
268
+ */
269
+ declare const cloudEventsStructured: RabbitMqCodec;
270
+ /**
271
+ * Where each value of a JSON message lives, for {@link jsonEnvelope}. Besides
272
+ * `{ body }` and `{ header }`, the id may be `'message-id-property'`: the
273
+ * AMQP `messageId` property, which the codec always writes and which survives
274
+ * redeliveries; a message without one is malformed.
275
+ */
276
+ type RabbitMqJsonEnvelopeMapping = JsonEnvelopeMapping<'message-id-property'>;
277
+ /**
278
+ * A JSON wire format declared by a mapping, in both directions: for queues
279
+ * shared with services that wrap events in their own envelope
280
+ * (`{ "eventName": ..., "data": {...} }`, MassTransit) or send the bare
281
+ * payload. Every message also gets the AMQP properties of binary mode:
282
+ * `messageId` (the event id), `type`, `appId` (the source), `correlationId`,
283
+ * `timestamp` (seconds) and `persistent`. Event
284
+ * headers that are message headers are written as they are, so a name the
285
+ * mapping uses for another field, or one the broker reserves, is refused
286
+ * with {@link RabbitMqUnencodableEventError}.
287
+ *
288
+ * ```ts
289
+ * codec: jsonEnvelope({
290
+ * type: { body: 'eventName' },
291
+ * id: 'message-id-property',
292
+ * payload: { body: 'data' },
293
+ * })
294
+ * ```
295
+ *
296
+ * @throws ConfigurationError for an incomplete or self-overlapping mapping.
297
+ */
298
+ declare function jsonEnvelope(mapping: RabbitMqJsonEnvelopeMapping): RabbitMqCodec;
299
+
106
300
  /** Connection settings accepted by `amqplib.connect`, given as an object instead of a URL. */
107
301
  interface BrokerConnectionSettings {
108
302
  readonly protocol?: string;
@@ -139,9 +333,11 @@ interface RabbitMqTransportOptions {
139
333
  readonly url?: string | BrokerConnectionSettings;
140
334
  /**
141
335
  * Opens one connection. Called again after every connection loss. Use it for
142
- * TLS options, custom credentials or an alternative amqplib instance.
336
+ * TLS options, custom credentials or an alternative amqplib instance. `signal` aborts when a
337
+ * shutdown runs past its deadline: pass it on (`amqplib.connect(url, { signal })`) so a broker
338
+ * that never answers the handshake cannot hold the socket open.
143
339
  */
144
- readonly connect?: () => Promise<AmqpConnection>;
340
+ readonly connect?: (options: ConnectOptions) => Promise<AmqpConnection>;
145
341
  /** Topic exchange events are published to. Durable, created when missing. Default `'relaybus'`. */
146
342
  readonly exchange?: string;
147
343
  /**
@@ -209,11 +405,36 @@ interface RabbitMqTransportOptions {
209
405
  readonly publishTimeout?: Duration;
210
406
  /** How long `close` waits for running handlers. Default `'30s'`. */
211
407
  readonly shutdownTimeout?: Duration;
408
+ /**
409
+ * The wire format, both ways: how events become messages and consumed
410
+ * messages become events. Default {@link cloudEventsBinary}: CloudEvents
411
+ * binary mode (`cloudEvents:*` headers, AMQP properties as fallback), any
412
+ * other message is malformed and dead-lettered. {@link cloudEventsStructured}
413
+ * writes structured mode and reads both, {@link jsonEnvelope} maps a JSON
414
+ * format of your own (an envelope with the payload inside, or the bare
415
+ * payload), and an object with `encode` and `decode` can do anything else.
416
+ * Whatever `decode` throws marks the message as malformed: it is
417
+ * dead-lettered, the consumer keeps running.
418
+ */
419
+ readonly codec?: RabbitMqCodec;
420
+ /**
421
+ * How consumed messages become events; events are still written in
422
+ * CloudEvents binary mode.
423
+ *
424
+ * @deprecated Use `codec`: `decode: fn` is `codec: { encode: encodeEnvelope, decode: fn }`.
425
+ * Giving both `decode` and `codec` is a configuration error.
426
+ */
427
+ readonly decode?: RabbitMqMessageDecoder;
212
428
  /** Default: `console`. */
213
429
  readonly logger?: RelayBusLogger;
214
430
  /** Random source for jitter. Default `Math.random`. Intended for tests. */
215
431
  readonly random?: () => number;
216
432
  }
433
+ /** What `connect` gets for each connection it opens. */
434
+ interface ConnectOptions {
435
+ /** Aborted when a shutdown runs past its deadline; destroys a socket still connecting. */
436
+ readonly signal: AbortSignal;
437
+ }
217
438
 
218
439
  /**
219
440
  * RabbitMQ {@link Transport} built on amqplib.
@@ -240,6 +461,7 @@ interface RabbitMqTransportOptions {
240
461
  declare class RabbitMqTransport implements Transport {
241
462
  #private;
242
463
  readonly name: string;
464
+ readonly system = "rabbitmq";
243
465
  constructor(options: RabbitMqTransportOptions);
244
466
  /**
245
467
  * True while the connection is open and publishing and consuming are set up.
@@ -262,6 +484,7 @@ declare class RabbitMqTransport implements Transport {
262
484
  * @throws {RabbitMqUnavailableError} not connected, channel lost, or confirm timed out
263
485
  * @throws {RabbitMqPublishRejectedError} the broker nacked the message
264
486
  * @throws {UnroutableMessageError} no queue is bound to `destination` (unless `allowUnroutable`)
487
+ * @throws whatever the `codec`'s `encode` throws, such as {@link RabbitMqUnencodableEventError}
265
488
  */
266
489
  publish(envelope: EventEnvelope, destination: string): Promise<void>;
267
490
  /**
@@ -269,13 +492,14 @@ declare class RabbitMqTransport implements Transport {
269
492
  * to `onMessage`. Call it once per transport; give each subscription its own
270
493
  * transport instance, with its own `name` and `consumerGroup`.
271
494
  */
272
- subscribe(destinations: readonly string[], onMessage: TransportMessageHandler): Promise<void>;
495
+ subscribe(destinations: readonly string[], onMessage: TransportMessageHandler, hooks?: TransportSubscriptionHooks): Promise<void>;
273
496
  /**
274
497
  * Stops consuming, waits up to `shutdownTimeout` for running handlers
275
498
  * (messages still being retried go back to the queue), then closes the
276
- * channels and the connection.
499
+ * channels and the connection. Once `options.signal` aborts (the bus shutdown deadline passed),
500
+ * the connection's socket is destroyed instead of waiting for the broker.
277
501
  */
278
- close(): Promise<void>;
502
+ close(options?: CloseOptions): Promise<void>;
279
503
  }
280
504
 
281
- export { type AmqpChannel, type AmqpConfirmChannel, type AmqpConnection, AmqpLimitExceededError, type AmqpMessage, type AmqpMessageProperties, type AmqpPublishOptions, type BrokerConnectionSettings, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, type RabbitMqTransportOptions, RabbitMqUnavailableError, type ReconnectOptions, type RedeliveryOptions, UnroutableMessageError, UnserializablePayloadError };
505
+ export { type AmqpChannel, type AmqpConfirmChannel, type AmqpConnection, AmqpLimitExceededError, type AmqpMessage, type AmqpMessageProperties, type AmqpPublishOptions, type BrokerConnectionSettings, type DecodedEvent, type EncodedMessage, MalformedMessageError, type PlainJsonDecoderOptions, type PlainJsonMessage, type RabbitMqCodec, type RabbitMqJsonEnvelopeMapping, type RabbitMqMessageDecoder, RabbitMqPublishRejectedError, RabbitMqTransport, type RabbitMqTransportOptions, RabbitMqUnavailableError, RabbitMqUnencodableEventError, type ReconnectOptions, type RedeliveryOptions, UnroutableMessageError, UnserializablePayloadError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, jsonEnvelope, plainJsonDecoder };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import { NonRetryableError, RelayBusError, RetryPolicyOptions, Duration, RelayBusLogger, Transport, EventEnvelope, TransportMessageHandler } from '@relaybus/core';
2
+ import { NonRetryableError, RelayBusError, EventHeaders, EventEnvelope, RetryPolicyOptions, Duration, RelayBusLogger, Transport, TransportMessageHandler, TransportSubscriptionHooks, CloseOptions } from '@relaybus/core';
3
+ import { JsonEnvelopeMapping } from '@relaybus/core/internal';
4
+ export { fromHeader, fromPayloadField } from '@relaybus/core/internal';
3
5
 
4
6
  /**
5
7
  * The slice of amqplib this package relies on. Declaring it here keeps the
@@ -74,18 +76,26 @@ interface AmqpConnection extends EventEmitter {
74
76
  * Always retryable; the outbox keeps the delivery and tries again.
75
77
  */
76
78
  declare class RabbitMqUnavailableError extends RelayBusError {
79
+ name: string;
80
+ readonly code: string;
77
81
  }
78
82
  /** The broker negatively acknowledged a published message. Retryable. */
79
83
  declare class RabbitMqPublishRejectedError extends RelayBusError {
84
+ name: string;
85
+ readonly code: string;
80
86
  }
81
87
  /**
82
88
  * The broker accepted the message but no queue is bound to its routing key, so
83
89
  * it would have been discarded. Retryable: it succeeds once a consumer binds.
84
90
  */
85
91
  declare class UnroutableMessageError extends RelayBusError {
92
+ name: string;
93
+ readonly code: string;
86
94
  }
87
95
  /** A received message is not a valid relaybus CloudEvent. It is dead-lettered. */
88
96
  declare class MalformedMessageError extends RelayBusError {
97
+ name: string;
98
+ readonly code: string;
89
99
  }
90
100
  /**
91
101
  * The payload of an event cannot be represented as JSON without losing or
@@ -94,6 +104,7 @@ declare class MalformedMessageError extends RelayBusError {
94
104
  */
95
105
  declare class UnserializablePayloadError extends NonRetryableError {
96
106
  name: string;
107
+ readonly code: string;
97
108
  }
98
109
  /**
99
110
  * A value exceeds a protocol limit of AMQP 0-9-1, for example an event id or a
@@ -101,8 +112,191 @@ declare class UnserializablePayloadError extends NonRetryableError {
101
112
  */
102
113
  declare class AmqpLimitExceededError extends NonRetryableError {
103
114
  name: string;
115
+ readonly code: string;
116
+ }
117
+ /**
118
+ * An event the configured codec cannot write: a header name the format
119
+ * reserves, or a type a fixed-type route does not carry. Never retried.
120
+ */
121
+ declare class RabbitMqUnencodableEventError extends NonRetryableError {
122
+ name: string;
123
+ readonly code: string;
104
124
  }
105
125
 
126
+ /**
127
+ * The event a decoder extracted from a message. `id`, `type` and `payload` are
128
+ * required; every other field may be left out and is then filled by the
129
+ * transport: `source` with the `appId` property (else the routing key), `key`
130
+ * with null, `headers` with none, `correlationId` and `causationId` with null,
131
+ * and `occurredAt` with the `timestamp` property (else the time of receipt).
132
+ */
133
+ interface DecodedEvent {
134
+ readonly id: string;
135
+ readonly type: string;
136
+ readonly payload: unknown;
137
+ readonly source?: string;
138
+ readonly key?: string | null;
139
+ readonly headers?: EventHeaders;
140
+ readonly correlationId?: string | null;
141
+ readonly causationId?: string | null;
142
+ readonly occurredAt?: Date;
143
+ }
144
+ /**
145
+ * Turns a consumed message into an event, for queues fed by producers that do
146
+ * not use relaybus. Must be synchronous and free of side effects: it runs again
147
+ * on every redelivery, so the same message must always give the same event,
148
+ * above all the same `id`, which is what the inbox deduplicates on.
149
+ *
150
+ * Anything it throws marks the message as malformed: it is dead-lettered and
151
+ * the consumer moves on. An event the bus then rejects (empty id, invalid type
152
+ * or time) is dead-lettered the same way.
153
+ */
154
+ type RabbitMqMessageDecoder = (message: AmqpMessage) => DecodedEvent;
155
+ /** A plain JSON message, decoded for the extractor options of {@link plainJsonDecoder}. */
156
+ interface PlainJsonMessage {
157
+ readonly routingKey: string;
158
+ /** The AMQP properties as delivered (`messageId`, `type`, `appId`, `timestamp`, ...). */
159
+ readonly properties: AmqpMessageProperties;
160
+ /**
161
+ * The message headers with string values, without the broker's and the
162
+ * transport's own (`x-death`, `x-first-death-*`, `x-last-death-*`, `x-relaybus-*`).
163
+ */
164
+ readonly headers: EventHeaders;
165
+ /** The body parsed as JSON. */
166
+ readonly payload: unknown;
167
+ }
168
+ /** Options of {@link plainJsonDecoder}. Extractors returning undefined or `''` select the default. */
169
+ interface PlainJsonDecoderOptions {
170
+ /**
171
+ * Event type handlers subscribe to: a fixed name for a queue that carries one
172
+ * kind of event, or an extractor such as `fromHeader('event-type')`,
173
+ * `fromPayloadField('type')` or `(message) => message.properties.type`.
174
+ * Required; a message without a type is malformed.
175
+ */
176
+ readonly type: string | ((message: PlainJsonMessage) => string | undefined);
177
+ /**
178
+ * Event id the inbox deduplicates on. Default, and fallback when the
179
+ * extractor finds none: the `messageId` property, which survives
180
+ * redeliveries. A message with neither is malformed, because AMQP has no
181
+ * other identity that is stable across redeliveries; give producers a
182
+ * `messageId` (Spring AMQP can generate one with `setCreateMessageIds(true)`).
183
+ */
184
+ readonly id?: (message: PlainJsonMessage) => string | undefined;
185
+ /** Ordering key. Default: none. */
186
+ readonly key?: (message: PlainJsonMessage) => string | null | undefined;
187
+ /** Logical name of the producer. Default: the `appId` property, else the routing key. */
188
+ readonly source?: string | ((message: PlainJsonMessage) => string | undefined);
189
+ /** When the event happened. Default: the `timestamp` property, else the time of receipt. */
190
+ readonly occurredAt?: (message: PlainJsonMessage) => Date | undefined;
191
+ }
192
+ /**
193
+ * Decodes a CloudEvents 1.0 message in either content mode:
194
+ * - structured (`contentType` property `application/cloudevents+json`): the
195
+ * body is a JSON object with `specversion`, `id`, `type`, `source` and
196
+ * optionally `time` (else the `timestamp` property, else the time of
197
+ * receipt), `subject` (the key), `data` (the payload, null when absent) and
198
+ * a JSON `datacontenttype`. `data_base64` is refused, because relaybus
199
+ * payloads are JSON. Extension attributes with string values (`traceparent`,
200
+ * ...) become event headers, as do the string message headers;
201
+ * `correlationid` and `causationid` fill their envelope fields.
202
+ * - binary (anything else): exactly the default decoder, `cloudEvents:*`
203
+ * headers with the AMQP properties as fallback.
204
+ *
205
+ * @throws MalformedMessageError for a message that is neither.
206
+ */
207
+ declare function decodeCloudEvent(message: AmqpMessage): EventEnvelope;
208
+ /**
209
+ * Creates a decoder for messages whose body is plain JSON, as published by a
210
+ * service that does not use relaybus (plain amqplib, Spring AMQP's
211
+ * `Jackson2JsonMessageConverter`, a Go or Python service): the body becomes the
212
+ * payload, the string headers become event headers, and the options say where
213
+ * the event type and id come from.
214
+ *
215
+ * A message that is a CloudEvent anyway (a `cloudEvents:specversion` header,
216
+ * or a structured `application/cloudevents+json` body) is decoded with
217
+ * {@link decodeCloudEvent} instead, so relaybus and foreign producers can feed
218
+ * one queue. A body that is not JSON, a missing or invalid type, a missing id
219
+ * and an extractor returning anything but a string are malformed.
220
+ *
221
+ * ```ts
222
+ * new RabbitMqTransport({
223
+ * url,
224
+ * consumerGroup: 'billing',
225
+ * decode: plainJsonDecoder({ type: (message) => message.properties.type as string }),
226
+ * });
227
+ * ```
228
+ *
229
+ * @throws ConfigurationError when a fixed `type` is not a valid event type.
230
+ */
231
+ declare function plainJsonDecoder(options: PlainJsonDecoderOptions): RabbitMqMessageDecoder;
232
+
233
+ /** A serialized event, ready for `channel.publish`. */
234
+ interface EncodedMessage {
235
+ readonly content: Buffer;
236
+ readonly options: AmqpPublishOptions;
237
+ }
238
+
239
+ /**
240
+ * The wire format of a {@link RabbitMqTransport}: how an event becomes a
241
+ * message (`encode`, on publish) and a consumed message becomes an event
242
+ * (`decode`).
243
+ *
244
+ * Both must be synchronous and free of side effects. What `encode` throws
245
+ * fails the publish: the outbox retries it, unless it is a `NonRetryableError`
246
+ * such as {@link RabbitMqUnencodableEventError}. Give every message a
247
+ * `messageId`: the transport matches broker returns of unroutable messages by
248
+ * it. What `decode` throws marks the message as malformed: it is
249
+ * dead-lettered and the consumer moves on. See {@link RabbitMqMessageDecoder}
250
+ * for the decode contract.
251
+ */
252
+ interface RabbitMqCodec {
253
+ readonly encode: (envelope: EventEnvelope) => EncodedMessage;
254
+ readonly decode: RabbitMqMessageDecoder;
255
+ }
256
+ /**
257
+ * CloudEvents 1.0 binary content mode, the default: the body is the JSON
258
+ * payload, the attributes travel as `cloudEvents:*` headers mirrored in the
259
+ * AMQP properties. Decoding is strict: any other message is malformed.
260
+ */
261
+ declare const cloudEventsBinary: RabbitMqCodec;
262
+ /**
263
+ * CloudEvents 1.0 structured content mode: the body is one
264
+ * `application/cloudevents+json` document with the attributes and `data`.
265
+ * Decoding accepts structured and binary mode ({@link decodeCloudEvent}).
266
+ * Event headers travel under their own names; names starting with `x-death`,
267
+ * `x-first-death-`, `x-last-death-` or `x-relaybus-` are refused.
268
+ */
269
+ declare const cloudEventsStructured: RabbitMqCodec;
270
+ /**
271
+ * Where each value of a JSON message lives, for {@link jsonEnvelope}. Besides
272
+ * `{ body }` and `{ header }`, the id may be `'message-id-property'`: the
273
+ * AMQP `messageId` property, which the codec always writes and which survives
274
+ * redeliveries; a message without one is malformed.
275
+ */
276
+ type RabbitMqJsonEnvelopeMapping = JsonEnvelopeMapping<'message-id-property'>;
277
+ /**
278
+ * A JSON wire format declared by a mapping, in both directions: for queues
279
+ * shared with services that wrap events in their own envelope
280
+ * (`{ "eventName": ..., "data": {...} }`, MassTransit) or send the bare
281
+ * payload. Every message also gets the AMQP properties of binary mode:
282
+ * `messageId` (the event id), `type`, `appId` (the source), `correlationId`,
283
+ * `timestamp` (seconds) and `persistent`. Event
284
+ * headers that are message headers are written as they are, so a name the
285
+ * mapping uses for another field, or one the broker reserves, is refused
286
+ * with {@link RabbitMqUnencodableEventError}.
287
+ *
288
+ * ```ts
289
+ * codec: jsonEnvelope({
290
+ * type: { body: 'eventName' },
291
+ * id: 'message-id-property',
292
+ * payload: { body: 'data' },
293
+ * })
294
+ * ```
295
+ *
296
+ * @throws ConfigurationError for an incomplete or self-overlapping mapping.
297
+ */
298
+ declare function jsonEnvelope(mapping: RabbitMqJsonEnvelopeMapping): RabbitMqCodec;
299
+
106
300
  /** Connection settings accepted by `amqplib.connect`, given as an object instead of a URL. */
107
301
  interface BrokerConnectionSettings {
108
302
  readonly protocol?: string;
@@ -139,9 +333,11 @@ interface RabbitMqTransportOptions {
139
333
  readonly url?: string | BrokerConnectionSettings;
140
334
  /**
141
335
  * Opens one connection. Called again after every connection loss. Use it for
142
- * TLS options, custom credentials or an alternative amqplib instance.
336
+ * TLS options, custom credentials or an alternative amqplib instance. `signal` aborts when a
337
+ * shutdown runs past its deadline: pass it on (`amqplib.connect(url, { signal })`) so a broker
338
+ * that never answers the handshake cannot hold the socket open.
143
339
  */
144
- readonly connect?: () => Promise<AmqpConnection>;
340
+ readonly connect?: (options: ConnectOptions) => Promise<AmqpConnection>;
145
341
  /** Topic exchange events are published to. Durable, created when missing. Default `'relaybus'`. */
146
342
  readonly exchange?: string;
147
343
  /**
@@ -209,11 +405,36 @@ interface RabbitMqTransportOptions {
209
405
  readonly publishTimeout?: Duration;
210
406
  /** How long `close` waits for running handlers. Default `'30s'`. */
211
407
  readonly shutdownTimeout?: Duration;
408
+ /**
409
+ * The wire format, both ways: how events become messages and consumed
410
+ * messages become events. Default {@link cloudEventsBinary}: CloudEvents
411
+ * binary mode (`cloudEvents:*` headers, AMQP properties as fallback), any
412
+ * other message is malformed and dead-lettered. {@link cloudEventsStructured}
413
+ * writes structured mode and reads both, {@link jsonEnvelope} maps a JSON
414
+ * format of your own (an envelope with the payload inside, or the bare
415
+ * payload), and an object with `encode` and `decode` can do anything else.
416
+ * Whatever `decode` throws marks the message as malformed: it is
417
+ * dead-lettered, the consumer keeps running.
418
+ */
419
+ readonly codec?: RabbitMqCodec;
420
+ /**
421
+ * How consumed messages become events; events are still written in
422
+ * CloudEvents binary mode.
423
+ *
424
+ * @deprecated Use `codec`: `decode: fn` is `codec: { encode: encodeEnvelope, decode: fn }`.
425
+ * Giving both `decode` and `codec` is a configuration error.
426
+ */
427
+ readonly decode?: RabbitMqMessageDecoder;
212
428
  /** Default: `console`. */
213
429
  readonly logger?: RelayBusLogger;
214
430
  /** Random source for jitter. Default `Math.random`. Intended for tests. */
215
431
  readonly random?: () => number;
216
432
  }
433
+ /** What `connect` gets for each connection it opens. */
434
+ interface ConnectOptions {
435
+ /** Aborted when a shutdown runs past its deadline; destroys a socket still connecting. */
436
+ readonly signal: AbortSignal;
437
+ }
217
438
 
218
439
  /**
219
440
  * RabbitMQ {@link Transport} built on amqplib.
@@ -240,6 +461,7 @@ interface RabbitMqTransportOptions {
240
461
  declare class RabbitMqTransport implements Transport {
241
462
  #private;
242
463
  readonly name: string;
464
+ readonly system = "rabbitmq";
243
465
  constructor(options: RabbitMqTransportOptions);
244
466
  /**
245
467
  * True while the connection is open and publishing and consuming are set up.
@@ -262,6 +484,7 @@ declare class RabbitMqTransport implements Transport {
262
484
  * @throws {RabbitMqUnavailableError} not connected, channel lost, or confirm timed out
263
485
  * @throws {RabbitMqPublishRejectedError} the broker nacked the message
264
486
  * @throws {UnroutableMessageError} no queue is bound to `destination` (unless `allowUnroutable`)
487
+ * @throws whatever the `codec`'s `encode` throws, such as {@link RabbitMqUnencodableEventError}
265
488
  */
266
489
  publish(envelope: EventEnvelope, destination: string): Promise<void>;
267
490
  /**
@@ -269,13 +492,14 @@ declare class RabbitMqTransport implements Transport {
269
492
  * to `onMessage`. Call it once per transport; give each subscription its own
270
493
  * transport instance, with its own `name` and `consumerGroup`.
271
494
  */
272
- subscribe(destinations: readonly string[], onMessage: TransportMessageHandler): Promise<void>;
495
+ subscribe(destinations: readonly string[], onMessage: TransportMessageHandler, hooks?: TransportSubscriptionHooks): Promise<void>;
273
496
  /**
274
497
  * Stops consuming, waits up to `shutdownTimeout` for running handlers
275
498
  * (messages still being retried go back to the queue), then closes the
276
- * channels and the connection.
499
+ * channels and the connection. Once `options.signal` aborts (the bus shutdown deadline passed),
500
+ * the connection's socket is destroyed instead of waiting for the broker.
277
501
  */
278
- close(): Promise<void>;
502
+ close(options?: CloseOptions): Promise<void>;
279
503
  }
280
504
 
281
- export { type AmqpChannel, type AmqpConfirmChannel, type AmqpConnection, AmqpLimitExceededError, type AmqpMessage, type AmqpMessageProperties, type AmqpPublishOptions, type BrokerConnectionSettings, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, type RabbitMqTransportOptions, RabbitMqUnavailableError, type ReconnectOptions, type RedeliveryOptions, UnroutableMessageError, UnserializablePayloadError };
505
+ export { type AmqpChannel, type AmqpConfirmChannel, type AmqpConnection, AmqpLimitExceededError, type AmqpMessage, type AmqpMessageProperties, type AmqpPublishOptions, type BrokerConnectionSettings, type DecodedEvent, type EncodedMessage, MalformedMessageError, type PlainJsonDecoderOptions, type PlainJsonMessage, type RabbitMqCodec, type RabbitMqJsonEnvelopeMapping, type RabbitMqMessageDecoder, RabbitMqPublishRejectedError, RabbitMqTransport, type RabbitMqTransportOptions, RabbitMqUnavailableError, RabbitMqUnencodableEventError, type ReconnectOptions, type RedeliveryOptions, UnroutableMessageError, UnserializablePayloadError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, jsonEnvelope, plainJsonDecoder };