@relaybus/rabbitmq 0.1.0 → 0.2.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,5 @@
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 } from '@relaybus/core';
3
3
 
4
4
  /**
5
5
  * The slice of amqplib this package relies on. Declaring it here keeps the
@@ -103,6 +103,113 @@ declare class AmqpLimitExceededError extends NonRetryableError {
103
103
  name: string;
104
104
  }
105
105
 
106
+ /**
107
+ * The event a decoder extracted from a message. `id`, `type` and `payload` are
108
+ * required; every other field may be left out and is then filled by the
109
+ * transport: `source` with the `appId` property (else the routing key), `key`
110
+ * with null, `headers` with none, `correlationId` and `causationId` with null,
111
+ * and `occurredAt` with the `timestamp` property (else the time of receipt).
112
+ */
113
+ interface DecodedEvent {
114
+ readonly id: string;
115
+ readonly type: string;
116
+ readonly payload: unknown;
117
+ readonly source?: string;
118
+ readonly key?: string | null;
119
+ readonly headers?: EventHeaders;
120
+ readonly correlationId?: string | null;
121
+ readonly causationId?: string | null;
122
+ readonly occurredAt?: Date;
123
+ }
124
+ /**
125
+ * Turns a consumed message into an event, for queues fed by producers that do
126
+ * not use relaybus. Must be synchronous and free of side effects: it runs again
127
+ * on every redelivery, so the same message must always give the same event,
128
+ * above all the same `id`, which is what the inbox deduplicates on.
129
+ *
130
+ * Anything it throws marks the message as malformed: it is dead-lettered and
131
+ * the consumer moves on. An event the bus then rejects (empty id, invalid type
132
+ * or time) is dead-lettered the same way.
133
+ */
134
+ type RabbitMqMessageDecoder = (message: AmqpMessage) => DecodedEvent;
135
+ /** A plain JSON message, decoded for the extractor options of {@link plainJsonDecoder}. */
136
+ interface PlainJsonMessage {
137
+ readonly routingKey: string;
138
+ /** The AMQP properties as delivered (`messageId`, `type`, `appId`, `timestamp`, ...). */
139
+ readonly properties: AmqpMessageProperties;
140
+ /**
141
+ * The message headers with string values, without the broker's and the
142
+ * transport's own (`x-death`, `x-first-death-*`, `x-last-death-*`, `x-relaybus-*`).
143
+ */
144
+ readonly headers: EventHeaders;
145
+ /** The body parsed as JSON. */
146
+ readonly payload: unknown;
147
+ }
148
+ /** Options of {@link plainJsonDecoder}. Extractors returning undefined or `''` select the default. */
149
+ interface PlainJsonDecoderOptions {
150
+ /**
151
+ * Event type handlers subscribe to: a fixed name for a queue that carries one
152
+ * kind of event, or an extractor such as `fromHeader('event-type')`,
153
+ * `fromPayloadField('type')` or `(message) => message.properties.type`.
154
+ * Required; a message without a type is malformed.
155
+ */
156
+ readonly type: string | ((message: PlainJsonMessage) => string | undefined);
157
+ /**
158
+ * Event id the inbox deduplicates on. Default, and fallback when the
159
+ * extractor finds none: the `messageId` property, which survives
160
+ * redeliveries. A message with neither is malformed, because AMQP has no
161
+ * other identity that is stable across redeliveries; give producers a
162
+ * `messageId` (Spring AMQP can generate one with `setCreateMessageIds(true)`).
163
+ */
164
+ readonly id?: (message: PlainJsonMessage) => string | undefined;
165
+ /** Ordering key. Default: none. */
166
+ readonly key?: (message: PlainJsonMessage) => string | null | undefined;
167
+ /** Logical name of the producer. Default: the `appId` property, else the routing key. */
168
+ readonly source?: string | ((message: PlainJsonMessage) => string | undefined);
169
+ /** When the event happened. Default: the `timestamp` property, else the time of receipt. */
170
+ readonly occurredAt?: (message: PlainJsonMessage) => Date | undefined;
171
+ }
172
+ /**
173
+ * Decodes a CloudEvents 1.0 message in either content mode:
174
+ * - structured (`contentType` property `application/cloudevents+json`): the
175
+ * body is a JSON object with `specversion`, `id`, `type`, `source` and
176
+ * optionally `time` (else the `timestamp` property, else the time of
177
+ * receipt), `subject` (the key), `data` (the payload, null when absent) and
178
+ * a JSON `datacontenttype`. `data_base64` is refused, because relaybus
179
+ * payloads are JSON. Extension attributes with string values (`traceparent`,
180
+ * ...) become event headers, as do the string message headers;
181
+ * `correlationid` and `causationid` fill their envelope fields.
182
+ * - binary (anything else): exactly the default decoder, `cloudEvents:*`
183
+ * headers with the AMQP properties as fallback.
184
+ *
185
+ * @throws MalformedMessageError for a message that is neither.
186
+ */
187
+ declare function decodeCloudEvent(message: AmqpMessage): EventEnvelope;
188
+ /**
189
+ * Creates a decoder for messages whose body is plain JSON, as published by a
190
+ * service that does not use relaybus (plain amqplib, Spring AMQP's
191
+ * `Jackson2JsonMessageConverter`, a Go or Python service): the body becomes the
192
+ * payload, the string headers become event headers, and the options say where
193
+ * the event type and id come from.
194
+ *
195
+ * A message that is a CloudEvent anyway (a `cloudEvents:specversion` header,
196
+ * or a structured `application/cloudevents+json` body) is decoded with
197
+ * {@link decodeCloudEvent} instead, so relaybus and foreign producers can feed
198
+ * one queue. A body that is not JSON, a missing or invalid type, a missing id
199
+ * and an extractor returning anything but a string are malformed.
200
+ *
201
+ * ```ts
202
+ * new RabbitMqTransport({
203
+ * url,
204
+ * consumerGroup: 'billing',
205
+ * decode: plainJsonDecoder({ type: (message) => message.properties.type as string }),
206
+ * });
207
+ * ```
208
+ *
209
+ * @throws ConfigurationError when a fixed `type` is not a valid event type.
210
+ */
211
+ declare function plainJsonDecoder(options: PlainJsonDecoderOptions): RabbitMqMessageDecoder;
212
+
106
213
  /** Connection settings accepted by `amqplib.connect`, given as an object instead of a URL. */
