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.
Files changed (52) hide show
  1. package/README.md +609 -0
  2. package/dist/application/configMapper.d.ts +4 -0
  3. package/dist/application/configMapper.d.ts.map +1 -0
  4. package/dist/application/failedMessageMonitor.d.ts +10 -0
  5. package/dist/application/failedMessageMonitor.d.ts.map +1 -0
  6. package/dist/application/resilience.d.ts +44 -0
  7. package/dist/application/resilience.d.ts.map +1 -0
  8. package/dist/domain/dtos/config.dto.d.ts +177 -0
  9. package/dist/domain/dtos/config.dto.d.ts.map +1 -0
  10. package/dist/domain/dtos/eventManager.cjs +131 -0
  11. package/dist/domain/dtos/eventManager.d.ts +2 -0
  12. package/dist/domain/dtos/eventManager.d.ts.map +1 -0
  13. package/dist/domain/dtos/eventManager.dto.d.ts +32 -0
  14. package/dist/domain/dtos/eventManager.dto.d.ts.map +1 -0
  15. package/dist/domain/dtos/eventManager.mjs +98 -0
  16. package/dist/domain/dtos/handler.dto.d.ts +23 -0
  17. package/dist/domain/dtos/handler.dto.d.ts.map +1 -0
  18. package/dist/domain/dtos/messageProcessing.dto.d.ts +19 -0
  19. package/dist/domain/dtos/messageProcessing.dto.d.ts.map +1 -0
  20. package/dist/domain/dtos/metrics.dto.d.ts +19 -0
  21. package/dist/domain/dtos/metrics.dto.d.ts.map +1 -0
  22. package/dist/domain/dtos/naming.dto.d.ts +15 -0
  23. package/dist/domain/dtos/naming.dto.d.ts.map +1 -0
  24. package/dist/domain/dtos/publish.dto.d.ts +23 -0
  25. package/dist/domain/dtos/publish.dto.d.ts.map +1 -0
  26. package/dist/index.cjs +38081 -0
  27. package/dist/index.d.ts +16 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.mjs +38048 -0
  30. package/dist/infrastructure/database/database.service.d.ts +13 -0
  31. package/dist/infrastructure/database/database.service.d.ts.map +1 -0
  32. package/dist/infrastructure/database/migrations.d.ts +2 -0
  33. package/dist/infrastructure/database/migrations.d.ts.map +1 -0
  34. package/dist/infrastructure/database/repositories.d.ts +19 -0
  35. package/dist/infrastructure/database/repositories.d.ts.map +1 -0
  36. package/dist/infrastructure/database/schema.d.ts +256 -0
  37. package/dist/infrastructure/database/schema.d.ts.map +1 -0
  38. package/dist/infrastructure/rabbitmq/rabbitmq.service.d.ts +22 -0
  39. package/dist/infrastructure/rabbitmq/rabbitmq.service.d.ts.map +1 -0
  40. package/dist/presentation/healthServer.d.ts +13 -0
  41. package/dist/presentation/healthServer.d.ts.map +1 -0
  42. package/dist/shared/constants.d.ts +31 -0
  43. package/dist/shared/constants.d.ts.map +1 -0
  44. package/dist/shared/naming.d.ts +10 -0
  45. package/dist/shared/naming.d.ts.map +1 -0
  46. package/dist/shared/primitives.d.ts +5 -0
  47. package/dist/shared/primitives.d.ts.map +1 -0
  48. package/dist/shared/types.d.ts +10 -0
  49. package/dist/shared/types.d.ts.map +1 -0
  50. package/dist/shared/utils.d.ts +11 -0
  51. package/dist/shared/utils.d.ts.map +1 -0
  52. 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"}