@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/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,30 @@ 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
- | `logger` | `console` | `log`/`warn`/`error`. |
78
- | `random` | `Math.random` | Jitter source, for tests. |
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. |
79
+ | `decode` | strict binary mode | How consumed messages become events. `decodeCloudEvent` (binary and structured mode), `plainJsonDecoder({...})` (plain JSON from producers without relaybus) or your own function. See [Interoperating with services that do not use relaybus](#interoperating-with-services-that-do-not-use-relaybus). |
80
+ | `logger` | `console` | `log`/`warn`/`error`. |
81
+ | `random` | `Math.random` | Jitter source, for tests. |
79
82
 
80
83
  ## Wire format
81
84
 
@@ -111,6 +114,78 @@ rejects with `AmqpLimitExceededError` (a `NonRetryableError`) instead of failing
111
114
  `type`, `appId` and `correlationId` are mirrored into AMQP properties only when they fit; the
112
115
  `cloudEvents:` headers always carry them, so long values lose only the broker-tooling mirror.
113
116
 
117
+ ## Interoperating with services that do not use relaybus
118
+
119
+ **relaybus producer, other consumer.** Bind a queue to the exchange and read the message as is: the
120
+ body is the JSON payload (UTF-8), the `messageId` and `type` properties carry the event id and type,
121
+ and the `cloudEvents:*` headers carry every attribute. A plain amqplib, Spring AMQP, Go or Python
122
+ consumer only needs to parse the body; deduplicate on `messageId`, because an outbox delivers at
123
+ least once. Event headers are prefixed: `traceparent` arrives as `relaybus:traceparent`.
124
+
125
+ **Other producer, relaybus consumer.** By default the transport accepts only binary-mode
126
+ CloudEvents; anything else is malformed and dead-lettered, so a misconfigured producer cannot slip
127
+ events in. Opt in per transport (one transport is one queue) with `decode`:
128
+
129
+ ```ts
130
+ import {
131
+ RabbitMqTransport,
132
+ decodeCloudEvent,
133
+ fromHeader,
134
+ fromPayloadField,
135
+ plainJsonDecoder,
136
+ type RabbitMqMessageDecoder,
137
+ } from '@relaybus/rabbitmq';
138
+
139
+ // A plain amqplib / Spring AMQP producer: JSON body, event type in the `type` property.
140
+ const transport = new RabbitMqTransport({
141
+ url,
142
+ consumerGroup: 'billing',
143
+ decode: plainJsonDecoder({ type: (message) => message.properties.type as string | undefined }),
144
+ });
145
+
146
+ // The type in a header, the ordering key in the payload.
147
+ const byHeader = plainJsonDecoder({
148
+ type: fromHeader('event-type'),
149
+ key: fromPayloadField('orderId'),
150
+ });
151
+
152
+ // A CloudEvents SDK producer in structured mode (`contentType: application/cloudevents+json`).
153
+ const cloudEvents: RabbitMqMessageDecoder = decodeCloudEvent;
154
+ ```
155
+
156
+ | Decoder | Accepts |
157
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158
+ | default | Binary-mode CloudEvents only, as described above. Unchanged. |
159
+ | `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). |
160
+ | `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. |
161
+ | your function | `(message) => ({ id, type, payload, ...optional })`. Missing fields are filled: `source` = `appId` (else routing key), `key` = null, `occurredAt` = `timestamp` property (else receipt), `headers` = `{}`. |
162
+
163
+ `plainJsonDecoder` options (each extractor gets `{ routingKey, properties, headers, payload }`; an
164
+ extractor returning `undefined` or `''` selects the default):
165
+
166
+ | Option | Default | Meaning |
167
+ | ------------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
168
+ | `type` | required | A fixed event type, or an extractor such as `fromHeader('event-type')`, `fromPayloadField('type')` or `(m) => m.properties.type`. |
169
+ | `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. |
170
+ | `key` | none | Ordering key, for example `fromHeader('customer')`. |
171
+ | `source` | the `appId` property, else routing key | Fixed text or an extractor. |
172
+ | `occurredAt` | the `timestamp` property, else receipt | Extractor returning a `Date`. |
173
+
174
+ String message headers become event headers, except the broker's and the transport's own
175
+ (`x-first-death-*`, `x-last-death-*`, `x-relaybus-*`; non-string values such as `x-death` are skipped).
176
+
177
+ **Guarantees stay the same.** Decoding runs again on every redelivery (also after a retry-queue
178
+ round trip, which keeps `messageId` and restores the original routing key from the
179
+ `x-relaybus-routing-key` header it adds to retry copies) and must be deterministic; the inbox deduplicates on the id, so
180
+ a redelivered message is stored and handled once. A message that cannot be decoded (not JSON, no id,
181
+ no type, an invalid type, an extractor returning a non-string) and anything a custom decoder throws
182
+ are dead-lettered (`x-relaybus-dead-letter-reason: malformed message`, the detail is logged) and the
183
+ consumer keeps running. Messages are handled in queue order only with `ordered: true`.
184
+
185
+ **Give producers a `messageId`.** Spring AMQP generates one when the message converter has
186
+ `setCreateMessageIds(true)`; amqplib takes the `messageId` publish option. Without it, configure `id` to read a business id from a header or the
187
+ payload.
188
+
114
189
  ## Guarantees
115
190
 
116
191
  - **No silent loss on publish.** `publish` rejects (never resolves) when the transport is
@@ -235,3 +310,28 @@ The dead-letter queue and exchange are unchanged and need no migration.
235
310
  | `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
311
  | Publishes hang until `publishTimeout` | Broker is blocked (memory or disk alarm) or the connection died without heartbeats. |
237
312
  | `Needs 'consumerGroup' or 'queue' to subscribe` | Set `consumerGroup` (or `queue`) on the transport that subscribes. |
313
+
314
+ ## When to use @relaybus/rabbitmq, and when not to
315
+
316
+ 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`.
317
+
318
+ Documentation: <https://berkcansavur.github.io/relaybus/packages/rabbitmq>
319
+
320
+ ### relaybus vs alternatives
321
+
322
+ <!-- relaybus:snippet:comparison -->
323
+
324
+ | | What it is | Survives a restart | Written in your database transaction | Choose it when |
325
+ | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
326
+ | **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 |
327
+ | [`@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 |
328
+ | [`@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 |
329
+ | [`@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 |
330
+ | [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 |
331
+ | [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 |
332
+ | 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 |
333
+
334
+ This table summarises the vendors' own documentation as of October 2026; check their docs for current details.
335
+ Full comparison and sources: <https://berkcansavur.github.io/relaybus/comparison>.
336
+
337
+ <!-- relaybus:end -->