107
214
  interface BrokerConnectionSettings {
108
215
  readonly protocol?: string;
@@ -209,6 +316,18 @@ interface RabbitMqTransportOptions {
209
316
  readonly publishTimeout?: Duration;
210
317
  /** How long `close` waits for running handlers. Default `'30s'`. */
211
318
  readonly shutdownTimeout?: Duration;
319
+ /**
320
+ * How consumed messages become events. Default: CloudEvents binary mode as
321
+ * relaybus producers write it (`cloudEvents:*` headers, AMQP properties as
322
+ * fallback); any other message is malformed and dead-lettered. Set it to
323
+ * consume messages from services that do not use relaybus:
324
+ * {@link decodeCloudEvent} also accepts structured mode, {@link plainJsonDecoder}
325
+ * maps plain JSON messages, and a function of your own can do anything else
326
+ * (switch on `message.fields.routingKey` to decode routing keys differently).
327
+ * Whatever a decoder throws marks the message as malformed: it is
328
+ * dead-lettered, the consumer keeps running.
329
+ */
330
+ readonly decode?: RabbitMqMessageDecoder;
212
331
  /** Default: `console`. */
213
332
  readonly logger?: RelayBusLogger;
214
333
  /** Random source for jitter. Default `Math.random`. Intended for tests. */
@@ -278,4 +397,33 @@ declare class RabbitMqTransport implements Transport {
278
397
  close(): Promise<void>;
279
398
  }
280
399
 
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 };
400
+ /**
401
+ * Reads one header of an incoming message, for the `type`, `id`, `key` or
402
+ * `source` option of {@link plainJsonDecoder}. Header names are compared
403
+ * exactly (AMQP header names are case-sensitive). Returns undefined when the
404
+ * header is absent, so the decoder falls back to its default.
405
+ *
406
+ * ```ts
407
+ * plainJsonDecoder({ type: fromHeader('event-type'), id: fromHeader('message-id') })
408
+ * ```
409
+ */
410
+ declare function fromHeader(name: string): (message: {
411
+ readonly headers: EventHeaders;
412
+ }) => string | undefined;
413
+ /**
414
+ * Reads a field of the JSON payload by a dotted path (`'meta.eventType'`), for
415
+ * the `type`, `id`, `key` or `source` option of {@link plainJsonDecoder}. A
416
+ * string is returned as is and a safe integer as its decimal text (numeric ids
417
+ * are common). Anything else returns undefined so the decoder falls back to its
418
+ * default: a missing field, and also a fraction or an integer beyond 2^53, which
419
+ * JSON parsing may already have rounded, so two different ids could collide.
420
+ *
421
+ * ```ts
422
+ * plainJsonDecoder({ type: fromPayloadField('eventType'), key: fromPayloadField('order.id') })
423
+ * ```
424
+ */
425
+ declare function fromPayloadField(path: string): (message: {
426
+ readonly payload: unknown;
427
+ }) => string | undefined;
428
+
429
+ export { type AmqpChannel, type AmqpConfirmChannel, type AmqpConnection, AmqpLimitExceededError, type AmqpMessage, type AmqpMessageProperties, type AmqpPublishOptions, type BrokerConnectionSettings, type DecodedEvent, MalformedMessageError, type PlainJsonDecoderOptions, type PlainJsonMessage, type RabbitMqMessageDecoder, RabbitMqPublishRejectedError, RabbitMqTransport, type RabbitMqTransportOptions, RabbitMqUnavailableError, type ReconnectOptions, type RedeliveryOptions, UnroutableMessageError, UnserializablePayloadError, decodeCloudEvent, fromHeader, fromPayloadField, plainJsonDecoder };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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 } from '@relaybus/core';
3
3
 
