@relaybus/rabbitmq 0.2.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 CHANGED
@@ -55,30 +55,31 @@ narrower check of the connection alone. relaybus health checks use `isReady()`.
55
55
 
56
56
  ## Options
57
57
 
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. |
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. |
82
83
 
83
84
  ## Wire format
84
85
 
@@ -122,37 +123,75 @@ and the `cloudEvents:*` headers carry every attribute. A plain amqplib, Spring A
122
123
  consumer only needs to parse the body; deduplicate on `messageId`, because an outbox delivers at
123
124
  least once. Event headers are prefixed: `traceparent` arrives as `relaybus:traceparent`.
124
125
 
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`:
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):
128
130
 
129
131
  ```ts
130
132
  import {
131
133
  RabbitMqTransport,
132
- decodeCloudEvent,
133
- fromHeader,
134
- fromPayloadField,
134
+ cloudEventsStructured,
135
+ encodeEnvelope,
136
+ jsonEnvelope,
135
137
  plainJsonDecoder,
136
- type RabbitMqMessageDecoder,
138
+ type RabbitMqCodec,
137
139
  } from '@relaybus/rabbitmq';
138
140
 
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 }),
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',
144
154
  });
145
155
 
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'),
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',
150
161
  });
151
162
 
152
- // A CloudEvents SDK producer in structured mode (`contentType: application/cloudevents+json`).
153
- const cloudEvents: RabbitMqMessageDecoder = decodeCloudEvent;
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
+ };
154
168
  ```
155
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
+
156
195
  | Decoder | Accepts |
157
196
  | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158
197
  | default | Binary-mode CloudEvents only, as described above. Unchanged. |
@@ -177,9 +216,9 @@ String message headers become event headers, except the broker's and the transpo
177
216
  **Guarantees stay the same.** Decoding runs again on every redelivery (also after a retry-queue
178
217
  round trip, which keeps `messageId` and restores the original routing key from the
179
218
  `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
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
183
222
  consumer keeps running. Messages are handled in queue order only with `ordered: true`.
184
223
 
185
224
  **Give producers a `messageId`.** Spring AMQP generates one when the message converter has
@@ -222,7 +261,10 @@ payload.
222
261
  `InvalidEnvelopeError` from `bus.receive`. The consumer publishes a confirmed copy to the
223
262
  dead-letter exchange, then acknowledges the original; the copy keeps the message headers, drops
224
263
  the retry state (so replaying it starts a fresh retry window) and adds
225
- `x-relaybus-dead-letter-reason`. If that publish fails the consumer rejects the message without
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
226
268
  requeue and the broker dead-letters it (at-least-once strategy), so it still ends in `<queue>.dead`.
227
269
  - **Broker-level backstop.** `x-delivery-limit` (`deliveryLimit`) dead-letters a message that keeps
228
270
  coming back because the consumer or its channel dies while holding it.