rbtmq-resilience 0.1.0-rc.1
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 +609 -0
- package/dist/application/configMapper.d.ts +4 -0
- package/dist/application/configMapper.d.ts.map +1 -0
- package/dist/application/failedMessageMonitor.d.ts +10 -0
- package/dist/application/failedMessageMonitor.d.ts.map +1 -0
- package/dist/application/resilience.d.ts +44 -0
- package/dist/application/resilience.d.ts.map +1 -0
- package/dist/domain/dtos/config.dto.d.ts +177 -0
- package/dist/domain/dtos/config.dto.d.ts.map +1 -0
- package/dist/domain/dtos/eventManager.cjs +131 -0
- package/dist/domain/dtos/eventManager.d.ts +2 -0
- package/dist/domain/dtos/eventManager.d.ts.map +1 -0
- package/dist/domain/dtos/eventManager.dto.d.ts +32 -0
- package/dist/domain/dtos/eventManager.dto.d.ts.map +1 -0
- package/dist/domain/dtos/eventManager.mjs +98 -0
- package/dist/domain/dtos/handler.dto.d.ts +23 -0
- package/dist/domain/dtos/handler.dto.d.ts.map +1 -0
- package/dist/domain/dtos/messageProcessing.dto.d.ts +19 -0
- package/dist/domain/dtos/messageProcessing.dto.d.ts.map +1 -0
- package/dist/domain/dtos/metrics.dto.d.ts +19 -0
- package/dist/domain/dtos/metrics.dto.d.ts.map +1 -0
- package/dist/domain/dtos/naming.dto.d.ts +15 -0
- package/dist/domain/dtos/naming.dto.d.ts.map +1 -0
- package/dist/domain/dtos/publish.dto.d.ts +23 -0
- package/dist/domain/dtos/publish.dto.d.ts.map +1 -0
- package/dist/index.cjs +38081 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +38048 -0
- package/dist/infrastructure/database/database.service.d.ts +13 -0
- package/dist/infrastructure/database/database.service.d.ts.map +1 -0
- package/dist/infrastructure/database/migrations.d.ts +2 -0
- package/dist/infrastructure/database/migrations.d.ts.map +1 -0
- package/dist/infrastructure/database/repositories.d.ts +19 -0
- package/dist/infrastructure/database/repositories.d.ts.map +1 -0
- package/dist/infrastructure/database/schema.d.ts +256 -0
- package/dist/infrastructure/database/schema.d.ts.map +1 -0
- package/dist/infrastructure/rabbitmq/rabbitmq.service.d.ts +22 -0
- package/dist/infrastructure/rabbitmq/rabbitmq.service.d.ts.map +1 -0
- package/dist/presentation/healthServer.d.ts +13 -0
- package/dist/presentation/healthServer.d.ts.map +1 -0
- package/dist/shared/constants.d.ts +31 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/naming.d.ts +10 -0
- package/dist/shared/naming.d.ts.map +1 -0
- package/dist/shared/primitives.d.ts +5 -0
- package/dist/shared/primitives.d.ts.map +1 -0
- package/dist/shared/types.d.ts +10 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/utils.d.ts +11 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
# rbtmq-resilience
|
|
2
|
+
|
|
3
|
+
Reusable RabbitMQ resilience library for TypeScript microservices.
|
|
4
|
+
|
|
5
|
+
`rbtmq-resilience` helps services publish and consume RabbitMQ messages with a consistent topology, retries, poison-message handling, inbox/outbox persistence, and optional health endpoints.
|
|
6
|
+
|
|
7
|
+
It is designed for services that need:
|
|
8
|
+
|
|
9
|
+
- Integration events through a `fanout` exchange.
|
|
10
|
+
- Tactical/internal events through a `topic` exchange.
|
|
11
|
+
- Commands, retries, and error routing through a `direct` exchange.
|
|
12
|
+
- Immediate retries in memory.
|
|
13
|
+
- Delayed retries with RabbitMQ TTL and dead-letter routing.
|
|
14
|
+
- Inbox idempotency per `message_id + handler_name`.
|
|
15
|
+
- Outbox publishing backed by MySQL and Drizzle ORM.
|
|
16
|
+
- Runtime health and resilience metrics through Hono.
|
|
17
|
+
|
|
18
|
+
All names in this README are fictional examples. Replace `commerce`, `orders`, `checkout`, and `app.*` with names that match your own service and RabbitMQ topology.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add rbtmq-resilience@rc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or with npm:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install rbtmq-resilience@rc
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Exchange Model
|
|
33
|
+
|
|
34
|
+
Use three exchange names with three different responsibilities:
|
|
35
|
+
|
|
36
|
+
```txt
|
|
37
|
+
integration exchange fanout public integration events
|
|
38
|
+
tactical exchange topic tactical/internal events
|
|
39
|
+
direct exchange direct commands, retries, and error queues
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
|
|
44
|
+
```env
|
|
45
|
+
RABBIT_EXCHANGE=app.integration
|
|
46
|
+
RABBIT_TYPE_EXCHANGE=fanout
|
|
47
|
+
|
|
48
|
+
RABBIT_TACTICAL_EXCHANGE=app.tactical
|
|
49
|
+
RABBIT_TACTICAL_TYPE_EXCHANGE=topic
|
|
50
|
+
|
|
51
|
+
RABBIT_DIRECT_EXCHANGE=app.direct
|
|
52
|
+
RABBIT_TYPE_DIRECT_EXCHANGE=direct
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
RabbitMQ does not allow an existing exchange to change type. If `app.integration` already exists as `fanout`, do not reuse it as `topic`; create a separate tactical exchange such as `app.tactical`.
|
|
56
|
+
|
|
57
|
+
## Queue Naming
|
|
58
|
+
|
|
59
|
+
Instead of storing full queue names in `.env`, store the parts that identify the service:
|
|
60
|
+
|
|
61
|
+
```env
|
|
62
|
+
RABBIT_DOMAIN=commerce
|
|
63
|
+
RABBIT_SUBDOMAIN=orders
|
|
64
|
+
RABBIT_BOUNDED_CONTEXT=checkout
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then generate names with `RbtmqNaming`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { QueueNamePartsDto, RbtmqNaming } from "rbtmq-resilience";
|
|
71
|
+
|
|
72
|
+
const eventNames = RbtmqNaming.queueNames(
|
|
73
|
+
new QueueNamePartsDto(
|
|
74
|
+
"commerce",
|
|
75
|
+
"orders",
|
|
76
|
+
"checkout",
|
|
77
|
+
"events",
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
console.log(eventNames.queue);
|
|
82
|
+
// commerce.orders.checkout.events
|
|
83
|
+
|
|
84
|
+
console.log(eventNames.retryQueue);
|
|
85
|
+
// commerce.orders.checkout.events-retry
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For topic bindings:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const bindingPattern = RbtmqNaming.tacticalBindingPattern(
|
|
92
|
+
"commerce",
|
|
93
|
+
"orders",
|
|
94
|
+
"checkout",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
console.log(bindingPattern);
|
|
98
|
+
// commerce.orders.checkout.#
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Example Environment Variables
|
|
102
|
+
|
|
103
|
+
```env
|
|
104
|
+
PORT=3001
|
|
105
|
+
|
|
106
|
+
DB_HOST=localhost
|
|
107
|
+
DB_PORT=3306
|
|
108
|
+
DB_USER=root
|
|
109
|
+
DB_PASSWORD=secret
|
|
110
|
+
DB_NAME=service_db
|
|
111
|
+
|
|
112
|
+
RABBIT_PROTOCOL=amqp
|
|
113
|
+
RABBIT_HOSTNAME=localhost
|
|
114
|
+
RABBIT_PORT=5672
|
|
115
|
+
RABBIT_USERNAME=guest
|
|
116
|
+
RABBIT_PASSWORD=guest
|
|
117
|
+
RABBIT_VHOST=/
|
|
118
|
+
|
|
119
|
+
RABBIT_DOMAIN=commerce
|
|
120
|
+
RABBIT_SUBDOMAIN=orders
|
|
121
|
+
RABBIT_BOUNDED_CONTEXT=checkout
|
|
122
|
+
|
|
123
|
+
RABBIT_EXCHANGE=app.integration
|
|
124
|
+
RABBIT_TYPE_EXCHANGE=fanout
|
|
125
|
+
|
|
126
|
+
RABBIT_TACTICAL_EXCHANGE=app.tactical
|
|
127
|
+
RABBIT_TACTICAL_TYPE_EXCHANGE=topic
|
|
128
|
+
|
|
129
|
+
RABBIT_DIRECT_EXCHANGE=app.direct
|
|
130
|
+
RABBIT_TYPE_DIRECT_EXCHANGE=direct
|
|
131
|
+
|
|
132
|
+
RABBIT_PREFETCH=1
|
|
133
|
+
RABBIT_RETRY_ENDPOINT=checkout-service
|
|
134
|
+
RABBIT_MESSAGE_TTL=10000
|
|
135
|
+
|
|
136
|
+
RABBIT_DEAD_LETTER_QUEUE=commerce.orders.checkout.error
|
|
137
|
+
RABBIT_DEAD_LETTER_ROUTING_KEY=commerce.orders.checkout.error
|
|
138
|
+
|
|
139
|
+
RABBIT_HEALTH_PORT=3002
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Use only the consumer blocks your service needs:
|
|
143
|
+
|
|
144
|
+
```txt
|
|
145
|
+
Only integration events configure eventsToProcess
|
|
146
|
+
Integration + topic events configure eventsToProcess and tacticalEvents
|
|
147
|
+
Commands only configure commands and keep eventsToProcess as []
|
|
148
|
+
Retries and error queues always use the direct exchange internally
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Register Integration Event Handlers
|
|
152
|
+
|
|
153
|
+
`eventsToProcess` is used for messages consumed from the integration/fanout flow.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { EventProcessConfig, RabbitMQMessageDto } from "rbtmq-resilience";
|
|
157
|
+
|
|
158
|
+
export const eventsToProcess: EventProcessConfig[] = [
|
|
159
|
+
{
|
|
160
|
+
eventType: "commerce.orders.order_created",
|
|
161
|
+
processes: [
|
|
162
|
+
{
|
|
163
|
+
processName: "create-local-order",
|
|
164
|
+
processFunction: async (event: RabbitMQMessageDto) => {
|
|
165
|
+
const payload = JSON.parse(event.content.toString("utf8"));
|
|
166
|
+
console.log(payload);
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The library stores each successful handler execution in `inbox`, using `message_id + handler_name`, so duplicate messages are not processed twice by the same handler.
|
|
175
|
+
|
|
176
|
+
## Register Tactical Topic Handlers
|
|
177
|
+
|
|
178
|
+
Use `tacticalEvents` when a service consumes topic events.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { EventProcessConfig, RabbitMQMessageDto } from "rbtmq-resilience";
|
|
182
|
+
|
|
183
|
+
export const tacticalEventsToProcess: EventProcessConfig[] = [
|
|
184
|
+
{
|
|
185
|
+
eventType: "commerce.orders.payment_authorized",
|
|
186
|
+
processes: [
|
|
187
|
+
{
|
|
188
|
+
processName: "confirm-checkout-payment",
|
|
189
|
+
processFunction: async (event: RabbitMQMessageDto) => {
|
|
190
|
+
const payload = JSON.parse(event.content.toString("utf8"));
|
|
191
|
+
console.log(payload);
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The tactical queue will have two useful bindings:
|
|
200
|
+
|
|
201
|
+
```txt
|
|
202
|
+
app.tactical commerce.orders.checkout.#
|
|
203
|
+
app.direct commerce.orders.checkout.tactical-events
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The `topic` binding is for business messages. The `direct` binding is internal and allows delayed retry queues to route failed messages back to the primary queue.
|
|
207
|
+
|
|
208
|
+
RabbitMQ also shows a default exchange binding for every queue. That is created automatically by RabbitMQ and can be ignored.
|
|
209
|
+
|
|
210
|
+
## Configure the Runtime
|
|
211
|
+
|
|
212
|
+
Create a RabbitMQ module in your service, for example `src/infrastructure/rabbitmq/rabbitmq.ts`.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import env from "@/shared/env";
|
|
216
|
+
import {
|
|
217
|
+
QueueNamePartsDto,
|
|
218
|
+
RabbitMQResilience,
|
|
219
|
+
RbtmqConsumerConfigDto,
|
|
220
|
+
RbtmqNaming,
|
|
221
|
+
RbtmqResilienceConfig,
|
|
222
|
+
} from "rbtmq-resilience";
|
|
223
|
+
import { eventsToProcess } from "./eventsToProcess";
|
|
224
|
+
import { tacticalEventsToProcess } from "./tacticalEventsToProcess";
|
|
225
|
+
|
|
226
|
+
const eventNames = RbtmqNaming.queueNames(
|
|
227
|
+
new QueueNamePartsDto(
|
|
228
|
+
env.RABBIT_DOMAIN,
|
|
229
|
+
env.RABBIT_SUBDOMAIN,
|
|
230
|
+
env.RABBIT_BOUNDED_CONTEXT,
|
|
231
|
+
"events",
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const tacticalEventNames = RbtmqNaming.queueNames(
|
|
236
|
+
new QueueNamePartsDto(
|
|
237
|
+
env.RABBIT_DOMAIN,
|
|
238
|
+
env.RABBIT_SUBDOMAIN,
|
|
239
|
+
env.RABBIT_BOUNDED_CONTEXT,
|
|
240
|
+
"tactical-events",
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
export const rbtmqResilienceConfig: RbtmqResilienceConfig = {
|
|
245
|
+
rabbitMQConfigConnect: {
|
|
246
|
+
protocol: env.RABBIT_PROTOCOL,
|
|
247
|
+
hostname: env.RABBIT_HOSTNAME,
|
|
248
|
+
port: Number(env.RABBIT_PORT),
|
|
249
|
+
username: env.RABBIT_USERNAME,
|
|
250
|
+
password: env.RABBIT_PASSWORD,
|
|
251
|
+
vhost: env.RABBIT_VHOST,
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
queue: eventNames.queue,
|
|
255
|
+
routingKey: eventNames.routingKey,
|
|
256
|
+
exchange: env.RABBIT_EXCHANGE,
|
|
257
|
+
typeExchange: env.RABBIT_TYPE_EXCHANGE,
|
|
258
|
+
|
|
259
|
+
tacticalExchange: env.RABBIT_TACTICAL_EXCHANGE,
|
|
260
|
+
tacticalTypeExchange: env.RABBIT_TACTICAL_TYPE_EXCHANGE,
|
|
261
|
+
|
|
262
|
+
directExchange: env.RABBIT_DIRECT_EXCHANGE,
|
|
263
|
+
typeDirectExchange: env.RABBIT_TYPE_DIRECT_EXCHANGE,
|
|
264
|
+
|
|
265
|
+
prefetch: env.RABBIT_PREFETCH,
|
|
266
|
+
retryQueue: eventNames.retryQueue,
|
|
267
|
+
retryRoutingKey: eventNames.retryRoutingKey,
|
|
268
|
+
retryEndpoint: env.RABBIT_RETRY_ENDPOINT,
|
|
269
|
+
deadLetterQueue: env.RABBIT_DEAD_LETTER_QUEUE,
|
|
270
|
+
deadLetterRoutingKey: env.RABBIT_DEAD_LETTER_ROUTING_KEY,
|
|
271
|
+
messageTTL: env.RABBIT_MESSAGE_TTL,
|
|
272
|
+
|
|
273
|
+
eventResilienceHandlerConfig: {
|
|
274
|
+
immediateRetryAttempts: 5,
|
|
275
|
+
delayedRetryAttempts: 3,
|
|
276
|
+
delayInMs: 1000,
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
eventsToProcess,
|
|
280
|
+
|
|
281
|
+
tacticalEvents: new RbtmqConsumerConfigDto(
|
|
282
|
+
tacticalEventNames.queue,
|
|
283
|
+
"tacticalEvents",
|
|
284
|
+
tacticalEventNames.queue,
|
|
285
|
+
tacticalEventNames.routingKey,
|
|
286
|
+
tacticalEventNames.retryQueue,
|
|
287
|
+
tacticalEventNames.retryRoutingKey,
|
|
288
|
+
`${tacticalEventNames.queue}-error`,
|
|
289
|
+
`${tacticalEventNames.routingKey}-error`,
|
|
290
|
+
env.RABBIT_MESSAGE_TTL,
|
|
291
|
+
tacticalEventsToProcess,
|
|
292
|
+
env.RABBIT_PREFETCH,
|
|
293
|
+
env.RABBIT_TACTICAL_EXCHANGE,
|
|
294
|
+
RbtmqNaming.tacticalBindingPattern(
|
|
295
|
+
env.RABBIT_DOMAIN,
|
|
296
|
+
env.RABBIT_SUBDOMAIN,
|
|
297
|
+
env.RABBIT_BOUNDED_CONTEXT,
|
|
298
|
+
),
|
|
299
|
+
),
|
|
300
|
+
|
|
301
|
+
mysql: {
|
|
302
|
+
host: env.DB_HOST,
|
|
303
|
+
port: Number(env.DB_PORT),
|
|
304
|
+
user: env.DB_USER,
|
|
305
|
+
password: env.DB_PASSWORD,
|
|
306
|
+
database: env.DB_NAME,
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
hono: {
|
|
310
|
+
enabled: true,
|
|
311
|
+
port: env.RABBIT_HEALTH_PORT,
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export const RabbitMQR = RabbitMQResilience.initialize(rbtmqResilienceConfig);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
If a service does not need topic events, omit `tacticalEvents`. If it does not need commands, omit `commands`. The integration event consumer still uses the top-level `queue`, `retryQueue`, and `eventsToProcess` fields.
|
|
319
|
+
|
|
320
|
+
## Start the Runtime
|
|
321
|
+
|
|
322
|
+
Call `init()` during service startup.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { RabbitMQR } from "@/infrastructure/rabbitmq";
|
|
326
|
+
|
|
327
|
+
await RabbitMQR.init();
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`init()` will:
|
|
331
|
+
|
|
332
|
+
- Connect to MySQL.
|
|
333
|
+
- Create or verify `inbox` and `outbox`.
|
|
334
|
+
- Connect to RabbitMQ.
|
|
335
|
+
- Declare exchanges, queues, retry queues, error queues, and bindings.
|
|
336
|
+
- Start configured consumers.
|
|
337
|
+
- Start the outbox relay unless disabled.
|
|
338
|
+
- Start Hono health endpoints if enabled.
|
|
339
|
+
|
|
340
|
+
Use `stop()` during graceful shutdown:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
await RabbitMQR.stop();
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Publish Integration Events
|
|
347
|
+
|
|
348
|
+
Integration events are published to the configured `fanout` exchange.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { RabbitMQR } from "@/infrastructure/rabbitmq";
|
|
352
|
+
|
|
353
|
+
await RabbitMQR.publishEvent({
|
|
354
|
+
type: "commerce.orders.order_created",
|
|
355
|
+
payload: {
|
|
356
|
+
orderId: "order-123",
|
|
357
|
+
receivedAt: new Date().toISOString(),
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The routing key is optional for fanout exchanges because RabbitMQ ignores it for delivery.
|
|
363
|
+
|
|
364
|
+
The library also supports the legacy-style `RabbitMQMessageDto`:
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
import { RabbitMQMessageDto } from "rbtmq-resilience";
|
|
368
|
+
import {
|
|
369
|
+
MessageFieldsDto,
|
|
370
|
+
MessagePropertiesDto,
|
|
371
|
+
} from "rbtmq-resilience/domain/dtos/eventManager";
|
|
372
|
+
import { RabbitMQR, rbtmqResilienceConfig } from "@/infrastructure/rabbitmq";
|
|
373
|
+
|
|
374
|
+
const message = new RabbitMQMessageDto(
|
|
375
|
+
Buffer.from(JSON.stringify({ orderId: "order-123" })),
|
|
376
|
+
new MessageFieldsDto(
|
|
377
|
+
0,
|
|
378
|
+
false,
|
|
379
|
+
rbtmqResilienceConfig.exchange,
|
|
380
|
+
rbtmqResilienceConfig.routingKey,
|
|
381
|
+
),
|
|
382
|
+
new MessagePropertiesDto(
|
|
383
|
+
"application/json",
|
|
384
|
+
"utf-8",
|
|
385
|
+
undefined,
|
|
386
|
+
2,
|
|
387
|
+
undefined,
|
|
388
|
+
undefined,
|
|
389
|
+
undefined,
|
|
390
|
+
undefined,
|
|
391
|
+
undefined,
|
|
392
|
+
crypto.randomUUID(),
|
|
393
|
+
"commerce.orders.order_created",
|
|
394
|
+
undefined,
|
|
395
|
+
"commerce.orders",
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
await RabbitMQR.publishEvent(message);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Publish Tactical Topic Events
|
|
403
|
+
|
|
404
|
+
Topic events require a routing key.
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import { QueueNamePartsDto, RbtmqNaming } from "rbtmq-resilience";
|
|
408
|
+
import { RabbitMQR } from "@/infrastructure/rabbitmq";
|
|
409
|
+
|
|
410
|
+
await RabbitMQR.publishTacticalEvent({
|
|
411
|
+
type: "commerce.orders.payment_authorized",
|
|
412
|
+
routingKey: RbtmqNaming.tacticalRoutingKey(
|
|
413
|
+
new QueueNamePartsDto(
|
|
414
|
+
"commerce",
|
|
415
|
+
"orders",
|
|
416
|
+
"checkout",
|
|
417
|
+
),
|
|
418
|
+
),
|
|
419
|
+
payload: {
|
|
420
|
+
orderId: "order-123",
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
With the binding pattern `commerce.orders.checkout.#`, this message will be delivered to that service tactical queue.
|
|
426
|
+
|
|
427
|
+
## Send Commands
|
|
428
|
+
|
|
429
|
+
Commands use the `direct` exchange and must provide an exact routing key.
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
await RabbitMQR.sendCommand({
|
|
433
|
+
type: "commerce.orders.reserve_stock",
|
|
434
|
+
routingKey: "commerce.orders.checkout.commands",
|
|
435
|
+
payload: {
|
|
436
|
+
orderId: "order-123",
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Use commands when one specific service/queue should receive the message.
|
|
442
|
+
|
|
443
|
+
## Outbox
|
|
444
|
+
|
|
445
|
+
Use the outbox when your service must persist a message before publishing it.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
const messageId = await RabbitMQR.saveOutboxMessage({
|
|
449
|
+
type: "commerce.orders.order_created",
|
|
450
|
+
body: {
|
|
451
|
+
orderId: "order-123",
|
|
452
|
+
},
|
|
453
|
+
exchange: "app.integration",
|
|
454
|
+
routingKey: "",
|
|
455
|
+
headers: {
|
|
456
|
+
source: "checkout-service",
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The relay runs automatically unless disabled:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
outbox: {
|
|
465
|
+
enabled: true,
|
|
466
|
+
batchSize: 50,
|
|
467
|
+
pollingIntervalMs: 5000,
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
You can also dispatch manually:
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
const sent = await RabbitMQR.dispatchOutbox(50);
|
|
475
|
+
console.log(sent);
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## Database
|
|
479
|
+
|
|
480
|
+
The library uses MySQL through Drizzle ORM and creates/verifies two tables at startup:
|
|
481
|
+
|
|
482
|
+
```txt
|
|
483
|
+
inbox
|
|
484
|
+
outbox
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
`inbox` is used for idempotent consumption. `outbox` is used for reliable publishing.
|
|
488
|
+
|
|
489
|
+
You can import the migration SQL if you want to include it in your own migration flow:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
import { mysqlMigration } from "rbtmq-resilience";
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
A service may already have its own database configuration. That is fine: pass the same MySQL connection settings, or pass a Drizzle database instance through `db`. The library only manages its own `inbox` and `outbox` tables.
|
|
496
|
+
|
|
497
|
+
## Health and Metrics
|
|
498
|
+
|
|
499
|
+
When Hono is enabled:
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
hono: {
|
|
503
|
+
enabled: true,
|
|
504
|
+
port: 3002,
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
The library exposes:
|
|
509
|
+
|
|
510
|
+
```txt
|
|
511
|
+
GET /health
|
|
512
|
+
GET /ready
|
|
513
|
+
GET /metrics/resilience
|
|
514
|
+
GET /metrics/resilience/failed
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Use a port different from your HTTP API port.
|
|
518
|
+
|
|
519
|
+
## Retry Behavior
|
|
520
|
+
|
|
521
|
+
For each message:
|
|
522
|
+
|
|
523
|
+
1. The library reads `message_id` and `type` from AMQP properties or headers.
|
|
524
|
+
2. The message body is parsed as JSON.
|
|
525
|
+
3. Matching handlers are selected by `eventType`.
|
|
526
|
+
4. `inbox` is checked before each handler.
|
|
527
|
+
5. The handler runs with immediate retries.
|
|
528
|
+
6. If it succeeds, `inbox` is marked and the message is acknowledged.
|
|
529
|
+
7. If it fails and delayed attempts remain, the message is sent to the retry queue.
|
|
530
|
+
8. If validation fails or attempts are exhausted, it is sent to the error queue.
|
|
531
|
+
|
|
532
|
+
The message is acknowledged only after success or after it has been safely published to retry/error. If publishing to retry/error fails, the message is nacked and requeued.
|
|
533
|
+
|
|
534
|
+
## Troubleshooting
|
|
535
|
+
|
|
536
|
+
**PRECONDITION_FAILED - inequivalent arg 'type' for exchange**
|
|
537
|
+
|
|
538
|
+
RabbitMQ already has an exchange with that name but a different type.
|
|
539
|
+
|
|
540
|
+
Example:
|
|
541
|
+
|
|
542
|
+
```txt
|
|
543
|
+
received 'topic' but current is 'fanout'
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Fix it by using different exchange names:
|
|
547
|
+
|
|
548
|
+
```env
|
|
549
|
+
RABBIT_EXCHANGE=app.integration
|
|
550
|
+
RABBIT_TYPE_EXCHANGE=fanout
|
|
551
|
+
|
|
552
|
+
RABBIT_TACTICAL_EXCHANGE=app.tactical
|
|
553
|
+
RABBIT_TACTICAL_TYPE_EXCHANGE=topic
|
|
554
|
+
|
|
555
|
+
RABBIT_DIRECT_EXCHANGE=app.direct
|
|
556
|
+
RABBIT_TYPE_DIRECT_EXCHANGE=direct
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
or delete/recreate the exchange in RabbitMQ if the existing type is wrong.
|
|
560
|
+
|
|
561
|
+
**Why does my queue have two bindings?**
|
|
562
|
+
|
|
563
|
+
That is expected.
|
|
564
|
+
|
|
565
|
+
```txt
|
|
566
|
+
tactical exchange topic routing for business messages
|
|
567
|
+
direct exchange internal routing for retry/error flow
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
RabbitMQ also creates a default exchange binding automatically for every queue.
|
|
571
|
+
|
|
572
|
+
**EADDRINUSE on startup**
|
|
573
|
+
|
|
574
|
+
Your service API and the library health server are trying to use the same port. Configure a separate health port:
|
|
575
|
+
|
|
576
|
+
```env
|
|
577
|
+
PORT=3001
|
|
578
|
+
RABBIT_HEALTH_PORT=3002
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
hono: {
|
|
583
|
+
enabled: true,
|
|
584
|
+
port: env.RABBIT_HEALTH_PORT,
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**pnpm minimum release age blocks alpha versions**
|
|
589
|
+
|
|
590
|
+
If your workspace uses `minimumReleaseAge`, allow this package explicitly:
|
|
591
|
+
|
|
592
|
+
```yaml
|
|
593
|
+
minimumReleaseAgeExclude:
|
|
594
|
+
- rbtmq-resilience
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
## Build
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
bun run typecheck
|
|
601
|
+
bun run build
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Before publishing:
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
npm pack --dry-run
|
|
608
|
+
npm publish --tag alpha
|
|
609
|
+
```
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RbtmqResilienceConfigDto as RbtmqResilienceConfig, RbtmqResilienceRuntimeConfigDto as RbtmqResilienceRuntimeConfig } from "../domain/dtos/config.dto";
|
|
2
|
+
export declare function isServiceConfig(config: RbtmqResilienceConfig | RbtmqResilienceRuntimeConfig): config is RbtmqResilienceConfig;
|
|
3
|
+
export declare function toRuntimeConfig(config: RbtmqResilienceConfig): RbtmqResilienceRuntimeConfig;
|
|
4
|
+
//# sourceMappingURL=configMapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configMapper.d.ts","sourceRoot":"","sources":["../../src/application/configMapper.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAKV,wBAAwB,IAAI,qBAAqB,EACjD,+BAA+B,IAAI,4BAA4B,EAEhE,MAAM,2BAA2B,CAAC;AAEnC,wBAAgB,eAAe,CAAC,MAAM,EAAE,qBAAqB,GAAG,4BAA4B,GAAG,MAAM,IAAI,qBAAqB,CAE7H;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,qBAAqB,GAAG,4BAA4B,CAkD3F"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FailedMessageDto } from "../domain/dtos/metrics.dto";
|
|
2
|
+
import type { HandlerFailureDto, ParsedMessageDto } from "../domain/dtos/messageProcessing.dto";
|
|
3
|
+
export declare class FailedMessageMonitor {
|
|
4
|
+
private readonly maxMessages;
|
|
5
|
+
private readonly messages;
|
|
6
|
+
constructor(maxMessages?: number);
|
|
7
|
+
record(parsed: ParsedMessageDto, consumerName: string, failures: HandlerFailureDto[]): void;
|
|
8
|
+
list(): FailedMessageDto[];
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=failedMessageMonitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failedMessageMonitor.d.ts","sourceRoot":"","sources":["../../src/application/failedMessageMonitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAEhG,qBAAa,oBAAoB;IAGnB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAFxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0B;gBAEtB,WAAW,SAAM;IAEvC,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAiB3F,IAAI,IAAI,gBAAgB,EAAE;CAGlC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { RabbitMQMessageDto } from "../domain/dtos/eventManager.dto";
|
|
2
|
+
import type { RbtmqResilienceConfigDto as RbtmqResilienceConfig, RbtmqResilienceRuntimeConfigDto as RbtmqResilienceRuntimeConfig } from "../domain/dtos/config.dto";
|
|
3
|
+
import { ResilienceMetricsDto } from "../domain/dtos/metrics.dto";
|
|
4
|
+
import type { OutboxInputDto as OutboxInput, PublishEnvelopeDto as PublishEnvelope } from "../domain/dtos/publish.dto";
|
|
5
|
+
export declare class RabbitMQResilience {
|
|
6
|
+
private connection?;
|
|
7
|
+
private channel?;
|
|
8
|
+
private db?;
|
|
9
|
+
private inboxRepository?;
|
|
10
|
+
private outboxRepository?;
|
|
11
|
+
private readonly databaseService;
|
|
12
|
+
private readonly rabbitMQService;
|
|
13
|
+
private readonly healthServer;
|
|
14
|
+
private readonly failedMessageMonitor;
|
|
15
|
+
private outboxRelayTimer?;
|
|
16
|
+
private readonly metrics;
|
|
17
|
+
private readonly config;
|
|
18
|
+
constructor(config: RbtmqResilienceConfig | RbtmqResilienceRuntimeConfig);
|
|
19
|
+
static initialize(config: RbtmqResilienceConfig | RbtmqResilienceRuntimeConfig): RabbitMQResilience;
|
|
20
|
+
init(): Promise<void>;
|
|
21
|
+
start(): Promise<void>;
|
|
22
|
+
stop(): Promise<void>;
|
|
23
|
+
declareTopology(): Promise<void>;
|
|
24
|
+
publishEvent(envelope: PublishEnvelope | RabbitMQMessageDto): Promise<string>;
|
|
25
|
+
publishTacticalEvent(envelope: PublishEnvelope): Promise<string>;
|
|
26
|
+
sendCommand(envelope: PublishEnvelope): Promise<string>;
|
|
27
|
+
saveOutboxMessage(input: OutboxInput): Promise<string>;
|
|
28
|
+
dispatchOutbox(batchSize?: number): Promise<number>;
|
|
29
|
+
getMetrics(): ResilienceMetricsDto;
|
|
30
|
+
isReady(): Promise<boolean>;
|
|
31
|
+
private ensureDatabase;
|
|
32
|
+
private ensureChannel;
|
|
33
|
+
private startConsumers;
|
|
34
|
+
private startConsumer;
|
|
35
|
+
private processMessage;
|
|
36
|
+
private parseMessage;
|
|
37
|
+
private runWithImmediateRetries;
|
|
38
|
+
private requireOutboxRepository;
|
|
39
|
+
private startOutboxRelay;
|
|
40
|
+
private startHono;
|
|
41
|
+
}
|
|
42
|
+
export declare function createRabbitMqResilience(config: RbtmqResilienceConfig | RbtmqResilienceRuntimeConfig): RabbitMQResilience;
|
|
43
|
+
export declare const RabbitMqResilience: typeof RabbitMQResilience;
|
|
44
|
+
//# sourceMappingURL=resilience.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilience.d.ts","sourceRoot":"","sources":["../../src/application/resilience.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AASrE,OAAO,KAAK,EAEV,wBAAwB,IAAI,qBAAqB,EACjD,+BAA+B,IAAI,4BAA4B,EAChE,MAAM,2BAA2B,CAAC;AAKnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EACV,cAAc,IAAI,WAAW,EAC7B,kBAAkB,IAAI,eAAe,EACtC,MAAM,4BAA4B,CAAC;AAUpC,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,UAAU,CAAC,CAAe;IAClC,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,EAAE,CAAC,CAAqB;IAChC,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAA8B;IACnE,OAAO,CAAC,gBAAgB,CAAC,CAAiC;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IAEtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;gBAE1C,MAAM,EAAE,qBAAqB,GAAG,4BAA4B;WAgB1D,UAAU,CAAC,MAAM,EAAE,qBAAqB,GAAG,4BAA4B,GAAG,kBAAkB;IAI7F,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAStB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAarB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAIhC,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAI7E,oBAAoB,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAIvD,iBAAiB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAItD,cAAc,CAAC,SAAS,SAAuC,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BvF,UAAU,IAAI,oBAAoB;IAW5B,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;YAU1B,cAAc;YAUd,aAAa;YAwBb,cAAc;YAMd,aAAa;YASb,cAAc;IAiG5B,OAAO,CAAC,YAAY;YAmCN,uBAAuB;IAarC,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,gBAAgB;IAexB,OAAO,CAAC,SAAS;CAGlB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,qBAAqB,GAAG,4BAA4B,GAAG,kBAAkB,CAEzH;AAED,eAAO,MAAM,kBAAkB,2BAAqB,CAAC"}
|