4
4
  /**
5
5
  * The slice of amqplib this package relies on. Declaring it here keeps the
@@ -103,6 +103,113 @@ declare class AmqpLimitExceededError extends NonRetryableError {
103
103
  name: string;
104
104
  }
105
105
 
106
+ /**
107
+ * The event a decoder extracted from a message. `id`, `type` and `payload` are
108
+ * required; every other field may be left out and is then filled by the
109
+ * transport: `source` with the `appId` property (else the routing key), `key`
110
+ * with null, `headers` with none, `correlationId` and `causationId` with null,
111
+ * and `occurredAt` with the `timestamp` property (else the time of receipt).
112
+ */
113
+ interface DecodedEvent {
114
+ readonly id: string;
115
+ readonly type: string;
116
+ readonly payload: unknown;
117
+ readonly source?: string;
118
+ readonly key?: string | null;
119
+ readonly headers?: EventHeaders;
120
+ readonly correlationId?: string | null;
121
+ readonly causationId?: string | null;
122
+ readonly occurredAt?: Date;
123
+ }
124
+ /**
125
+ * Turns a consumed message into an event, for queues fed by producers that do
126
+ * not use relaybus. Must be synchronous and free of side effects: it runs again
127
+ * on every redelivery, so the same message must always give the same event,
128
+ * above all the same `id`, which is what the inbox deduplicates on.
129
+ *
130
+ * Anything it throws marks the message as malformed: it is dead-lettered and
131
+ * the consumer moves on. An event the bus then rejects (empty id, invalid type
132
+ * or time) is dead-lettered the same way.
133
+ */
134
+ type RabbitMqMessageDecoder = (message: AmqpMessage) => DecodedEvent;
135
+ /** A plain JSON message, decoded for the extractor options of {@link plainJsonDecoder}. */
136
+ interface PlainJsonMessage {
137
+ readonly routingKey: string;
138
+ /** The AMQP properties as delivered (`messageId`, `type`, `appId`, `timestamp`, ...). */
139
+ readonly properties: AmqpMessageProperties;
140
+ /**
141
+ * The message headers with string values, without the broker's and the
142
+ * transport's own (`x-death`, `x-first-death-*`, `x-last-death-*`, `x-relaybus-*`).
143
+ */
144
+ readonly headers: EventHeaders;
145
+ /** The body parsed as JSON. */
146
+ readonly payload: unknown;
147
+ }
148
+ /** Options of {@link plainJsonDecoder}. Extractors returning undefined or `''` select the default. */
149
+ interface PlainJsonDecoderOptions {
150
+ /**
151
+ * Event type handlers subscribe to: a fixed name for a queue that carries one
152
+ * kind of event, or an extractor such as `fromHeader('event-type')`,
153
+ * `fromPayloadField('type')` or `(message) => message.properties.type`.
154
+ * Required; a message without a type is malformed.
155
+ */
156
+ readonly type: string | ((message: PlainJsonMessage) => string | undefined);
157
+ /**
158
+ * Event id the inbox deduplicates on. Default, and fallback when the
159
+ * extractor finds none: the `messageId` property, which survives
160
+ * redeliveries. A message with neither is malformed, because AMQP has no
161
+ * other identity that is stable across redeliveries; give producers a
162
+ * `messageId` (Spring AMQP can generate one with `setCreateMessageIds(true)`).
163
+ */
164
+ readonly id?: (message: PlainJsonMessage) => string | undefined;
165
+ /** Ordering key. Default: none. */
166
+ readonly key?: (message: PlainJsonMessage) => string | null | undefined;
167
+ /** Logical name of the producer. Default: the `appId` property, else the routing key. */
168
+ readonly source?: string | ((message: PlainJsonMessage) => string | undefined);
169
+ /** When the event happened. Default: the `timestamp` property, else the time of receipt. */
170
+ readonly occurredAt?: (message: PlainJsonMessage) => Date | undefined;
171
+ }
172
+ /**
173
+ * Decodes a CloudEvents 1.0 message in either content mode:
174
+ * - structured (`contentType` property `application/cloudevents+json`): the
175
+ * body is a JSON object with `specversion`, `id`, `type`, `source` and
176
+ * optionally `time` (else the `timestamp` property, else the time of
177
+ * receipt), `subject` (the key), `data` (the payload, null when absent) and
178
+ * a JSON `datacontenttype`. `data_base64` is refused, because relaybus
179
+ * payloads are JSON. Extension attributes with string values (`traceparent`,
180
+ * ...) become event headers, as do the string message headers;
181
+ * `correlationid` and `causationid` fill their envelope fields.
182
+ * - binary (anything else): exactly the default decoder, `cloudEvents:*`
183
+ * headers with the AMQP properties as fallback.
184
+ *
185
+ * @throws MalformedMessageError for a message that is neither.
186
+ */
187
+ declare function decodeCloudEvent(message: AmqpMessage): EventEnvelope;
188
+ /**
189
+ * Creates a decoder for messages whose body is plain JSON, as published by a
190
+ * service that does not use relaybus (plain amqplib, Spring AMQP's
191
+ * `Jackson2JsonMessageConverter`, a Go or Python service): the body becomes the
192
+ * payload, the string headers become event headers, and the options say where
193
+ * the event type and id come from.
194
+ *
195
+ * A message that is a CloudEvent anyway (a `cloudEvents:specversion` header,
196
+ * or a structured `application/cloudevents+json` body) is decoded with
197
+ * {@link decodeCloudEvent} instead, so relaybus and foreign producers can feed
198
+ * one queue. A body that is not JSON, a missing or invalid type, a missing id
199
+ * and an extractor returning anything but a string are malformed.
200
+ *
201
+ * ```ts
202
+ * new RabbitMqTransport({
203
+ * url,
204
+ * consumerGroup: 'billing',
205
+ * decode: plainJsonDecoder({ type: (message) => message.properties.type as string }),
206
+ * });
207
+ * ```
208
+ *
209
+ * @throws ConfigurationError when a fixed `type` is not a valid event type.
210
+ */
211
+ declare function plainJsonDecoder(options: PlainJsonDecoderOptions): RabbitMqMessageDecoder;
212
+
106
213
  /** Connection settings accepted by `amqplib.connect`, given as an object instead of a URL. */
