@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/README.md +166 -24
- package/dist/index.cjs +806 -319
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +231 -7
- package/dist/index.d.ts +231 -7
- package/dist/index.js +795 -321
- package/dist/index.js.map +1 -1
- package/package.json +22 -9
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @relaybus/rabbitmq
|
|
2
2
|
|
|
3
|
+
@relaybus/rabbitmq is the RabbitMQ transport of relaybus, a reliable, storage-agnostic event bus for NestJS and Node.js: it publishes outbox events with publisher confirms and consumes them with manual acknowledgements and an idempotent inbox.
|
|
4
|
+
|
|
3
5
|
RabbitMQ transport for [relaybus](https://github.com/berkcansavur/relaybus). It publishes events
|
|
4
6
|
from the outbox with broker confirms and consumes them with manual acknowledgements, so an event is
|
|
5
7
|
never lost between services: it is either delivered, still waiting in the outbox, or in a
|
|
@@ -53,29 +55,31 @@ narrower check of the connection alone. relaybus health checks use `isReady()`.
|
|
|
53
55
|
|
|
54
56
|
## Options
|
|
55
57
|
|
|
56
|
-
| Option | Default | Description
|
|
57
|
-
| -------------------- | ------------------------------------------------- |
|
|
58
|
-
| `name` | `'rabbitmq'` | Name referenced by routes and subscriptions.
|
|
59
|
-
| `url` | - | `amqp://` URL or `{ hostname, port, username, password, vhost, heartbeat, protocol }`. Give exactly one of `url` and `connect`.
|
|
60
|
-
| `connect` | - | Factory returning an amqplib connection. Called again after every loss. Use for TLS options or credentials.
|
|
61
|
-
| `exchange` | `'relaybus'` | Durable topic exchange, created when missing.
|
|
62
|
-
| `prefetch` | `50` (`1` when `ordered`) | Unacknowledged messages held by the consumer; also the handler concurrency.
|
|
63
|
-
| `queuePrefix` | `'relaybus'` | Prefix of derived queue names.
|
|
64
|
-
| `consumerGroup` | - | Services sharing a group share one queue `<queuePrefix>.<consumerGroup>` and split the messages. Required to subscribe unless `queue` is set.
|
|
65
|
-
| `queue` | - | Explicit queue name; overrides `queuePrefix` and `consumerGroup`.
|
|
66
|
-
| `deadLetterExchange` | `'<exchange>.dead'` | Direct exchange poison messages are dead-lettered to.
|
|
67
|
-
| `deadLetterQueue` | `'<queue>.dead'` | Quorum queue that keeps dead-lettered messages.
|
|
68
|
-
| `deliveryLimit` | `10` | Quorum queue `x-delivery-limit`: a backstop that makes the broker dead-letter a message that crashes its consumer again and again. Failing handlers do not use it.
|
|
69
|
-
| `handlerRetry` | 3 attempts, 200 ms, x2, 5 s cap, equal jitter | Handler attempts, with short backoff, on one delivery before the message is scheduled for delayed redelivery (`RetryPolicyOptions`).
|
|
70
|
-
| `redelivery` | 2 s, x2, 5 min cap, equal jitter, `maxAge` `'1h'` | Delayed redelivery after `handlerRetry`: `initialDelay`, `factor`, `maxDelay`, `jitter`, and `maxAge`, how long a message may keep failing before it is dead-lettered.
|
|
71
|
-
| `ordered` | `false` | Consume strictly in order: single active consumer, one message at a time, failing messages retried in place. See Ordering.
|
|
72
|
-
| `reconnect` | 500 ms, x2, 30 s cap, equal jitter | Reconnect backoff (`initialDelay`, `factor`, `maxDelay`, `jitter`; attempts are unlimited), or `false`.
|
|
73
|
-
| `failOnConnectError` | `true` | `connect()` rejects when the broker is unreachable. When `false` it resolves and keeps trying in the background.
|
|
74
|
-
| `allowUnroutable` | `false` | When `true`, publishing a message no queue is bound to resolves (the broker discards it) instead of rejecting.
|
|
75
|
-
| `publishTimeout` | `'30s'` | How long `publish` waits for the confirm.
|
|
76
|
-
| `shutdownTimeout` | `'30s'` | How long `close()` waits for running handlers.
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
58
|
+
| Option | Default | Description |
|
|
59
|
+
| -------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
60
|
+
| `name` | `'rabbitmq'` | Name referenced by routes and subscriptions. |
|
|
61
|
+
| `url` | - | `amqp://` URL or `{ hostname, port, username, password, vhost, heartbeat, protocol }`. Give exactly one of `url` and `connect`. |
|
|
62
|
+
| `connect` | - | Factory returning an amqplib connection. Called again after every loss. Use for TLS options or credentials. |
|
|
63
|
+
| `exchange` | `'relaybus'` | Durable topic exchange, created when missing. |
|
|
64
|
+
| `prefetch` | `50` (`1` when `ordered`) | Unacknowledged messages held by the consumer; also the handler concurrency. |
|
|
65
|
+
| `queuePrefix` | `'relaybus'` | Prefix of derived queue names. |
|
|
66
|
+
| `consumerGroup` | - | Services sharing a group share one queue `<queuePrefix>.<consumerGroup>` and split the messages. Required to subscribe unless `queue` is set. |
|
|
67
|
+
| `queue` | - | Explicit queue name; overrides `queuePrefix` and `consumerGroup`. |
|
|
68
|
+
| `deadLetterExchange` | `'<exchange>.dead'` | Direct exchange poison messages are dead-lettered to. |
|
|
69
|
+
| `deadLetterQueue` | `'<queue>.dead'` | Quorum queue that keeps dead-lettered messages. |
|
|
70
|
+
| `deliveryLimit` | `10` | Quorum queue `x-delivery-limit`: a backstop that makes the broker dead-letter a message that crashes its consumer again and again. Failing handlers do not use it. |
|
|
71
|
+
| `handlerRetry` | 3 attempts, 200 ms, x2, 5 s cap, equal jitter | Handler attempts, with short backoff, on one delivery before the message is scheduled for delayed redelivery (`RetryPolicyOptions`). |
|
|
72
|
+
| `redelivery` | 2 s, x2, 5 min cap, equal jitter, `maxAge` `'1h'` | Delayed redelivery after `handlerRetry`: `initialDelay`, `factor`, `maxDelay`, `jitter`, and `maxAge`, how long a message may keep failing before it is dead-lettered. |
|
|
73
|
+
| `ordered` | `false` | Consume strictly in order: single active consumer, one message at a time, failing messages retried in place. See Ordering. |
|
|
74
|
+
| `reconnect` | 500 ms, x2, 30 s cap, equal jitter | Reconnect backoff (`initialDelay`, `factor`, `maxDelay`, `jitter`; attempts are unlimited), or `false`. |
|
|
75
|
+
| `failOnConnectError` | `true` | `connect()` rejects when the broker is unreachable. When `false` it resolves and keeps trying in the background. |
|
|
76
|
+
| `allowUnroutable` | `false` | When `true`, publishing a message no queue is bound to resolves (the broker discards it) instead of rejecting. |
|
|
77
|
+
| `publishTimeout` | `'30s'` | How long `publish` waits for the confirm. |
|
|
78
|
+
| `shutdownTimeout` | `'30s'` | How long `close()` waits for running handlers; when the bus shutdown deadline passes first, the connection's socket is destroyed instead. |
|
|
79
|
+
| `codec` | `cloudEventsBinary` | The wire format, both ways: `cloudEventsBinary` (CloudEvents binary mode, strict on read), `cloudEventsStructured`, `jsonEnvelope({...})` (your JSON envelope, MassTransit, or the bare payload) or `{ encode, decode }`. See [Interoperating with services that do not use relaybus](#interoperating-with-services-that-do-not-use-relaybus). |
|
|
80
|
+
| `decode` | none | Deprecated: reads with your function, writes binary mode. Same as `codec: { encode: encodeEnvelope, decode }`; not together with `codec`. |
|
|
81
|
+
| `logger` | `console` | `log`/`warn`/`error`. |
|
|
82
|
+
| `random` | `Math.random` | Jitter source, for tests. |
|
|
79
83
|
|
|
80
84
|
## Wire format
|
|
81
85
|
|
|
@@ -111,6 +115,116 @@ rejects with `AmqpLimitExceededError` (a `NonRetryableError`) instead of failing
|
|
|
111
115
|
`type`, `appId` and `correlationId` are mirrored into AMQP properties only when they fit; the
|
|
112
116
|
`cloudEvents:` headers always carry them, so long values lose only the broker-tooling mirror.
|
|
113
117
|
|
|
118
|
+
## Interoperating with services that do not use relaybus
|
|
119
|
+
|
|
120
|
+
**relaybus producer, other consumer.** Bind a queue to the exchange and read the message as is: the
|
|
121
|
+
body is the JSON payload (UTF-8), the `messageId` and `type` properties carry the event id and type,
|
|
122
|
+
and the `cloudEvents:*` headers carry every attribute. A plain amqplib, Spring AMQP, Go or Python
|
|
123
|
+
consumer only needs to parse the body; deduplicate on `messageId`, because an outbox delivers at
|
|
124
|
+
least once. Event headers are prefixed: `traceparent` arrives as `relaybus:traceparent`.
|
|
125
|
+
|
|
126
|
+
**Other formats, both ways.** By default the transport reads only binary-mode CloudEvents;
|
|
127
|
+
anything else is malformed and dead-lettered, so a misconfigured producer cannot slip events in.
|
|
128
|
+
The `codec` option sets another wire format for publishing and consuming (one transport is one
|
|
129
|
+
queue):
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import {
|
|
133
|
+
RabbitMqTransport,
|
|
134
|
+
cloudEventsStructured,
|
|
135
|
+
encodeEnvelope,
|
|
136
|
+
jsonEnvelope,
|
|
137
|
+
plainJsonDecoder,
|
|
138
|
+
type RabbitMqCodec,
|
|
139
|
+
} from '@relaybus/rabbitmq';
|
|
140
|
+
|
|
141
|
+
// CloudEvents structured mode (`contentType: application/cloudevents+json`); reads binary too.
|
|
142
|
+
new RabbitMqTransport({ url, consumerGroup: 'billing', codec: cloudEventsStructured });
|
|
143
|
+
|
|
144
|
+
// MassTransit's JSON envelope.
|
|
145
|
+
const massTransit = jsonEnvelope({
|
|
146
|
+
type: { body: 'messageType.0' },
|
|
147
|
+
id: { body: 'messageId' },
|
|
148
|
+
payload: { body: 'message' },
|
|
149
|
+
correlationId: { body: 'correlationId' },
|
|
150
|
+
causationId: { body: 'initiatorId' },
|
|
151
|
+
occurredAt: { body: 'sentTime' },
|
|
152
|
+
headers: { body: 'headers' },
|
|
153
|
+
contentType: 'application/vnd.masstransit+json',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// The bare JSON payload of a plain amqplib / Spring AMQP producer, the type in a header.
|
|
157
|
+
const plain = jsonEnvelope({
|
|
158
|
+
type: { header: 'event-type' },
|
|
159
|
+
id: 'message-id-property', // the AMQP messageId, or { header: 'message-id' }
|
|
160
|
+
payload: 'whole-body',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Your own: binary mode out, plain JSON and CloudEvents in.
|
|
164
|
+
const custom: RabbitMqCodec = {
|
|
165
|
+
encode: encodeEnvelope,
|
|
166
|
+
decode: plainJsonDecoder({ type: (message) => message.properties.type as string | undefined }),
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
| Codec | Writes | Reads |
|
|
171
|
+
| ----------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
172
|
+
| `cloudEventsBinary` | Binary mode, as above. Default, byte for byte unchanged. | Binary mode only (AMQP properties as fallback). |
|
|
173
|
+
| `cloudEventsStructured` | One `application/cloudevents+json` document, binary-mode AMQP properties. | Structured and binary mode (`decodeCloudEvent`). |
|
|
174
|
+
| `jsonEnvelope(mapping)` | The mapped JSON body and headers, binary-mode AMQP properties. | The same mapping. A missing type, mapped id or payload is malformed. |
|
|
175
|
+
| `{ encode, decode }` | Whatever `encode` returns (`{ content, options }`; set `messageId`). | Whatever `decode` returns; missing fields are filled as for a decoder (table below). |
|
|
176
|
+
|
|
177
|
+
`jsonEnvelope` mappings name a body path (`{ body: 'meta.correlationId' }`, `'messageType.0'`
|
|
178
|
+
indexes an array) or a header (`{ header: 'event-type' }`) for `type`, `id`, `payload` (or
|
|
179
|
+
`'whole-body'`), `key`, `source`, `occurredAt`, `correlationId`, `causationId` and `headers`, plus a
|
|
180
|
+
`contentType`. `type`, `id` and `payload` are required by the types; `id: 'message-id-property'`
|
|
181
|
+
uses the `messageId` property, which the codec always writes. Unmapped, `source` is the `appId`
|
|
182
|
+
property and `occurredAt` the `timestamp` property (whole seconds). Event headers that are message
|
|
183
|
+
headers keep their names: a name the mapping uses, or one starting with `x-death`,
|
|
184
|
+
`x-first-death-`, `x-last-death-` or `x-relaybus-`, is refused with `RabbitMqUnencodableEventError`
|
|
185
|
+
(a `NonRetryableError`). The
|
|
186
|
+
[interoperability recipe](https://berkcansavur.github.io/relaybus/recipes/interoperating-with-other-services)
|
|
187
|
+
has a full table.
|
|
188
|
+
|
|
189
|
+
Decoded payloads go through the event's schema (`defineEvent(type, { schema })`) as relaybus's own
|
|
190
|
+
do; a payload that fails it makes the delivery dead with `EventValidationError`.
|
|
191
|
+
|
|
192
|
+
The decoders below remain available for a custom codec's `decode`, and for the deprecated `decode`
|
|
193
|
+
option:
|
|
194
|
+
|
|
195
|
+
| Decoder | Accepts |
|
|
196
|
+
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
197
|
+
| default | Binary-mode CloudEvents only, as described above. Unchanged. |
|
|
198
|
+
| `decodeCloudEvent` | Binary mode as the default, plus structured mode: a JSON body with `specversion` `1.0`, `id`, `type`, `source`, optional `time` (else the `timestamp` property, else receipt), `subject` (the key), `data` (the payload; `data_base64` is refused). |
|
|
199
|
+
| `plainJsonDecoder(opts)` | Any JSON body. A message that is a CloudEvent anyway (a `cloudEvents:specversion` header or a structured content type) goes through `decodeCloudEvent`, so relaybus and foreign producers can feed one queue. |
|
|
200
|
+
| your function | `(message) => ({ id, type, payload, ...optional })`. Missing fields are filled: `source` = `appId` (else routing key), `key` = null, `occurredAt` = `timestamp` property (else receipt), `headers` = `{}`. |
|
|
201
|
+
|
|
202
|
+
`plainJsonDecoder` options (each extractor gets `{ routingKey, properties, headers, payload }`; an
|
|
203
|
+
extractor returning `undefined` or `''` selects the default):
|
|
204
|
+
|
|
205
|
+
| Option | Default | Meaning |
|
|
206
|
+
| ------------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
207
|
+
| `type` | required | A fixed event type, or an extractor such as `fromHeader('event-type')`, `fromPayloadField('type')` or `(m) => m.properties.type`. |
|
|
208
|
+
| `id` | the `messageId` property | Inbox dedup id. A message with neither an extracted id nor a `messageId` is malformed: AMQP has no other identity stable across redeliveries. |
|
|
209
|
+
| `key` | none | Ordering key, for example `fromHeader('customer')`. |
|
|
210
|
+
| `source` | the `appId` property, else routing key | Fixed text or an extractor. |
|
|
211
|
+
| `occurredAt` | the `timestamp` property, else receipt | Extractor returning a `Date`. |
|
|
212
|
+
|
|
213
|
+
String message headers become event headers, except the broker's and the transport's own
|
|
214
|
+
(`x-first-death-*`, `x-last-death-*`, `x-relaybus-*`; non-string values such as `x-death` are skipped).
|
|
215
|
+
|
|
216
|
+
**Guarantees stay the same.** Decoding runs again on every redelivery (also after a retry-queue
|
|
217
|
+
round trip, which keeps `messageId` and restores the original routing key from the
|
|
218
|
+
`x-relaybus-routing-key` header it adds to retry copies) and must be deterministic; the inbox deduplicates on the id, so
|
|
219
|
+
a redelivered message is stored and handled once. Under every codec, a message that cannot be
|
|
220
|
+
decoded (not JSON, no id, no type, an invalid type, an extractor returning a non-string) and
|
|
221
|
+
anything a custom `decode` throws are dead-lettered (`x-relaybus-dead-letter-reason: malformed message`, the detail is logged) and the
|
|
222
|
+
consumer keeps running. Messages are handled in queue order only with `ordered: true`.
|
|
223
|
+
|
|
224
|
+
**Give producers a `messageId`.** Spring AMQP generates one when the message converter has
|
|
225
|
+
`setCreateMessageIds(true)`; amqplib takes the `messageId` publish option. Without it, configure `id` to read a business id from a header or the
|
|
226
|
+
payload.
|
|
227
|
+
|
|
114
228
|
## Guarantees
|
|
115
229
|
|
|
116
230
|
- **No silent loss on publish.** `publish` rejects (never resolves) when the transport is
|
|
@@ -147,7 +261,10 @@ rejects with `AmqpLimitExceededError` (a `NonRetryableError`) instead of failing
|
|
|
147
261
|
`InvalidEnvelopeError` from `bus.receive`. The consumer publishes a confirmed copy to the
|
|
148
262
|
dead-letter exchange, then acknowledges the original; the copy keeps the message headers, drops
|
|
149
263
|
the retry state (so replaying it starts a fresh retry window) and adds
|
|
150
|
-
`x-relaybus-dead-letter-reason
|
|
264
|
+
`x-relaybus-dead-letter-reason`, `x-relaybus-dead-letter-error-name` (the error class, such as
|
|
265
|
+
`MalformedMessageError`) and, for relaybus errors, `x-relaybus-dead-letter-error-code` (such as
|
|
266
|
+
`RELAYBUS_RABBITMQ_MALFORMED_MESSAGE`, see [Error codes](https://berkcansavur.github.io/relaybus/concepts/error-codes)).
|
|
267
|
+
The `error` log lines carry the same `errorName=` and `code=` fields. If that publish fails the consumer rejects the message without
|
|
151
268
|
requeue and the broker dead-letters it (at-least-once strategy), so it still ends in `<queue>.dead`.
|
|
152
269
|
- **Broker-level backstop.** `x-delivery-limit` (`deliveryLimit`) dead-letters a message that keeps
|
|
153
270
|
coming back because the consumer or its channel dies while holding it.
|
|
@@ -235,3 +352,28 @@ The dead-letter queue and exchange are unchanged and need no migration.
|
|
|
235
352
|
| `isReady()` is false but `isConnected()` is true | The consumer was cancelled (queue deleted, node failover) and is being restored, or the publisher channel closed. |
|
|
236
353
|
| Publishes hang until `publishTimeout` | Broker is blocked (memory or disk alarm) or the connection died without heartbeats. |
|
|
237
354
|
| `Needs 'consumerGroup' or 'queue' to subscribe` | Set `consumerGroup` (or `queue`) on the transport that subscribes. |
|
|
355
|
+
|
|
356
|
+
## When to use @relaybus/rabbitmq, and when not to
|
|
357
|
+
|
|
358
|
+
Use it to send events between services through RabbitMQ with publisher confirms, retry queues and dead-letter queues. It needs RabbitMQ 3.13 or 4.x (quorum queues) and does not preserve order unless you set `ordered: true`.
|
|
359
|
+
|
|
360
|
+
Documentation: <https://berkcansavur.github.io/relaybus/packages/rabbitmq>
|
|
361
|
+
|
|
362
|
+
### relaybus vs alternatives
|
|
363
|
+
|
|
364
|
+
<!-- relaybus:snippet:comparison -->
|
|
365
|
+
|
|
366
|
+
| | What it is | Survives a restart | Written in your database transaction | Choose it when |
|
|
367
|
+
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
368
|
+
| **relaybus** | Event bus built on the transactional outbox: PostgreSQL and MongoDB stores, Kafka and RabbitMQ transports, per-handler deliveries | Yes, events live in your database | Yes | Events must never be lost or phantom, and handlers need retries, dead letters and ordering keys |
|
|
369
|
+
| [`@nestjs/event-emitter`](https://docs.nestjs.com/techniques/events) | In-process event emitter built on `eventemitter2` | No persistence is documented | No | Simple in-process notifications where losing an event on restart is acceptable |
|
|
370
|
+
| [`@nestjs/cqrs`](https://docs.nestjs.com/recipes/cqrs) | CommandBus, QueryBus, EventBus and sagas; the docs say sagas and event handlers run in memory | No built-in persistence | No | You want CQRS structure; it can sit on top of an outbox |
|
|
371
|
+
| [`@nestjs/outbox`](https://github.com/nestjs/outbox) | Transactional outbox module from the NestJS organisation: PostgreSQL, MySQL and in-memory stores, relay with leases and retries, dead-letter table, inbox | Yes with the database stores | Yes | You need MySQL, or a NestJS-organisation module; compare features with relaybus before choosing |
|
|
372
|
+
| [BullMQ](https://docs.bullmq.io/) | Job queue on Redis with retries, delayed jobs and flows; at-least-once in the worst case | Yes, in Redis | No, jobs are stored in Redis and cannot join a SQL or MongoDB transaction | Your stack is Redis-based and background jobs matter more than event atomicity |
|
|
373
|
+
| [pg-boss](https://github.com/timgit/pg-boss) | Job queue on PostgreSQL using `SKIP LOCKED`, with retries, dead letter queues, scheduling and pub/sub; jobs can be created in your own transaction | Yes, in PostgreSQL | Yes, through its database adapters | You want a PostgreSQL job queue with scheduling; relaybus is an event bus with per-handler deliveries and transports |
|
|
374
|
+
| Kafka client directly | A distributed log; the idempotent producer and transactions give exactly-once within Kafka | Yes, in Kafka | No, producing to Kafka is a second write | You consume and produce only inside Kafka; add an outbox when a database write must be announced |
|
|
375
|
+
|
|
376
|
+
This table summarises the vendors' own documentation as of October 2026; check their docs for current details.
|
|
377
|
+
Full comparison and sources: <https://berkcansavur.github.io/relaybus/comparison>.
|
|
378
|
+
|
|
379
|
+
<!-- relaybus:end -->
|