107
214
  interface BrokerConnectionSettings {
108
215
  readonly protocol?: string;
@@ -209,6 +316,18 @@ interface RabbitMqTransportOptions {
209
316
  readonly publishTimeout?: Duration;
210
317
  /** How long `close` waits for running handlers. Default `'30s'`. */
211
318
  readonly shutdownTimeout?: Duration;
319
+ /**
320
+ * How consumed messages become events. Default: CloudEvents binary mode as
321
+ * relaybus producers write it (`cloudEvents:*` headers, AMQP properties as
322
+ * fallback); any other message is malformed and dead-lettered. Set it to
323
+ * consume messages from services that do not use relaybus:
324
+ * {@link decodeCloudEvent} also accepts structured mode, {@link plainJsonDecoder}
325
+ * maps plain JSON messages, and a function of your own can do anything else
326
+ * (switch on `message.fields.routingKey` to decode routing keys differently).
327
+ * Whatever a decoder throws marks the message as malformed: it is
328
+ * dead-lettered, the consumer keeps running.
329
+ */
330
+ readonly decode?: RabbitMqMessageDecoder;
212
331
  /** Default: `console`. */
213
332
  readonly logger?: RelayBusLogger;
214
333
  /** Random source for jitter. Default `Math.random`. Intended for tests. */
@@ -278,4 +397,33 @@ declare class RabbitMqTransport implements Transport {
278
397
  close(): Promise<void>;
279
398
  }
280
399
 
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 };
400
+ /**
401
+ * Reads one header of an incoming message, for the `type`, `id`, `key` or
402
+ * `source` option of {@link plainJsonDecoder}. Header names are compared
403
+ * exactly (AMQP header names are case-sensitive). Returns undefined when the
404
+ * header is absent, so the decoder falls back to its default.
405
+ *
406
+ * ```ts
407
+ * plainJsonDecoder({ type: fromHeader('event-type'), id: fromHeader('message-id') })
408
+ * ```
409
+ */
410
+ declare function fromHeader(name: string): (message: {
411
+ readonly headers: EventHeaders;
412
+ }) => string | undefined;
413
+ /**
414
+ * Reads a field of the JSON payload by a dotted path (`'meta.eventType'`), for
415
+ * the `type`, `id`, `key` or `source` option of {@link plainJsonDecoder}. A
416
+ * string is returned as is and a safe integer as its decimal text (numeric ids
417
+ * are common). Anything else returns undefined so the decoder falls back to its
418
+ * default: a missing field, and also a fraction or an integer beyond 2^53, which
419
+ * JSON parsing may already have rounded, so two different ids could collide.
420
+ *
421
+ * ```ts
422
+ * plainJsonDecoder({ type: fromPayloadField('eventType'), key: fromPayloadField('order.id') })
423
+ * ```
424
+ */
425
+ declare function fromPayloadField(path: string): (message: {
426
+ readonly payload: unknown;
427
+ }) => string | undefined;
428
+
429
+ export { type AmqpChannel, type AmqpConfirmChannel, type AmqpConnection, AmqpLimitExceededError, type AmqpMessage, type AmqpMessageProperties, type AmqpPublishOptions, type BrokerConnectionSettings, type DecodedEvent, MalformedMessageError, type PlainJsonDecoderOptions, type PlainJsonMessage, type RabbitMqMessageDecoder, RabbitMqPublishRejectedError, RabbitMqTransport, type RabbitMqTransportOptions, RabbitMqUnavailableError, type ReconnectOptions, type RedeliveryOptions, UnroutableMessageError, UnserializablePayloadError, decodeCloudEvent, fromHeader, fromPayloadField, plainJsonDecoder };