@zdavison/matador 2.0.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 (256) hide show
  1. package/cli.ts +453 -0
  2. package/dist/checkpoint/context.d.ts +59 -0
  3. package/dist/checkpoint/context.d.ts.map +1 -0
  4. package/dist/checkpoint/context.js +140 -0
  5. package/dist/checkpoint/context.test.d.ts +2 -0
  6. package/dist/checkpoint/context.test.d.ts.map +1 -0
  7. package/dist/checkpoint/context.test.js +424 -0
  8. package/dist/checkpoint/index.d.ts +7 -0
  9. package/dist/checkpoint/index.d.ts.map +1 -0
  10. package/dist/checkpoint/index.js +6 -0
  11. package/dist/checkpoint/stores/memory.d.ts +29 -0
  12. package/dist/checkpoint/stores/memory.d.ts.map +1 -0
  13. package/dist/checkpoint/stores/memory.js +39 -0
  14. package/dist/checkpoint/stores/noop.d.ts +14 -0
  15. package/dist/checkpoint/stores/noop.d.ts.map +1 -0
  16. package/dist/checkpoint/stores/noop.js +18 -0
  17. package/dist/checkpoint/stores/stores.test.d.ts +2 -0
  18. package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
  19. package/dist/checkpoint/stores/stores.test.js +146 -0
  20. package/dist/checkpoint/types.d.ts +119 -0
  21. package/dist/checkpoint/types.d.ts.map +1 -0
  22. package/dist/checkpoint/types.js +1 -0
  23. package/dist/codec/codec.d.ts +29 -0
  24. package/dist/codec/codec.d.ts.map +1 -0
  25. package/dist/codec/codec.js +15 -0
  26. package/dist/codec/header-aware-codec.d.ts +36 -0
  27. package/dist/codec/header-aware-codec.d.ts.map +1 -0
  28. package/dist/codec/header-aware-codec.js +1 -0
  29. package/dist/codec/index.d.ts +6 -0
  30. package/dist/codec/index.d.ts.map +1 -0
  31. package/dist/codec/index.js +3 -0
  32. package/dist/codec/json-codec.d.ts +13 -0
  33. package/dist/codec/json-codec.d.ts.map +1 -0
  34. package/dist/codec/json-codec.js +64 -0
  35. package/dist/codec/rabbitmq-codec.d.ts +28 -0
  36. package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
  37. package/dist/codec/rabbitmq-codec.js +242 -0
  38. package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
  39. package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
  40. package/dist/codec/rabbitmq-codec.test.js +433 -0
  41. package/dist/core/fanout.d.ts +59 -0
  42. package/dist/core/fanout.d.ts.map +1 -0
  43. package/dist/core/fanout.js +121 -0
  44. package/dist/core/fanout.test.d.ts +2 -0
  45. package/dist/core/fanout.test.d.ts.map +1 -0
  46. package/dist/core/fanout.test.js +1055 -0
  47. package/dist/core/index.d.ts +7 -0
  48. package/dist/core/index.d.ts.map +1 -0
  49. package/dist/core/index.js +3 -0
  50. package/dist/core/matador.d.ts +133 -0
  51. package/dist/core/matador.d.ts.map +1 -0
  52. package/dist/core/matador.js +222 -0
  53. package/dist/core/matador.test.d.ts +2 -0
  54. package/dist/core/matador.test.d.ts.map +1 -0
  55. package/dist/core/matador.test.js +446 -0
  56. package/dist/core/shutdown.d.ts +78 -0
  57. package/dist/core/shutdown.d.ts.map +1 -0
  58. package/dist/core/shutdown.js +111 -0
  59. package/dist/core/shutdown.test.d.ts +2 -0
  60. package/dist/core/shutdown.test.d.ts.map +1 -0
  61. package/dist/core/shutdown.test.js +524 -0
  62. package/dist/errors/checkpoint-errors.d.ts +30 -0
  63. package/dist/errors/checkpoint-errors.d.ts.map +1 -0
  64. package/dist/errors/checkpoint-errors.js +49 -0
  65. package/dist/errors/has-description.d.ts +18 -0
  66. package/dist/errors/has-description.d.ts.map +1 -0
  67. package/dist/errors/has-description.js +9 -0
  68. package/dist/errors/index.d.ts +6 -0
  69. package/dist/errors/index.d.ts.map +1 -0
  70. package/dist/errors/index.js +22 -0
  71. package/dist/errors/matador-errors.d.ts +183 -0
  72. package/dist/errors/matador-errors.d.ts.map +1 -0
  73. package/dist/errors/matador-errors.js +376 -0
  74. package/dist/errors/retry-errors.d.ts +67 -0
  75. package/dist/errors/retry-errors.d.ts.map +1 -0
  76. package/dist/errors/retry-errors.js +108 -0
  77. package/dist/hooks/index.d.ts +4 -0
  78. package/dist/hooks/index.d.ts.map +1 -0
  79. package/dist/hooks/index.js +2 -0
  80. package/dist/hooks/safe-hooks.d.ts +34 -0
  81. package/dist/hooks/safe-hooks.d.ts.map +1 -0
  82. package/dist/hooks/safe-hooks.js +135 -0
  83. package/dist/hooks/types.d.ts +189 -0
  84. package/dist/hooks/types.d.ts.map +1 -0
  85. package/dist/hooks/types.js +9 -0
  86. package/dist/index.cjs +3156 -0
  87. package/dist/index.cjs.map +1 -0
  88. package/dist/index.d.cts +23 -0
  89. package/dist/index.d.ts +23 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +31 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/pipeline/index.d.ts +3 -0
  94. package/dist/pipeline/index.d.ts.map +1 -0
  95. package/dist/pipeline/index.js +1 -0
  96. package/dist/pipeline/pipeline.d.ts +60 -0
  97. package/dist/pipeline/pipeline.d.ts.map +1 -0
  98. package/dist/pipeline/pipeline.js +261 -0
  99. package/dist/pipeline/pipeline.test.d.ts +2 -0
  100. package/dist/pipeline/pipeline.test.d.ts.map +1 -0
  101. package/dist/pipeline/pipeline.test.js +1065 -0
  102. package/dist/retry/index.d.ts +4 -0
  103. package/dist/retry/index.d.ts.map +1 -0
  104. package/dist/retry/index.js +1 -0
  105. package/dist/retry/policy.d.ts +43 -0
  106. package/dist/retry/policy.d.ts.map +1 -0
  107. package/dist/retry/policy.js +1 -0
  108. package/dist/retry/standard-policy.d.ts +44 -0
  109. package/dist/retry/standard-policy.d.ts.map +1 -0
  110. package/dist/retry/standard-policy.js +102 -0
  111. package/dist/retry/standard-policy.test.d.ts +2 -0
  112. package/dist/retry/standard-policy.test.d.ts.map +1 -0
  113. package/dist/retry/standard-policy.test.js +190 -0
  114. package/dist/schema/index.d.ts +4 -0
  115. package/dist/schema/index.d.ts.map +1 -0
  116. package/dist/schema/index.js +2 -0
  117. package/dist/schema/registry.d.ts +63 -0
  118. package/dist/schema/registry.d.ts.map +1 -0
  119. package/dist/schema/registry.js +171 -0
  120. package/dist/schema/registry.test.d.ts +2 -0
  121. package/dist/schema/registry.test.d.ts.map +1 -0
  122. package/dist/schema/registry.test.js +278 -0
  123. package/dist/schema/types.d.ts +158 -0
  124. package/dist/schema/types.d.ts.map +1 -0
  125. package/dist/schema/types.js +74 -0
  126. package/dist/schema/types.test.d.ts +2 -0
  127. package/dist/schema/types.test.d.ts.map +1 -0
  128. package/dist/schema/types.test.js +243 -0
  129. package/dist/topology/builder.d.ts +66 -0
  130. package/dist/topology/builder.d.ts.map +1 -0
  131. package/dist/topology/builder.js +156 -0
  132. package/dist/topology/builder.test.d.ts +2 -0
  133. package/dist/topology/builder.test.d.ts.map +1 -0
  134. package/dist/topology/builder.test.js +222 -0
  135. package/dist/topology/index.d.ts +5 -0
  136. package/dist/topology/index.d.ts.map +1 -0
  137. package/dist/topology/index.js +2 -0
  138. package/dist/topology/types.d.ts +76 -0
  139. package/dist/topology/types.d.ts.map +1 -0
  140. package/dist/topology/types.js +18 -0
  141. package/dist/transport/capabilities.d.ts +66 -0
  142. package/dist/transport/capabilities.d.ts.map +1 -0
  143. package/dist/transport/capabilities.js +18 -0
  144. package/dist/transport/connection-manager.d.ts +95 -0
  145. package/dist/transport/connection-manager.d.ts.map +1 -0
  146. package/dist/transport/connection-manager.js +144 -0
  147. package/dist/transport/index.d.ts +11 -0
  148. package/dist/transport/index.d.ts.map +1 -0
  149. package/dist/transport/index.js +5 -0
  150. package/dist/transport/local/local-transport.d.ts +62 -0
  151. package/dist/transport/local/local-transport.d.ts.map +1 -0
  152. package/dist/transport/local/local-transport.js +241 -0
  153. package/dist/transport/local/local-transport.test.d.ts +2 -0
  154. package/dist/transport/local/local-transport.test.d.ts.map +1 -0
  155. package/dist/transport/local/local-transport.test.js +192 -0
  156. package/dist/transport/multi/multi-transport.d.ts +94 -0
  157. package/dist/transport/multi/multi-transport.d.ts.map +1 -0
  158. package/dist/transport/multi/multi-transport.js +184 -0
  159. package/dist/transport/multi/multi-transport.test.d.ts +2 -0
  160. package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
  161. package/dist/transport/multi/multi-transport.test.js +236 -0
  162. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +73 -0
  163. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
  164. package/dist/transport/rabbitmq/rabbitmq-transport.js +518 -0
  165. package/dist/transport/transport.d.ts +152 -0
  166. package/dist/transport/transport.d.ts.map +1 -0
  167. package/dist/transport/transport.js +1 -0
  168. package/dist/types/common.d.ts +41 -0
  169. package/dist/types/common.d.ts.map +1 -0
  170. package/dist/types/common.js +12 -0
  171. package/dist/types/envelope.d.ts +82 -0
  172. package/dist/types/envelope.d.ts.map +1 -0
  173. package/dist/types/envelope.js +36 -0
  174. package/dist/types/event.d.ts +96 -0
  175. package/dist/types/event.d.ts.map +1 -0
  176. package/dist/types/event.js +26 -0
  177. package/dist/types/event.test.d.ts +2 -0
  178. package/dist/types/event.test.d.ts.map +1 -0
  179. package/dist/types/event.test.js +130 -0
  180. package/dist/types/index.d.ts +9 -0
  181. package/dist/types/index.d.ts.map +1 -0
  182. package/dist/types/index.js +4 -0
  183. package/dist/types/subscriber.d.ts +207 -0
  184. package/dist/types/subscriber.d.ts.map +1 -0
  185. package/dist/types/subscriber.js +99 -0
  186. package/examples/config.ts +126 -0
  187. package/examples/event.ts +26 -0
  188. package/examples/order-event.json +19 -0
  189. package/package.json +66 -0
  190. package/src/checkpoint/context.test.ts +510 -0
  191. package/src/checkpoint/context.ts +213 -0
  192. package/src/checkpoint/index.ts +30 -0
  193. package/src/checkpoint/stores/memory.ts +47 -0
  194. package/src/checkpoint/stores/noop.ts +22 -0
  195. package/src/checkpoint/stores/stores.test.ts +177 -0
  196. package/src/checkpoint/types.ts +147 -0
  197. package/src/codec/codec.ts +42 -0
  198. package/src/codec/header-aware-codec.ts +41 -0
  199. package/src/codec/index.ts +11 -0
  200. package/src/codec/json-codec.ts +69 -0
  201. package/src/codec/rabbitmq-codec.test.ts +516 -0
  202. package/src/codec/rabbitmq-codec.ts +336 -0
  203. package/src/core/fanout.test.ts +1351 -0
  204. package/src/core/fanout.ts +184 -0
  205. package/src/core/index.ts +12 -0
  206. package/src/core/matador.test.ts +575 -0
  207. package/src/core/matador.ts +344 -0
  208. package/src/core/shutdown.test.ts +853 -0
  209. package/src/core/shutdown.ts +165 -0
  210. package/src/errors/checkpoint-errors.ts +62 -0
  211. package/src/errors/has-description.ts +25 -0
  212. package/src/errors/index.ts +57 -0
  213. package/src/errors/matador-errors.ts +477 -0
  214. package/src/errors/retry-errors.ts +134 -0
  215. package/src/hooks/index.ts +15 -0
  216. package/src/hooks/safe-hooks.ts +223 -0
  217. package/src/hooks/types.ts +248 -0
  218. package/src/index.ts +231 -0
  219. package/src/pipeline/index.ts +2 -0
  220. package/src/pipeline/pipeline.test.ts +1317 -0
  221. package/src/pipeline/pipeline.ts +371 -0
  222. package/src/retry/index.ts +4 -0
  223. package/src/retry/policy.ts +46 -0
  224. package/src/retry/standard-policy.test.ts +282 -0
  225. package/src/retry/standard-policy.ts +156 -0
  226. package/src/schema/index.ts +16 -0
  227. package/src/schema/registry.test.ts +339 -0
  228. package/src/schema/registry.ts +226 -0
  229. package/src/schema/types.test.ts +281 -0
  230. package/src/schema/types.ts +217 -0
  231. package/src/topology/builder.test.ts +275 -0
  232. package/src/topology/builder.ts +199 -0
  233. package/src/topology/index.ts +15 -0
  234. package/src/topology/types.ts +109 -0
  235. package/src/transport/capabilities.ts +88 -0
  236. package/src/transport/connection-manager.ts +218 -0
  237. package/src/transport/index.ts +42 -0
  238. package/src/transport/local/local-transport.test.ts +262 -0
  239. package/src/transport/local/local-transport.ts +327 -0
  240. package/src/transport/multi/multi-transport.test.ts +320 -0
  241. package/src/transport/multi/multi-transport.ts +294 -0
  242. package/src/transport/rabbitmq/rabbitmq-transport.ts +753 -0
  243. package/src/transport/transport.ts +200 -0
  244. package/src/types/common.ts +53 -0
  245. package/src/types/envelope.ts +152 -0
  246. package/src/types/event.test.ts +157 -0
  247. package/src/types/event.ts +118 -0
  248. package/src/types/index.ts +52 -0
  249. package/src/types/subscriber.ts +310 -0
  250. package/test/e2e/multi-transport.e2e.test.ts +236 -0
  251. package/test/e2e/rabbitmq-transport.e2e.test.ts +327 -0
  252. package/test/e2e/transport-compliance.e2e.test.ts +505 -0
  253. package/test/integration/matador.integration.test.ts +634 -0
  254. package/tsconfig.json +29 -0
  255. package/tsconfig.tsbuildinfo +1 -0
  256. package/tsup.config.ts +13 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,3156 @@
1
+ 'use strict';
2
+
3
+ var amqplib = require('amqplib');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var amqplib__default = /*#__PURE__*/_interopDefault(amqplib);
8
+
9
+ // src/errors/has-description.ts
10
+ function hasDescription(error) {
11
+ return typeof error === "object" && error !== null && "description" in error && typeof error.description === "string";
12
+ }
13
+
14
+ // src/errors/retry-errors.ts
15
+ var RetryControlError = class extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = this.constructor.name;
19
+ Object.defineProperty(this, "name", {
20
+ value: this.constructor.name,
21
+ enumerable: true,
22
+ configurable: false,
23
+ writable: false
24
+ });
25
+ }
26
+ /**
27
+ * Returns a serializable representation for logging/monitoring.
28
+ */
29
+ toJSON() {
30
+ return {
31
+ name: this.name,
32
+ message: this.message,
33
+ description: this.description,
34
+ stack: this.stack
35
+ };
36
+ }
37
+ };
38
+ var DoRetry = class extends RetryControlError {
39
+ description = "A subscriber explicitly requested retry by throwing DoRetry. ACTION: Check the subscriber code to understand why retry was forced. This overrides the default retry behavior based on idempotency settings.";
40
+ constructor(message = "Forced retry requested") {
41
+ super(message);
42
+ }
43
+ };
44
+ var DontRetry = class extends RetryControlError {
45
+ description = "A subscriber explicitly prevented retry by throwing DontRetry. ACTION: Check the subscriber code to understand why retry was disabled. Typically used for permanent failures like invalid data or business rule violations. The message will be sent to the dead-letter queue for manual review.";
46
+ constructor(message = "Retry explicitly disabled") {
47
+ super(message);
48
+ }
49
+ };
50
+ var EventAssertionError = class extends Error {
51
+ description = "An event assertion failed, indicating a programming error or invariant violation. ACTION: Review the assertion failure message to identify the bug in the event payload or subscriber logic. These errors are never retried and go directly to the dead-letter queue.";
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = "EventAssertionError";
55
+ Object.defineProperty(this, "name", {
56
+ value: "EventAssertionError",
57
+ enumerable: true,
58
+ configurable: false,
59
+ writable: false
60
+ });
61
+ }
62
+ toJSON() {
63
+ return {
64
+ name: this.name,
65
+ message: this.message,
66
+ description: this.description,
67
+ stack: this.stack
68
+ };
69
+ }
70
+ };
71
+ function isDoRetry(error) {
72
+ return error instanceof DoRetry;
73
+ }
74
+ function isDontRetry(error) {
75
+ return error instanceof DontRetry;
76
+ }
77
+ function isAssertionError(error) {
78
+ return error instanceof EventAssertionError;
79
+ }
80
+
81
+ // src/errors/matador-errors.ts
82
+ var MatadorError = class extends Error {
83
+ constructor(message) {
84
+ super(message);
85
+ this.name = this.constructor.name;
86
+ Object.defineProperty(this, "name", {
87
+ value: this.constructor.name,
88
+ enumerable: true,
89
+ configurable: false,
90
+ writable: false
91
+ });
92
+ }
93
+ /**
94
+ * Returns a serializable representation for logging/monitoring.
95
+ */
96
+ toJSON() {
97
+ return {
98
+ name: this.name,
99
+ message: this.message,
100
+ description: this.description,
101
+ stack: this.stack
102
+ };
103
+ }
104
+ };
105
+ var NotStartedError = class extends MatadorError {
106
+ description = "Matador has not been started. ACTION: Call matador.start() before dispatching events or performing other operations. Ensure start() completes successfully before using other methods.";
107
+ constructor(operation = "operation") {
108
+ super(
109
+ `Cannot perform ${operation}: Matador has not been started. Call start() first.`
110
+ );
111
+ }
112
+ };
113
+ var ShutdownInProgressError = class extends MatadorError {
114
+ description = "Matador is shutting down and will not accept new events. ACTION: Do not dispatch events after calling shutdown(). If you need to send events, do so before initiating shutdown. Consider implementing a pre-shutdown event flush if needed.";
115
+ constructor() {
116
+ super("Cannot dispatch events: Matador is shutting down.");
117
+ }
118
+ };
119
+ var TransportNotConnectedError = class extends MatadorError {
120
+ constructor(transportName, operation = "operation") {
121
+ super(
122
+ `Cannot perform ${operation}: Transport "${transportName}" is not connected. Ensure connect() was called and the broker is accessible.`
123
+ );
124
+ this.transportName = transportName;
125
+ }
126
+ description = "The transport is not connected to the message broker. ACTION: Ensure the transport is connected by calling transport.connect() or matador.start(). Check that the broker (e.g., RabbitMQ) is running and accessible. Verify connection settings (URL, credentials, network access).";
127
+ };
128
+ var TransportClosedError = class extends MatadorError {
129
+ constructor(transportName) {
130
+ super(
131
+ `Transport "${transportName}" has been closed and will not accept new operations.`
132
+ );
133
+ this.transportName = transportName;
134
+ }
135
+ description = "The transport has been closed and will not accept new operations. ACTION: This typically occurs during application shutdown. If unexpected, check for early shutdown triggers. Events sent after transport closure will be lost.";
136
+ };
137
+ var AllTransportsFailedError = class extends MatadorError {
138
+ constructor(queue, errors) {
139
+ super(
140
+ `All transports failed to send message to queue "${queue}". Errors: ${errors.map((e) => e.message).join("; ")}`
141
+ );
142
+ this.queue = queue;
143
+ this.errors = errors;
144
+ }
145
+ description = "All transports failed to send the message. ACTION: Check the health of all configured transports (primary and fallbacks). Review the errors array for specific failure reasons. Ensure at least one transport is properly configured and reachable. Consider adding a LocalTransport as a last-resort fallback.";
146
+ };
147
+ var TransportSendError = class extends MatadorError {
148
+ constructor(queue, cause) {
149
+ super(`Failed to send message to queue "${queue}": ${cause.message}`);
150
+ this.queue = queue;
151
+ this.cause = cause;
152
+ }
153
+ description = "Failed to send a message through the transport. ACTION: Check the underlying error for details. Common causes: (1) Transport disconnected during send, (2) Network issues between application and broker, (3) Broker rejected the message (size, permissions, queue limits). The message was NOT delivered and should be retried or logged.";
154
+ toJSON() {
155
+ return {
156
+ ...super.toJSON(),
157
+ queue: this.queue,
158
+ cause: {
159
+ name: this.cause.name,
160
+ message: this.cause.message
161
+ }
162
+ };
163
+ }
164
+ };
165
+ var DelayedMessagesNotSupportedError = class extends MatadorError {
166
+ constructor(transportName) {
167
+ super(
168
+ `Delayed messages require the RabbitMQ delayed message exchange plugin. Install rabbitmq_delayed_message_exchange or remove delayMs from event options.`
169
+ );
170
+ this.transportName = transportName;
171
+ }
172
+ description = "Delayed messages were requested but the transport does not support them. ACTION: For RabbitMQ, install the rabbitmq_delayed_message_exchange plugin. Run: rabbitmq-plugins enable rabbitmq_delayed_message_exchange Then restart RabbitMQ and reconnect. Alternatively, remove delayMs from your event options if delays are not required.";
173
+ };
174
+ var EventNotRegisteredError = class extends MatadorError {
175
+ constructor(eventKey) {
176
+ super(
177
+ `Event "${eventKey}" is not registered in schema. Register it using matador.register(EventClass, subscribers).`
178
+ );
179
+ this.eventKey = eventKey;
180
+ }
181
+ description = "The event type is not registered in the schema. ACTION: Register the event using matador.register(EventClass, subscribers) before dispatching. If this occurs during message consumption, it may indicate schema drift between services. Ensure all services have matching schema registrations for shared events.";
182
+ };
183
+ var SubscriberNotRegisteredError = class extends MatadorError {
184
+ constructor(subscriberName, eventKey) {
185
+ super(
186
+ `Subscriber "${subscriberName}" is not registered` + (eventKey ? ` for event "${eventKey}"` : "") + ". Check schema registration."
187
+ );
188
+ this.subscriberName = subscriberName;
189
+ this.eventKey = eventKey;
190
+ }
191
+ description = "The subscriber is not registered for this event in the schema. ACTION: Ensure the subscriber is included in the registration for this event. This may occur if: (1) The subscriber was removed from the schema but messages still exist, (2) Schema drift between producer and consumer services, (3) A deployment is in progress with different schema versions. Check the dead-letter queue for affected messages.";
192
+ };
193
+ var NoSubscribersExistError = class extends MatadorError {
194
+ constructor(eventKey) {
195
+ super(
196
+ `No subscribers registered for event "${eventKey}". Add subscribers using matador.register(EventClass, [subscriber1, subscriber2]).`
197
+ );
198
+ this.eventKey = eventKey;
199
+ }
200
+ description = "The event has no subscribers registered. ACTION: Register at least one subscriber for this event type. If subscribers were intentionally removed, consider also removing the event dispatch. Events without subscribers are not useful and may indicate configuration issues.";
201
+ };
202
+ var InvalidSchemaError = class extends MatadorError {
203
+ constructor(message, cause) {
204
+ super(`Invalid schema: ${message}` + (cause ? `. Cause: ${cause}` : ""));
205
+ this.cause = cause;
206
+ }
207
+ description = "The schema configuration is invalid. ACTION: Review the schema registration for issues. Common problems: (1) Duplicate subscriber names for the same event, (2) Missing required fields on event class (key, description), (3) Invalid alias configuration. Check the cause property for specific details.";
208
+ };
209
+ var SubscriberIsStubError = class extends MatadorError {
210
+ constructor(subscriberName) {
211
+ super(
212
+ `Subscriber "${subscriberName}" is a stub and cannot be processed locally. Replace with a full Subscriber implementation in the consumer schema.`
213
+ );
214
+ this.subscriberName = subscriberName;
215
+ }
216
+ description = "A SubscriberStub was registered in a consuming schema. ACTION: SubscriberStubs should only be used in producer schemas to declare that a subscriber exists in another service. In the consumer service, provide a full Subscriber with a callback function. Remove the stub from the consumer schema and add the actual implementation.";
217
+ };
218
+ var LocalTransportCannotProcessStubError = class extends MatadorError {
219
+ constructor(subscriberName) {
220
+ super(
221
+ `LocalTransport cannot process stub subscriber "${subscriberName}". Stub subscribers require a distributed transport like RabbitMQ.`
222
+ );
223
+ this.subscriberName = subscriberName;
224
+ }
225
+ description = "The LocalTransport cannot process events for SubscriberStubs. ACTION: SubscriberStubs represent remote implementations that only RabbitMQ can route. If using LocalTransport for testing, provide mock implementations instead of stubs. For production fallback scenarios, be aware that stub-targeted events will be dropped.";
226
+ };
227
+ var QueueNotFoundError = class extends MatadorError {
228
+ constructor(queueName) {
229
+ super(
230
+ `Queue "${queueName}" not found. Ensure it is defined in topology and applyTopology() was called.`
231
+ );
232
+ this.queueName = queueName;
233
+ }
234
+ description = "The specified queue does not exist or has not been created. ACTION: Ensure the queue is defined in the topology configuration. Call transport.applyTopology() or matador.start() to create queues. Check that the queue name matches the topology definition.";
235
+ };
236
+ var InvalidEventError = class extends MatadorError {
237
+ constructor(message, cause) {
238
+ super(`Invalid event: ${message}` + (cause ? `. Cause: ${cause}` : ""));
239
+ this.cause = cause;
240
+ }
241
+ description = "The event is invalid or missing required fields. ACTION: Ensure the event has all required properties. Common issues: missing targetSubscriber during processing, null/undefined data when the event type requires data, malformed event structure from codec decode failure.";
242
+ };
243
+ var MessageMaybePoisonedError = class extends MatadorError {
244
+ constructor(eventId, deliveryCount, maxDeliveries) {
245
+ super(
246
+ `Message "${eventId}" delivered ${deliveryCount} times (max: ${maxDeliveries}). Possible poison message - will not be retried.`
247
+ );
248
+ this.eventId = eventId;
249
+ this.deliveryCount = deliveryCount;
250
+ this.maxDeliveries = maxDeliveries;
251
+ }
252
+ description = "A message was redelivered multiple times without successful processing. This usually indicates the message causes a crash or timeout during processing. ACTION: (1) Check application logs for errors/crashes during message processing, (2) Inspect the message in the dead-letter queue for malformed data, (3) Review the subscriber code for bugs that cause crashes, (4) Consider increasing processing timeout if the operation is legitimately slow. This message will NOT be retried to prevent crash loops.";
253
+ };
254
+ var IdempotentMessageCannotRetryError = class extends MatadorError {
255
+ constructor(eventId, subscriberName) {
256
+ super(
257
+ `Non-idempotent subscriber "${subscriberName}" cannot retry redelivered message "${eventId}". Mark subscriber as idempotent='yes' or implement idempotency handling.`
258
+ );
259
+ this.eventId = eventId;
260
+ this.subscriberName = subscriberName;
261
+ }
262
+ description = "A non-idempotent subscriber received a redelivered message. Retrying would risk duplicate side effects (e.g., double payments, duplicate emails). ACTION: (1) Mark the subscriber as idempotent if it safely handles duplicates, (2) Implement idempotency keys in the subscriber logic, (3) Manually inspect and replay the message from the dead-letter queue after verification. The message will be sent to the dead-letter queue for manual review.";
263
+ };
264
+ var TimeoutError = class extends MatadorError {
265
+ constructor(operation, timeoutMs) {
266
+ super(`Operation "${operation}" timed out after ${timeoutMs}ms.`);
267
+ this.operation = operation;
268
+ this.timeoutMs = timeoutMs;
269
+ }
270
+ description = "An operation timed out before completing. ACTION: (1) Increase the timeout if the operation legitimately needs more time, (2) Optimize the operation to complete faster, (3) Check for deadlocks or blocking operations, (4) Verify external service dependencies are responsive.";
271
+ };
272
+ function isMatadorError(error) {
273
+ return error instanceof MatadorError;
274
+ }
275
+ function isNotStartedError(error) {
276
+ return error instanceof NotStartedError;
277
+ }
278
+ function isTransportNotConnectedError(error) {
279
+ return error instanceof TransportNotConnectedError;
280
+ }
281
+ function isEventNotRegisteredError(error) {
282
+ return error instanceof EventNotRegisteredError;
283
+ }
284
+ function isSubscriberNotRegisteredError(error) {
285
+ return error instanceof SubscriberNotRegisteredError;
286
+ }
287
+ function isMessageMaybePoisonedError(error) {
288
+ return error instanceof MessageMaybePoisonedError;
289
+ }
290
+ function isIdempotentMessageCannotRetryError(error) {
291
+ return error instanceof IdempotentMessageCannotRetryError;
292
+ }
293
+
294
+ // src/errors/checkpoint-errors.ts
295
+ var DuplicateIoKeyError = class extends MatadorError {
296
+ constructor(key, subscriberName) {
297
+ super(
298
+ `Duplicate io key "${key}" in subscriber "${subscriberName}". Each io() call must have a unique key.`
299
+ );
300
+ this.key = key;
301
+ this.subscriberName = subscriberName;
302
+ }
303
+ description = "The same io() key was used multiple times in a single subscriber execution. Each io() call must have a unique key to ensure correct checkpoint behavior. ACTION: Ensure all io() keys are unique within the subscriber. For dynamic operations (e.g., loops), include a unique identifier in the key (e.g., `process-item-${item.id}`).";
304
+ };
305
+ var CheckpointStoreError = class extends MatadorError {
306
+ constructor(operation, envelopeId, cause) {
307
+ super(
308
+ `Checkpoint store ${operation} failed for envelope "${envelopeId}": ${cause.message}`
309
+ );
310
+ this.operation = operation;
311
+ this.envelopeId = envelopeId;
312
+ this.cause = cause;
313
+ }
314
+ description = "A checkpoint store operation failed. This may cause issues with resumable subscribers. ACTION: Check the underlying storage system (Redis, etc.) for connectivity issues. The subscriber may re-execute operations that were already completed.";
315
+ };
316
+ function isDuplicateIoKeyError(error) {
317
+ return error instanceof DuplicateIoKeyError;
318
+ }
319
+ function isCheckpointStoreError(error) {
320
+ return error instanceof CheckpointStoreError;
321
+ }
322
+
323
+ // src/topology/types.ts
324
+ function getQualifiedQueueName(namespace, queueName) {
325
+ return `${namespace}.${queueName}`;
326
+ }
327
+ function getDeadLetterQueueName(namespace, queueName, dlqType) {
328
+ return `${namespace}.${queueName}.${dlqType}`;
329
+ }
330
+ function getRetryQueueName(namespace, queueName) {
331
+ return `${namespace}.${queueName}.retry`;
332
+ }
333
+
334
+ // src/topology/builder.ts
335
+ var TopologyValidationError = class extends Error {
336
+ constructor(message, issues) {
337
+ super(message);
338
+ this.issues = issues;
339
+ this.name = "TopologyValidationError";
340
+ }
341
+ description = "The topology configuration is invalid. Check the issues array for specific validation failures such as missing namespace, invalid queue names, or conflicting settings. This error occurs during Matador initialization and must be fixed in the configuration.";
342
+ };
343
+ var TopologyBuilder = class _TopologyBuilder {
344
+ /**
345
+ * Creates a new TopologyBuilder instance.
346
+ */
347
+ static create() {
348
+ return new _TopologyBuilder();
349
+ }
350
+ namespace = "";
351
+ queues = [];
352
+ deadLetter = {
353
+ unhandled: { enabled: true },
354
+ undeliverable: { enabled: true }
355
+ };
356
+ retry = {
357
+ enabled: true,
358
+ defaultDelayMs: 1e3,
359
+ maxDelayMs: 3e5
360
+ // 5 minutes
361
+ };
362
+ /**
363
+ * Sets the namespace prefix for all queues.
364
+ */
365
+ withNamespace(namespace) {
366
+ this.namespace = namespace;
367
+ return this;
368
+ }
369
+ /**
370
+ * Adds a queue to the topology.
371
+ */
372
+ addQueue(name, options = {}) {
373
+ this.queues.push({ name, ...options });
374
+ return this;
375
+ }
376
+ /**
377
+ * Alias for addQueue().
378
+ */
379
+ queue(name, options = {}) {
380
+ return this.addQueue(name, options);
381
+ }
382
+ /**
383
+ * Configures dead-letter queue settings.
384
+ */
385
+ withDeadLetter(config) {
386
+ this.deadLetter = {
387
+ unhandled: config.unhandled ?? this.deadLetter.unhandled,
388
+ undeliverable: config.undeliverable ?? this.deadLetter.undeliverable
389
+ };
390
+ return this;
391
+ }
392
+ /**
393
+ * Configures retry settings.
394
+ */
395
+ withRetry(config) {
396
+ this.retry = {
397
+ enabled: config.enabled ?? this.retry.enabled,
398
+ defaultDelayMs: config.defaultDelayMs ?? this.retry.defaultDelayMs,
399
+ maxDelayMs: config.maxDelayMs ?? this.retry.maxDelayMs
400
+ };
401
+ return this;
402
+ }
403
+ /**
404
+ * Disables retry functionality.
405
+ */
406
+ withoutRetry() {
407
+ this.retry = { ...this.retry, enabled: false };
408
+ return this;
409
+ }
410
+ /**
411
+ * Disables dead-letter queues.
412
+ */
413
+ withoutDeadLetter() {
414
+ this.deadLetter = {
415
+ unhandled: { enabled: false },
416
+ undeliverable: { enabled: false }
417
+ };
418
+ return this;
419
+ }
420
+ /**
421
+ * Validates the topology configuration.
422
+ */
423
+ validate() {
424
+ const issues = [];
425
+ if (!this.namespace || this.namespace.trim() === "") {
426
+ issues.push("Namespace is required");
427
+ } else if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(this.namespace)) {
428
+ issues.push(
429
+ "Namespace must start with a letter and contain only alphanumeric characters, underscores, and hyphens"
430
+ );
431
+ }
432
+ if (this.queues.length === 0) {
433
+ issues.push("At least one queue is required");
434
+ }
435
+ const queueNames = /* @__PURE__ */ new Set();
436
+ for (const queue of this.queues) {
437
+ if (!queue.name || queue.name.trim() === "") {
438
+ issues.push("Queue name cannot be empty");
439
+ } else if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(queue.name)) {
440
+ issues.push(
441
+ `Queue name "${queue.name}" must start with a letter and contain only alphanumeric characters, underscores, and hyphens`
442
+ );
443
+ } else if (queueNames.has(queue.name)) {
444
+ issues.push(`Duplicate queue name: "${queue.name}"`);
445
+ } else {
446
+ queueNames.add(queue.name);
447
+ }
448
+ if (queue.concurrency !== void 0 && queue.concurrency < 1) {
449
+ issues.push(`Queue "${queue.name}" concurrency must be at least 1`);
450
+ }
451
+ if (queue.consumerTimeout !== void 0 && queue.consumerTimeout < 0) {
452
+ issues.push(
453
+ `Queue "${queue.name}" consumer timeout must be non-negative`
454
+ );
455
+ }
456
+ }
457
+ if (this.retry.enabled) {
458
+ if (this.retry.defaultDelayMs < 0) {
459
+ issues.push("Default retry delay must be non-negative");
460
+ }
461
+ if (this.retry.maxDelayMs < this.retry.defaultDelayMs) {
462
+ issues.push(
463
+ "Max retry delay must be greater than or equal to default delay"
464
+ );
465
+ }
466
+ }
467
+ return issues;
468
+ }
469
+ /**
470
+ * Builds the topology configuration.
471
+ * @throws TopologyValidationError if validation fails
472
+ */
473
+ build() {
474
+ const issues = this.validate();
475
+ if (issues.length > 0) {
476
+ throw new TopologyValidationError(
477
+ `Invalid topology: ${issues.join("; ")}`,
478
+ issues
479
+ );
480
+ }
481
+ return {
482
+ namespace: this.namespace,
483
+ queues: [...this.queues],
484
+ deadLetter: this.deadLetter,
485
+ retry: this.retry
486
+ };
487
+ }
488
+ };
489
+
490
+ // src/types/common.ts
491
+ function validResult() {
492
+ return { valid: true, errors: [] };
493
+ }
494
+ function invalidResult(errors) {
495
+ return { valid: false, errors };
496
+ }
497
+
498
+ // src/types/envelope.ts
499
+ function createEnvelope(options) {
500
+ const now = (/* @__PURE__ */ new Date()).toISOString();
501
+ const mergedMetadata = options.universalMetadata || options.metadata ? { ...options.universalMetadata, ...options.metadata } : void 0;
502
+ return {
503
+ id: options.id ?? crypto.randomUUID(),
504
+ data: options.data,
505
+ docket: {
506
+ // Routing
507
+ eventKey: options.eventKey,
508
+ ...options.eventDescription !== void 0 && {
509
+ eventDescription: options.eventDescription
510
+ },
511
+ targetSubscriber: options.targetSubscriber,
512
+ ...options.delayMs !== void 0 && options.delayMs > 0 && {
513
+ scheduledFor: new Date(Date.now() + options.delayMs).toISOString()
514
+ },
515
+ // Processing state
516
+ attempts: 1,
517
+ createdAt: now,
518
+ // Observability
519
+ importance: options.importance,
520
+ ...options.correlationId !== void 0 && {
521
+ correlationId: options.correlationId
522
+ },
523
+ ...mergedMetadata !== void 0 && { metadata: mergedMetadata }
524
+ }
525
+ };
526
+ }
527
+
528
+ // src/types/event.ts
529
+ var MatadorEvent = class {
530
+ static key;
531
+ static description;
532
+ static aliases;
533
+ /** Event-specific metadata */
534
+ metadata;
535
+ };
536
+
537
+ // src/types/subscriber.ts
538
+ function isSubscriberStub(subscriber) {
539
+ return "isStub" in subscriber && subscriber.isStub === true;
540
+ }
541
+ function isSubscriber(subscriber) {
542
+ return "callback" in subscriber && typeof subscriber.callback === "function";
543
+ }
544
+ function isResumableSubscriber(subscriber) {
545
+ return isSubscriber(subscriber) && subscriber.idempotent === "resumable";
546
+ }
547
+ function createSubscriber(input) {
548
+ const base = {
549
+ name: input.name,
550
+ description: input.description,
551
+ callback: input.callback,
552
+ importance: input.importance ?? "should-investigate",
553
+ ...input.targetQueue !== void 0 && {
554
+ targetQueue: input.targetQueue
555
+ },
556
+ ...input.enabled !== void 0 && { enabled: input.enabled }
557
+ };
558
+ if (input.idempotent === "resumable") {
559
+ return {
560
+ ...base,
561
+ idempotent: "resumable",
562
+ callback: input.callback
563
+ };
564
+ }
565
+ return {
566
+ ...base,
567
+ idempotent: input.idempotent ?? "unknown",
568
+ callback: input.callback
569
+ };
570
+ }
571
+ function createSubscriberStub(input) {
572
+ return {
573
+ name: input.name,
574
+ description: input.description,
575
+ isStub: true,
576
+ idempotent: input.idempotent ?? "unknown",
577
+ importance: input.importance ?? "should-investigate",
578
+ ...input.targetQueue !== void 0 && {
579
+ targetQueue: input.targetQueue
580
+ },
581
+ ...input.enabled !== void 0 && { enabled: input.enabled }
582
+ };
583
+ }
584
+
585
+ // src/core/fanout.ts
586
+ var FanoutEngine = class {
587
+ transport;
588
+ schema;
589
+ hooks;
590
+ namespace;
591
+ defaultQueue;
592
+ enqueuingCount = 0;
593
+ constructor(config) {
594
+ this.transport = config.transport;
595
+ this.schema = config.schema;
596
+ this.hooks = config.hooks;
597
+ this.namespace = config.namespace;
598
+ this.defaultQueue = config.defaultQueue;
599
+ }
600
+ /**
601
+ * Current count of events being enqueued.
602
+ */
603
+ get eventsBeingEnqueuedCount() {
604
+ return this.enqueuingCount;
605
+ }
606
+ /**
607
+ * Sends an event to all registered subscribers.
608
+ */
609
+ async send(eventClass, event, options = {}) {
610
+ const eventKey = eventClass.key;
611
+ const subscribers = this.schema.getSubscribers(eventKey);
612
+ const errors = [];
613
+ let sent = 0;
614
+ let skipped = 0;
615
+ const universalMetadata = await this.hooks.loadUniversalMetadata();
616
+ const mergedMetadata = event.metadata || options.metadata ? { ...event.metadata, ...options.metadata } : void 0;
617
+ for (const subscriber of subscribers) {
618
+ const enabled = await this.isSubscriberEnabled(subscriber);
619
+ if (!enabled) {
620
+ skipped++;
621
+ continue;
622
+ }
623
+ const targetQueue = subscriber.targetQueue ?? this.defaultQueue;
624
+ const qualifiedQueue = getQualifiedQueueName(this.namespace, targetQueue);
625
+ const envelope = createEnvelope({
626
+ eventKey,
627
+ eventDescription: eventClass.description,
628
+ targetSubscriber: subscriber.name,
629
+ data: event.data,
630
+ importance: subscriber.importance ?? "should-investigate",
631
+ correlationId: options.correlationId,
632
+ metadata: mergedMetadata,
633
+ universalMetadata,
634
+ delayMs: options.delayMs
635
+ });
636
+ this.enqueuingCount++;
637
+ try {
638
+ const usedTransport = await this.transport.send(
639
+ qualifiedQueue,
640
+ envelope,
641
+ options.delayMs !== void 0 ? { delay: options.delayMs } : void 0
642
+ );
643
+ sent++;
644
+ await this.hooks.onEnqueueSuccess({
645
+ envelope,
646
+ queue: qualifiedQueue,
647
+ transport: usedTransport
648
+ });
649
+ } catch (error) {
650
+ const cause = error instanceof Error ? error : new Error(String(error));
651
+ const err = new TransportSendError(qualifiedQueue, cause);
652
+ errors.push({
653
+ subscriberName: subscriber.name,
654
+ queue: qualifiedQueue,
655
+ error: err
656
+ });
657
+ await this.hooks.onEnqueueError({
658
+ envelope,
659
+ error: err,
660
+ transport: this.transport.name
661
+ });
662
+ } finally {
663
+ this.enqueuingCount--;
664
+ }
665
+ }
666
+ return {
667
+ eventKey,
668
+ subscribersSent: sent,
669
+ subscribersSkipped: skipped,
670
+ errors
671
+ };
672
+ }
673
+ async isSubscriberEnabled(subscriber) {
674
+ if (!subscriber.enabled) {
675
+ return true;
676
+ }
677
+ try {
678
+ const result = await subscriber.enabled();
679
+ return result;
680
+ } catch {
681
+ return true;
682
+ }
683
+ }
684
+ };
685
+
686
+ // src/hooks/types.ts
687
+ var consoleLogger = {
688
+ debug: (message, ...args) => console.debug(message, ...args),
689
+ info: (message, ...args) => console.info(message, ...args),
690
+ warn: (message, ...args) => console.warn(message, ...args),
691
+ error: (message, ...args) => console.error(message, ...args)
692
+ };
693
+
694
+ // src/hooks/safe-hooks.ts
695
+ var SafeHooks = class {
696
+ hooks;
697
+ /** The logger instance used by Matador. */
698
+ logger;
699
+ constructor(hooks = {}) {
700
+ this.hooks = hooks;
701
+ this.logger = hooks.logger ?? consoleLogger;
702
+ }
703
+ async onEnqueueSuccess(context) {
704
+ await this.safeCall(
705
+ "onEnqueueSuccess",
706
+ () => this.hooks.onEnqueueSuccess?.(context)
707
+ );
708
+ }
709
+ async onEnqueueWarning(context) {
710
+ await this.safeCall(
711
+ "onEnqueueWarning",
712
+ () => this.hooks.onEnqueueWarning?.(context)
713
+ );
714
+ }
715
+ async onEnqueueError(context) {
716
+ await this.safeCall(
717
+ "onEnqueueError",
718
+ () => this.hooks.onEnqueueError?.(context)
719
+ );
720
+ }
721
+ async onWorkerWrap(envelope, subscriber, execute) {
722
+ if (this.hooks.onWorkerWrap) {
723
+ try {
724
+ await this.hooks.onWorkerWrap(envelope, subscriber, execute);
725
+ } catch (error) {
726
+ this.logger.warn(
727
+ "[Matador] \u{1F7E1} Hook onWorkerWrap threw an error",
728
+ error
729
+ );
730
+ await execute();
731
+ }
732
+ } else {
733
+ await execute();
734
+ }
735
+ }
736
+ async onWorkerBeforeProcess(envelope, subscriber) {
737
+ await this.safeCall(
738
+ "onWorkerBeforeProcess",
739
+ () => this.hooks.onWorkerBeforeProcess?.(envelope, subscriber)
740
+ );
741
+ }
742
+ async onWorkerSuccess(context) {
743
+ await this.safeCall(
744
+ "onWorkerSuccess",
745
+ () => this.hooks.onWorkerSuccess?.(context)
746
+ );
747
+ }
748
+ async onWorkerError(context) {
749
+ await this.safeCall(
750
+ "onWorkerError",
751
+ () => this.hooks.onWorkerError?.(context)
752
+ );
753
+ }
754
+ async onDecodeError(context) {
755
+ await this.safeCall(
756
+ "onDecodeError",
757
+ () => this.hooks.onDecodeError?.(context)
758
+ );
759
+ }
760
+ async onConnectionStateChange(state) {
761
+ await this.safeCall(
762
+ "onConnectionStateChange",
763
+ () => this.hooks.onConnectionStateChange?.(state)
764
+ );
765
+ }
766
+ async loadUniversalMetadata() {
767
+ if (!this.hooks.loadUniversalMetadata) {
768
+ return {};
769
+ }
770
+ try {
771
+ const result = await this.hooks.loadUniversalMetadata();
772
+ return result ?? {};
773
+ } catch (error) {
774
+ this.logger.warn(
775
+ "[Matador] \u{1F7E1} Hook loadUniversalMetadata threw an error",
776
+ error
777
+ );
778
+ return {};
779
+ }
780
+ }
781
+ async getQueueConcurrency(queueName) {
782
+ if (!this.hooks.getQueueConcurrency) {
783
+ return void 0;
784
+ }
785
+ try {
786
+ return await this.hooks.getQueueConcurrency(queueName);
787
+ } catch (error) {
788
+ this.logger.warn(
789
+ "[Matador] \u{1F7E1} Hook getQueueConcurrency threw an error",
790
+ error
791
+ );
792
+ return void 0;
793
+ }
794
+ }
795
+ async getRetryDelay(envelope, attemptNumber) {
796
+ if (!this.hooks.getRetryDelay) {
797
+ return void 0;
798
+ }
799
+ try {
800
+ return await this.hooks.getRetryDelay(envelope, attemptNumber);
801
+ } catch (error) {
802
+ this.logger.warn("[Matador] \u{1F7E1} Hook getRetryDelay threw an error", error);
803
+ return void 0;
804
+ }
805
+ }
806
+ async getAttempts(envelope) {
807
+ if (!this.hooks.getAttempts) {
808
+ return void 0;
809
+ }
810
+ try {
811
+ return await this.hooks.getAttempts(envelope);
812
+ } catch (error) {
813
+ this.logger.warn("[Matador] \u{1F7E1} Hook getAttempts threw an error", error);
814
+ return void 0;
815
+ }
816
+ }
817
+ async getMaxDeliveries(envelope) {
818
+ if (!this.hooks.getMaxDeliveries) {
819
+ return void 0;
820
+ }
821
+ try {
822
+ return await this.hooks.getMaxDeliveries(envelope);
823
+ } catch (error) {
824
+ this.logger.warn(
825
+ "[Matador] \u{1F7E1} Hook getMaxDeliveries threw an error",
826
+ error
827
+ );
828
+ return void 0;
829
+ }
830
+ }
831
+ // === Checkpoint Hooks ===
832
+ async onCheckpointLoaded(context) {
833
+ await this.safeCall(
834
+ "onCheckpointLoaded",
835
+ () => this.hooks.onCheckpointLoaded?.(context)
836
+ );
837
+ }
838
+ async onCheckpointHit(context) {
839
+ await this.safeCall(
840
+ "onCheckpointHit",
841
+ () => this.hooks.onCheckpointHit?.(context)
842
+ );
843
+ }
844
+ async onCheckpointMiss(context) {
845
+ await this.safeCall(
846
+ "onCheckpointMiss",
847
+ () => this.hooks.onCheckpointMiss?.(context)
848
+ );
849
+ }
850
+ async onCheckpointCleared(context) {
851
+ await this.safeCall(
852
+ "onCheckpointCleared",
853
+ () => this.hooks.onCheckpointCleared?.(context)
854
+ );
855
+ }
856
+ async safeCall(hookName, fn) {
857
+ try {
858
+ await fn();
859
+ } catch (error) {
860
+ this.logger.warn(`[Matador] \u{1F7E1} Hook ${hookName} threw an error`, error);
861
+ }
862
+ }
863
+ };
864
+
865
+ // src/core/shutdown.ts
866
+ var defaultShutdownConfig = {
867
+ gracefulShutdownTimeout: 3e4,
868
+ idlePollingInterval: 1e3,
869
+ logger: consoleLogger
870
+ };
871
+ var ShutdownManager = class {
872
+ constructor(getEnqueueCount, stopReceiving, disconnectTransport, config = {}) {
873
+ this.getEnqueueCount = getEnqueueCount;
874
+ this.stopReceiving = stopReceiving;
875
+ this.disconnectTransport = disconnectTransport;
876
+ this.config = { ...defaultShutdownConfig, ...config };
877
+ }
878
+ _state = "running";
879
+ config;
880
+ eventsBeingProcessed = 0;
881
+ acceptingEnqueue = true;
882
+ /**
883
+ * Current shutdown state.
884
+ */
885
+ get state() {
886
+ return this._state;
887
+ }
888
+ /**
889
+ * Whether enqueue is currently allowed.
890
+ */
891
+ get isEnqueueAllowed() {
892
+ return this.acceptingEnqueue;
893
+ }
894
+ /**
895
+ * Gets current handler state.
896
+ */
897
+ getHandlersState() {
898
+ const eventsBeingEnqueued = this.getEnqueueCount();
899
+ return {
900
+ eventsBeingProcessed: this.eventsBeingProcessed,
901
+ eventsBeingEnqueued,
902
+ isIdle: this.eventsBeingProcessed === 0 && eventsBeingEnqueued === 0
903
+ };
904
+ }
905
+ /**
906
+ * Increment processing counter.
907
+ */
908
+ incrementProcessing() {
909
+ this.eventsBeingProcessed++;
910
+ }
911
+ /**
912
+ * Decrement processing counter.
913
+ */
914
+ decrementProcessing() {
915
+ this.eventsBeingProcessed--;
916
+ }
917
+ /**
918
+ * Performs graceful shutdown.
919
+ */
920
+ async shutdown() {
921
+ if (this._state !== "running") {
922
+ return;
923
+ }
924
+ this._state = "stopping-receive";
925
+ await this.stopReceiving();
926
+ this._state = "waiting-handlers";
927
+ await this.waitForIdle();
928
+ this._state = "stopping-enqueue";
929
+ this.acceptingEnqueue = false;
930
+ this._state = "disconnecting";
931
+ await this.disconnectTransport();
932
+ this._state = "stopped";
933
+ }
934
+ /**
935
+ * Forcefully stops without waiting.
936
+ */
937
+ async forceStop() {
938
+ this.acceptingEnqueue = false;
939
+ await this.stopReceiving();
940
+ await this.disconnectTransport();
941
+ this._state = "stopped";
942
+ }
943
+ async waitForIdle() {
944
+ const deadline = Date.now() + this.config.gracefulShutdownTimeout;
945
+ while (!this.getHandlersState().isIdle) {
946
+ if (Date.now() > deadline) {
947
+ this.config.logger.warn(
948
+ `[Matador] \u26A0\uFE0F Shutdown timeout reached with ${this.eventsBeingProcessed} events still processing`
949
+ );
950
+ break;
951
+ }
952
+ await this.sleep(this.config.idlePollingInterval);
953
+ }
954
+ }
955
+ sleep(ms) {
956
+ return new Promise((resolve) => setTimeout(resolve, ms));
957
+ }
958
+ };
959
+
960
+ // src/codec/codec.ts
961
+ var CodecDecodeError = class extends Error {
962
+ constructor(message, cause) {
963
+ super(message);
964
+ this.cause = cause;
965
+ this.name = "CodecDecodeError";
966
+ }
967
+ description = "Failed to decode a message from the transport. This typically indicates corrupted data, incompatible codec versions, or messages from a different system. Check the cause property for the underlying parsing error. The message will be sent to the dead-letter queue for investigation.";
968
+ };
969
+
970
+ // src/codec/json-codec.ts
971
+ var JsonCodec = class {
972
+ contentType = "application/json";
973
+ encode(envelope) {
974
+ const json = JSON.stringify(envelope);
975
+ return new TextEncoder().encode(json);
976
+ }
977
+ decode(buffer) {
978
+ if (buffer.length === 0) {
979
+ throw new CodecDecodeError("Cannot decode empty buffer");
980
+ }
981
+ let json;
982
+ try {
983
+ json = new TextDecoder("utf-8").decode(buffer);
984
+ } catch (error) {
985
+ throw new CodecDecodeError("Invalid UTF-8 encoding", error);
986
+ }
987
+ let parsed;
988
+ try {
989
+ parsed = JSON.parse(json);
990
+ } catch (error) {
991
+ throw new CodecDecodeError("Invalid JSON", error);
992
+ }
993
+ if (!this.isValidEnvelope(parsed)) {
994
+ throw new CodecDecodeError("Invalid envelope structure");
995
+ }
996
+ return parsed;
997
+ }
998
+ isValidEnvelope(value) {
999
+ if (typeof value !== "object" || value === null) {
1000
+ return false;
1001
+ }
1002
+ const envelope = value;
1003
+ if (typeof envelope["id"] !== "string") return false;
1004
+ if (!("data" in envelope)) return false;
1005
+ if (typeof envelope["docket"] !== "object" || envelope["docket"] === null)
1006
+ return false;
1007
+ const docket = envelope["docket"];
1008
+ if (typeof docket["eventKey"] !== "string") return false;
1009
+ if (typeof docket["targetSubscriber"] !== "string") return false;
1010
+ if (typeof docket["attempts"] !== "number") return false;
1011
+ if (typeof docket["createdAt"] !== "string") return false;
1012
+ if (typeof docket["importance"] !== "string") return false;
1013
+ return true;
1014
+ }
1015
+ };
1016
+
1017
+ // src/codec/rabbitmq-codec.ts
1018
+ var HEADERS = {
1019
+ // Routing
1020
+ EVENT_KEY: "x-matador-event-key",
1021
+ TARGET_SUBSCRIBER: "x-matador-subscriber",
1022
+ SCHEDULED_FOR: "x-matador-scheduled-for",
1023
+ ORIGINAL_QUEUE: "x-matador-original-queue",
1024
+ // Processing state
1025
+ ATTEMPTS: "x-matador-attempts",
1026
+ CREATED_AT: "x-matador-created-at",
1027
+ FIRST_ERROR: "x-matador-first-error",
1028
+ LAST_ERROR: "x-matador-last-error",
1029
+ // Observability
1030
+ IMPORTANCE: "x-matador-importance",
1031
+ CORRELATION_ID: "x-matador-correlation-id",
1032
+ // v1 compatibility headers
1033
+ V1_EVENT_ID: "x-event-id",
1034
+ V1_CORRELATION_ID: "x-correlation-id"};
1035
+ var RabbitMQCodec = class {
1036
+ contentType = "application/json";
1037
+ encode(envelope) {
1038
+ const { docket } = envelope;
1039
+ const body = {
1040
+ id: envelope.id,
1041
+ data: envelope.data,
1042
+ ...docket.metadata !== void 0 && { metadata: docket.metadata }
1043
+ };
1044
+ const headers = {
1045
+ // Routing
1046
+ [HEADERS.EVENT_KEY]: docket.eventKey,
1047
+ [HEADERS.TARGET_SUBSCRIBER]: docket.targetSubscriber,
1048
+ // Processing state
1049
+ [HEADERS.ATTEMPTS]: docket.attempts,
1050
+ [HEADERS.CREATED_AT]: docket.createdAt,
1051
+ // Observability
1052
+ [HEADERS.IMPORTANCE]: docket.importance
1053
+ };
1054
+ if (docket.scheduledFor !== void 0) {
1055
+ headers[HEADERS.SCHEDULED_FOR] = docket.scheduledFor;
1056
+ }
1057
+ if (docket.originalQueue !== void 0) {
1058
+ headers[HEADERS.ORIGINAL_QUEUE] = docket.originalQueue;
1059
+ }
1060
+ if (docket.firstError !== void 0) {
1061
+ headers[HEADERS.FIRST_ERROR] = docket.firstError;
1062
+ }
1063
+ if (docket.lastError !== void 0) {
1064
+ headers[HEADERS.LAST_ERROR] = docket.lastError;
1065
+ }
1066
+ if (docket.correlationId !== void 0) {
1067
+ headers[HEADERS.CORRELATION_ID] = docket.correlationId;
1068
+ }
1069
+ return {
1070
+ body: new TextEncoder().encode(JSON.stringify(body)),
1071
+ headers,
1072
+ contentType: this.contentType
1073
+ };
1074
+ }
1075
+ decode(body, headers) {
1076
+ if (body.length === 0) {
1077
+ throw new CodecDecodeError("Cannot decode empty buffer");
1078
+ }
1079
+ let json;
1080
+ try {
1081
+ json = new TextDecoder("utf-8").decode(body);
1082
+ } catch (error) {
1083
+ throw new CodecDecodeError("Invalid UTF-8 encoding", error);
1084
+ }
1085
+ let parsed;
1086
+ try {
1087
+ parsed = JSON.parse(json);
1088
+ } catch (error) {
1089
+ throw new CodecDecodeError("Invalid JSON", error);
1090
+ }
1091
+ if (this.isV1Body(parsed)) {
1092
+ return this.decodeV1(parsed, headers);
1093
+ }
1094
+ if (!this.isV2Body(parsed)) {
1095
+ throw new CodecDecodeError("Invalid message body structure");
1096
+ }
1097
+ return this.decodeV2(parsed, headers);
1098
+ }
1099
+ isV1Body(value) {
1100
+ if (typeof value !== "object" || value === null) return false;
1101
+ const obj = value;
1102
+ return typeof obj["key"] === "string" && typeof obj["targetSubscriber"] === "string" && "data" in obj && !("payload" in obj);
1103
+ }
1104
+ isV2Body(value) {
1105
+ if (typeof value !== "object" || value === null) return false;
1106
+ const obj = value;
1107
+ return typeof obj["id"] === "string" && "data" in obj && !("key" in obj);
1108
+ }
1109
+ decodeV2(body, headers) {
1110
+ const eventKey = this.requireStringHeader(headers, HEADERS.EVENT_KEY);
1111
+ const targetSubscriber = this.requireStringHeader(
1112
+ headers,
1113
+ HEADERS.TARGET_SUBSCRIBER
1114
+ );
1115
+ const attempts = this.requireNumberHeader(headers, HEADERS.ATTEMPTS);
1116
+ const createdAt = this.requireStringHeader(headers, HEADERS.CREATED_AT);
1117
+ const importance = this.requireStringHeader(headers, HEADERS.IMPORTANCE);
1118
+ const scheduledFor = this.optionalStringHeader(
1119
+ headers,
1120
+ HEADERS.SCHEDULED_FOR
1121
+ );
1122
+ const originalQueue = this.optionalStringHeader(
1123
+ headers,
1124
+ HEADERS.ORIGINAL_QUEUE
1125
+ );
1126
+ const firstError = this.optionalStringHeader(headers, HEADERS.FIRST_ERROR);
1127
+ const lastError = this.optionalStringHeader(headers, HEADERS.LAST_ERROR);
1128
+ const correlationId = this.optionalStringHeader(
1129
+ headers,
1130
+ HEADERS.CORRELATION_ID
1131
+ );
1132
+ const docket = {
1133
+ // Routing
1134
+ eventKey,
1135
+ targetSubscriber,
1136
+ ...scheduledFor !== void 0 && { scheduledFor },
1137
+ ...originalQueue !== void 0 && { originalQueue },
1138
+ // Processing state
1139
+ attempts,
1140
+ createdAt,
1141
+ ...firstError !== void 0 && { firstError },
1142
+ ...lastError !== void 0 && { lastError },
1143
+ // Observability
1144
+ importance,
1145
+ ...correlationId !== void 0 && { correlationId },
1146
+ ...body.metadata !== void 0 && { metadata: body.metadata }
1147
+ };
1148
+ return {
1149
+ id: body.id,
1150
+ data: body.data,
1151
+ docket
1152
+ };
1153
+ }
1154
+ /**
1155
+ * Decodes a v1 format message for backwards compatibility.
1156
+ * v1 format: { key, data, metadata, universal, targetSubscriber, ... }
1157
+ */
1158
+ decodeV1(body, headers) {
1159
+ const { event_id, user_id, correlation_id, ...otherUniversal } = body.universal ?? {};
1160
+ const mergedMetadata = {};
1161
+ if (body.metadata && typeof body.metadata === "object") {
1162
+ Object.assign(mergedMetadata, body.metadata);
1163
+ }
1164
+ if (user_id !== void 0 && user_id !== null) {
1165
+ mergedMetadata["user_id"] = user_id;
1166
+ }
1167
+ Object.assign(mergedMetadata, otherUniversal);
1168
+ const attempts = typeof headers[HEADERS.ATTEMPTS] === "number" ? headers[HEADERS.ATTEMPTS] : 1;
1169
+ const importance = typeof headers[HEADERS.IMPORTANCE] === "string" ? headers[HEADERS.IMPORTANCE] : "should-investigate";
1170
+ const correlationId = headers[HEADERS.CORRELATION_ID] ?? headers[HEADERS.V1_CORRELATION_ID] ?? correlation_id ?? void 0;
1171
+ const eventId = headers[HEADERS.V1_EVENT_ID] ?? event_id ?? crypto.randomUUID();
1172
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1173
+ let scheduledFor;
1174
+ if (body.options?.delayMs) {
1175
+ scheduledFor = new Date(Date.now() + body.options.delayMs).toISOString();
1176
+ }
1177
+ const docket = {
1178
+ // Routing
1179
+ eventKey: body.key,
1180
+ targetSubscriber: body.targetSubscriber,
1181
+ ...scheduledFor !== void 0 && { scheduledFor },
1182
+ // Processing state
1183
+ attempts,
1184
+ createdAt: now,
1185
+ // Observability
1186
+ importance,
1187
+ ...correlationId !== void 0 && { correlationId },
1188
+ ...Object.keys(mergedMetadata).length > 0 && {
1189
+ metadata: mergedMetadata
1190
+ }
1191
+ };
1192
+ return {
1193
+ id: eventId,
1194
+ data: body.data,
1195
+ docket
1196
+ };
1197
+ }
1198
+ requireStringHeader(headers, name) {
1199
+ const value = headers[name];
1200
+ if (value === void 0) {
1201
+ throw new CodecDecodeError(`Missing required header: ${name}`);
1202
+ }
1203
+ if (typeof value !== "string") {
1204
+ throw new CodecDecodeError(
1205
+ `Header ${name} must be string, got ${typeof value}`
1206
+ );
1207
+ }
1208
+ return value;
1209
+ }
1210
+ requireNumberHeader(headers, name) {
1211
+ const value = headers[name];
1212
+ if (value === void 0) {
1213
+ throw new CodecDecodeError(`Missing required header: ${name}`);
1214
+ }
1215
+ if (typeof value !== "number") {
1216
+ throw new CodecDecodeError(
1217
+ `Header ${name} must be number, got ${typeof value}`
1218
+ );
1219
+ }
1220
+ return value;
1221
+ }
1222
+ optionalStringHeader(headers, name) {
1223
+ const value = headers[name];
1224
+ if (value === void 0) return void 0;
1225
+ if (typeof value !== "string") return void 0;
1226
+ return value;
1227
+ }
1228
+ };
1229
+
1230
+ // src/checkpoint/context.ts
1231
+ var ResumableContext = class {
1232
+ checkpoint;
1233
+ usedKeys = /* @__PURE__ */ new Set();
1234
+ store;
1235
+ envelope;
1236
+ subscriber;
1237
+ hooks;
1238
+ constructor(config) {
1239
+ this.store = config.store;
1240
+ this.envelope = config.envelope;
1241
+ this.subscriber = config.subscriber;
1242
+ this.hooks = config.hooks;
1243
+ this.checkpoint = config.existingCheckpoint ?? {
1244
+ envelopeId: config.envelope.id,
1245
+ subscriberName: config.subscriber.name,
1246
+ completedSteps: {}
1247
+ };
1248
+ }
1249
+ get attempt() {
1250
+ return this.envelope.docket.attempts;
1251
+ }
1252
+ get isRetry() {
1253
+ return this.attempt > 1;
1254
+ }
1255
+ async io(key, fn) {
1256
+ if (this.usedKeys.has(key)) {
1257
+ throw new DuplicateIoKeyError(key, this.subscriber.name);
1258
+ }
1259
+ this.usedKeys.add(key);
1260
+ if (key in this.checkpoint.completedSteps) {
1261
+ await this.hooks?.onCheckpointHit?.({
1262
+ envelope: this.envelope,
1263
+ subscriber: this.subscriber,
1264
+ stepKey: key
1265
+ });
1266
+ return this.checkpoint.completedSteps[key];
1267
+ }
1268
+ await this.hooks?.onCheckpointMiss?.({
1269
+ envelope: this.envelope,
1270
+ subscriber: this.subscriber,
1271
+ stepKey: key
1272
+ });
1273
+ const result = await fn();
1274
+ this.checkpoint = {
1275
+ ...this.checkpoint,
1276
+ completedSteps: {
1277
+ ...this.checkpoint.completedSteps,
1278
+ [key]: result
1279
+ }
1280
+ };
1281
+ await this.store.set(this.envelope.id, this.checkpoint);
1282
+ return result;
1283
+ }
1284
+ async all(ops) {
1285
+ const keysInThisCall = /* @__PURE__ */ new Set();
1286
+ for (const [key] of ops) {
1287
+ if (keysInThisCall.has(key)) {
1288
+ throw new DuplicateIoKeyError(key, this.subscriber.name);
1289
+ }
1290
+ keysInThisCall.add(key);
1291
+ }
1292
+ for (const [key] of ops) {
1293
+ if (this.usedKeys.has(key)) {
1294
+ throw new DuplicateIoKeyError(key, this.subscriber.name);
1295
+ }
1296
+ }
1297
+ for (const [key] of ops) {
1298
+ this.usedKeys.add(key);
1299
+ }
1300
+ const results = await Promise.all(
1301
+ ops.map(async ([key, fn]) => {
1302
+ if (key in this.checkpoint.completedSteps) {
1303
+ await this.hooks?.onCheckpointHit?.({
1304
+ envelope: this.envelope,
1305
+ subscriber: this.subscriber,
1306
+ stepKey: key
1307
+ });
1308
+ return this.checkpoint.completedSteps[key];
1309
+ }
1310
+ await this.hooks?.onCheckpointMiss?.({
1311
+ envelope: this.envelope,
1312
+ subscriber: this.subscriber,
1313
+ stepKey: key
1314
+ });
1315
+ const result = await fn();
1316
+ this.checkpoint = {
1317
+ ...this.checkpoint,
1318
+ completedSteps: {
1319
+ ...this.checkpoint.completedSteps,
1320
+ [key]: result
1321
+ }
1322
+ };
1323
+ return result;
1324
+ })
1325
+ );
1326
+ await this.store.set(this.envelope.id, this.checkpoint);
1327
+ return results;
1328
+ }
1329
+ /**
1330
+ * Clears the checkpoint from storage.
1331
+ * Called after successful completion or dead-letter.
1332
+ */
1333
+ async clear() {
1334
+ await this.store.delete(this.envelope.id);
1335
+ }
1336
+ /**
1337
+ * Gets the current checkpoint state.
1338
+ * Useful for debugging and testing.
1339
+ */
1340
+ getCheckpoint() {
1341
+ return this.checkpoint;
1342
+ }
1343
+ /**
1344
+ * Gets the number of cached steps.
1345
+ */
1346
+ get cachedStepCount() {
1347
+ return Object.keys(this.checkpoint.completedSteps).length;
1348
+ }
1349
+ };
1350
+
1351
+ // src/checkpoint/stores/memory.ts
1352
+ var MemoryCheckpointStore = class {
1353
+ checkpoints = /* @__PURE__ */ new Map();
1354
+ async get(envelopeId) {
1355
+ return this.checkpoints.get(envelopeId);
1356
+ }
1357
+ async set(envelopeId, checkpoint) {
1358
+ this.checkpoints.set(envelopeId, checkpoint);
1359
+ }
1360
+ async delete(envelopeId) {
1361
+ this.checkpoints.delete(envelopeId);
1362
+ }
1363
+ /**
1364
+ * Gets the number of stored checkpoints.
1365
+ * Useful for testing.
1366
+ */
1367
+ get size() {
1368
+ return this.checkpoints.size;
1369
+ }
1370
+ /**
1371
+ * Clears all stored checkpoints.
1372
+ * Useful for testing.
1373
+ */
1374
+ clear() {
1375
+ this.checkpoints.clear();
1376
+ }
1377
+ /**
1378
+ * Gets all stored checkpoint IDs.
1379
+ * Useful for testing.
1380
+ */
1381
+ keys() {
1382
+ return [...this.checkpoints.keys()];
1383
+ }
1384
+ };
1385
+
1386
+ // src/checkpoint/stores/noop.ts
1387
+ var NoOpCheckpointStore = class {
1388
+ async get(_envelopeId) {
1389
+ return void 0;
1390
+ }
1391
+ async set(_envelopeId, _checkpoint) {
1392
+ }
1393
+ async delete(_envelopeId) {
1394
+ }
1395
+ };
1396
+
1397
+ // src/pipeline/pipeline.ts
1398
+ var ProcessingPipeline = class {
1399
+ transport;
1400
+ schema;
1401
+ codec;
1402
+ retryPolicy;
1403
+ hooks;
1404
+ checkpointStore;
1405
+ constructor(config) {
1406
+ this.transport = config.transport;
1407
+ this.schema = config.schema;
1408
+ this.codec = config.codec;
1409
+ this.retryPolicy = config.retryPolicy;
1410
+ this.hooks = config.hooks;
1411
+ this.checkpointStore = config.checkpointStore ?? new NoOpCheckpointStore();
1412
+ }
1413
+ /**
1414
+ * Processes a raw message from the transport.
1415
+ */
1416
+ async process(rawMessage, receipt) {
1417
+ const startTime = performance.now();
1418
+ let envelope;
1419
+ try {
1420
+ envelope = this.codec.decode(rawMessage);
1421
+ } catch (error2) {
1422
+ const decodeError = error2 instanceof CodecDecodeError ? error2 : new CodecDecodeError("Unknown decode error", error2);
1423
+ await this.transport.complete(receipt);
1424
+ await this.hooks.onDecodeError({
1425
+ error: decodeError,
1426
+ rawMessage,
1427
+ sourceQueue: receipt.sourceQueue,
1428
+ transport: receipt.sourceTransport
1429
+ });
1430
+ return {
1431
+ success: false,
1432
+ error: decodeError,
1433
+ durationMs: performance.now() - startTime
1434
+ };
1435
+ }
1436
+ const subscriberDef = this.schema.getSubscriberDefinition(
1437
+ envelope.docket.eventKey,
1438
+ envelope.docket.targetSubscriber
1439
+ );
1440
+ if (!subscriberDef) {
1441
+ const error2 = new SubscriberNotRegisteredError(
1442
+ envelope.docket.targetSubscriber,
1443
+ envelope.docket.eventKey
1444
+ );
1445
+ const decision2 = this.getUnhandledRetryDecision(envelope, receipt, error2);
1446
+ await this.handleRetryDecision(receipt, envelope, decision2);
1447
+ return {
1448
+ success: false,
1449
+ envelope,
1450
+ error: error2,
1451
+ decision: decision2,
1452
+ durationMs: performance.now() - startTime
1453
+ };
1454
+ }
1455
+ const subscriber = this.schema.getExecutableSubscriber(
1456
+ envelope.docket.eventKey,
1457
+ envelope.docket.targetSubscriber
1458
+ );
1459
+ if (!subscriber) {
1460
+ const error2 = new SubscriberIsStubError(envelope.docket.targetSubscriber);
1461
+ const decision2 = this.getUnhandledRetryDecision(envelope, receipt, error2);
1462
+ await this.handleRetryDecision(receipt, envelope, decision2);
1463
+ return {
1464
+ success: false,
1465
+ envelope,
1466
+ subscriber: subscriberDef,
1467
+ error: error2,
1468
+ decision: decision2,
1469
+ durationMs: performance.now() - startTime
1470
+ };
1471
+ }
1472
+ let result;
1473
+ let error;
1474
+ let context;
1475
+ const isResumable = isResumableSubscriber(subscriber);
1476
+ let existingCheckpoint;
1477
+ if (isResumable) {
1478
+ existingCheckpoint = await this.checkpointStore.get(envelope.id);
1479
+ if (existingCheckpoint) {
1480
+ await this.hooks.onCheckpointLoaded?.({
1481
+ envelope,
1482
+ subscriber: subscriberDef,
1483
+ checkpoint: existingCheckpoint,
1484
+ cachedSteps: Object.keys(existingCheckpoint.completedSteps).length
1485
+ });
1486
+ }
1487
+ context = new ResumableContext({
1488
+ store: this.checkpointStore,
1489
+ envelope,
1490
+ subscriber: subscriberDef,
1491
+ existingCheckpoint,
1492
+ hooks: {
1493
+ onCheckpointHit: (ctx) => this.hooks.onCheckpointHit?.(ctx),
1494
+ onCheckpointMiss: (ctx) => this.hooks.onCheckpointMiss?.(ctx)
1495
+ }
1496
+ });
1497
+ }
1498
+ await this.hooks.onWorkerWrap(envelope, subscriberDef, async () => {
1499
+ await this.hooks.onWorkerBeforeProcess(envelope, subscriberDef);
1500
+ try {
1501
+ if (isResumable && context) {
1502
+ const resumableCallback = subscriber.callback;
1503
+ result = await resumableCallback(envelope, context);
1504
+ } else {
1505
+ const standardCallback = subscriber.callback;
1506
+ result = await standardCallback(envelope);
1507
+ }
1508
+ } catch (e) {
1509
+ error = e instanceof Error ? e : new Error(String(e));
1510
+ }
1511
+ });
1512
+ const durationMs = performance.now() - startTime;
1513
+ if (!error) {
1514
+ if (context) {
1515
+ await context.clear();
1516
+ await this.hooks.onCheckpointCleared?.({
1517
+ envelope,
1518
+ subscriber: subscriberDef,
1519
+ reason: "success"
1520
+ });
1521
+ }
1522
+ await this.transport.complete(receipt);
1523
+ await this.hooks.onWorkerSuccess({
1524
+ envelope,
1525
+ subscriber: subscriberDef,
1526
+ result,
1527
+ durationMs,
1528
+ transport: receipt.sourceTransport
1529
+ });
1530
+ return {
1531
+ success: true,
1532
+ envelope,
1533
+ subscriber: subscriberDef,
1534
+ durationMs
1535
+ };
1536
+ }
1537
+ const decision = this.retryPolicy.shouldRetry({
1538
+ envelope,
1539
+ error,
1540
+ subscriber: subscriberDef,
1541
+ receipt
1542
+ });
1543
+ envelope.docket.lastError = error.message;
1544
+ envelope.docket.firstError ??= error.message;
1545
+ if (decision.action === "dead-letter" && context) {
1546
+ await context.clear();
1547
+ await this.hooks.onCheckpointCleared?.({
1548
+ envelope,
1549
+ subscriber: subscriberDef,
1550
+ reason: "dead-letter"
1551
+ });
1552
+ }
1553
+ await this.handleRetryDecision(receipt, envelope, decision);
1554
+ await this.hooks.onWorkerError({
1555
+ envelope,
1556
+ subscriber: subscriberDef,
1557
+ error,
1558
+ durationMs,
1559
+ decision,
1560
+ transport: receipt.sourceTransport
1561
+ });
1562
+ return {
1563
+ success: false,
1564
+ envelope,
1565
+ subscriber: subscriberDef,
1566
+ error,
1567
+ decision,
1568
+ durationMs
1569
+ };
1570
+ }
1571
+ async handleRetryDecision(receipt, envelope, decision) {
1572
+ switch (decision.action) {
1573
+ case "retry": {
1574
+ envelope.docket.attempts++;
1575
+ envelope.docket.scheduledFor = new Date(
1576
+ Date.now() + decision.delay
1577
+ ).toISOString();
1578
+ await this.transport.send(receipt.sourceQueue, envelope, {
1579
+ delay: decision.delay
1580
+ });
1581
+ await this.transport.complete(receipt);
1582
+ break;
1583
+ }
1584
+ case "dead-letter": {
1585
+ await this.sendToDeadLetter(
1586
+ receipt,
1587
+ envelope,
1588
+ decision.queue,
1589
+ decision.reason
1590
+ );
1591
+ break;
1592
+ }
1593
+ case "discard": {
1594
+ await this.transport.complete(receipt);
1595
+ break;
1596
+ }
1597
+ }
1598
+ }
1599
+ async sendToDeadLetter(receipt, envelope, dlqName, reason) {
1600
+ envelope.docket.originalQueue ??= receipt.sourceQueue;
1601
+ if (this.transport.sendToDeadLetter) {
1602
+ await this.transport.sendToDeadLetter(receipt, dlqName, envelope, reason);
1603
+ } else {
1604
+ const fullDlqName = `${receipt.sourceQueue}.${dlqName}`;
1605
+ await this.transport.send(fullDlqName, envelope);
1606
+ await this.transport.complete(receipt);
1607
+ }
1608
+ }
1609
+ /**
1610
+ * Gets retry decision for unhandled messages (subscriber not found).
1611
+ * Uses same retry policy logic but sends to 'unhandled' DLQ after max attempts.
1612
+ */
1613
+ getUnhandledRetryDecision(envelope, receipt, error) {
1614
+ const syntheticSubscriber = {
1615
+ name: envelope.docket.targetSubscriber,
1616
+ description: "Synthetic subscriber for unhandled message retry",
1617
+ idempotent: "yes",
1618
+ // Safe to retry unhandled messages
1619
+ importance: envelope.docket.importance ?? "should-investigate"
1620
+ };
1621
+ const decision = this.retryPolicy.shouldRetry({
1622
+ envelope,
1623
+ error,
1624
+ subscriber: syntheticSubscriber,
1625
+ receipt
1626
+ });
1627
+ if (decision.action === "dead-letter") {
1628
+ return {
1629
+ action: "dead-letter",
1630
+ queue: "unhandled",
1631
+ reason: decision.reason
1632
+ };
1633
+ }
1634
+ return decision;
1635
+ }
1636
+ };
1637
+
1638
+ // src/retry/standard-policy.ts
1639
+ var defaultRetryConfig = {
1640
+ maxAttempts: 3,
1641
+ baseDelay: 1e3,
1642
+ maxDelay: 3e5,
1643
+ // 5 minutes
1644
+ backoffMultiplier: 2,
1645
+ maxDeliveries: 5
1646
+ };
1647
+ var StandardRetryPolicy = class {
1648
+ config;
1649
+ constructor(config = {}) {
1650
+ this.config = { ...defaultRetryConfig, ...config };
1651
+ }
1652
+ shouldRetry(context) {
1653
+ const { envelope, error, subscriber, receipt } = context;
1654
+ const errorMessage = error.message;
1655
+ if (receipt.deliveryCount >= this.config.maxDeliveries) {
1656
+ const poisonError = new MessageMaybePoisonedError(
1657
+ envelope.id,
1658
+ receipt.deliveryCount,
1659
+ this.config.maxDeliveries
1660
+ );
1661
+ return {
1662
+ action: "dead-letter",
1663
+ queue: "undeliverable",
1664
+ reason: poisonError.message
1665
+ };
1666
+ }
1667
+ if (isAssertionError(error)) {
1668
+ return {
1669
+ action: "dead-letter",
1670
+ queue: "undeliverable",
1671
+ reason: `assertion error: ${errorMessage}`
1672
+ };
1673
+ }
1674
+ if (isDontRetry(error)) {
1675
+ return {
1676
+ action: "dead-letter",
1677
+ queue: "undeliverable",
1678
+ reason: errorMessage
1679
+ };
1680
+ }
1681
+ if (isDoRetry(error)) {
1682
+ if (receipt.attemptNumber >= this.config.maxAttempts) {
1683
+ return {
1684
+ action: "dead-letter",
1685
+ queue: "undeliverable",
1686
+ reason: `max attempts exceeded (${this.config.maxAttempts}) with forced retry`
1687
+ };
1688
+ }
1689
+ return {
1690
+ action: "retry",
1691
+ delay: this.getDelay(context)
1692
+ };
1693
+ }
1694
+ if (receipt.attemptNumber >= this.config.maxAttempts) {
1695
+ return {
1696
+ action: "dead-letter",
1697
+ queue: "undeliverable",
1698
+ reason: `max attempts exceeded (${this.config.maxAttempts})`
1699
+ };
1700
+ }
1701
+ if (receipt.redelivered && (subscriber.idempotent === "no" || subscriber.idempotent === "unknown")) {
1702
+ const idempotentError = new IdempotentMessageCannotRetryError(
1703
+ envelope.id,
1704
+ subscriber.name
1705
+ );
1706
+ return {
1707
+ action: "dead-letter",
1708
+ queue: "undeliverable",
1709
+ reason: idempotentError.message
1710
+ };
1711
+ }
1712
+ return {
1713
+ action: "retry",
1714
+ delay: this.getDelay(context)
1715
+ };
1716
+ }
1717
+ getDelay(context) {
1718
+ const attempt = context.receipt.attemptNumber;
1719
+ const delay = this.config.baseDelay * Math.pow(this.config.backoffMultiplier, attempt - 1);
1720
+ return Math.min(delay, this.config.maxDelay);
1721
+ }
1722
+ };
1723
+
1724
+ // src/schema/types.ts
1725
+ function bind(eventClass, subscribers) {
1726
+ return [eventClass, subscribers];
1727
+ }
1728
+ function isSchemaEntryTuple(entry) {
1729
+ return Array.isArray(entry);
1730
+ }
1731
+ function installPlugins(schema, plugins) {
1732
+ const result = {};
1733
+ for (const [eventKey, entry] of Object.entries(schema)) {
1734
+ const [eventClass, existingSubscribers] = isSchemaEntryTuple(entry) ? [entry[0], entry[1]] : [entry.eventClass, entry.subscribers];
1735
+ const pluginSubscribers = [];
1736
+ for (const plugin of plugins) {
1737
+ if (!plugin.exclusions?.includes(eventKey)) {
1738
+ pluginSubscribers.push(plugin.subscriber);
1739
+ }
1740
+ }
1741
+ result[eventKey] = [
1742
+ eventClass,
1743
+ [...existingSubscribers, ...pluginSubscribers]
1744
+ ];
1745
+ }
1746
+ return result;
1747
+ }
1748
+
1749
+ // src/schema/registry.ts
1750
+ var SchemaError = class extends Error {
1751
+ description = "A schema operation failed, such as registering a duplicate event or conflicting alias. This is a configuration error that should be fixed during development. Review the event and subscriber registrations to resolve the conflict.";
1752
+ constructor(message) {
1753
+ super(message);
1754
+ this.name = "SchemaError";
1755
+ }
1756
+ };
1757
+ var SchemaRegistry = class {
1758
+ entries = /* @__PURE__ */ new Map();
1759
+ aliases = /* @__PURE__ */ new Map();
1760
+ /**
1761
+ * Registers an event class with its subscribers.
1762
+ */
1763
+ register(eventClass, subscribers, options = {}) {
1764
+ const key = eventClass.key;
1765
+ if (this.entries.has(key) && !options.override) {
1766
+ throw new SchemaError(`Event "${key}" is already registered`);
1767
+ }
1768
+ this.entries.set(key, {
1769
+ eventClass,
1770
+ subscribers
1771
+ });
1772
+ if (eventClass.aliases) {
1773
+ for (const alias of eventClass.aliases) {
1774
+ if (this.aliases.has(alias) && this.aliases.get(alias) !== key && !options.override) {
1775
+ throw new SchemaError(
1776
+ `Alias "${alias}" is already registered for event "${this.aliases.get(alias)}"`
1777
+ );
1778
+ }
1779
+ this.aliases.set(alias, key);
1780
+ }
1781
+ }
1782
+ }
1783
+ /**
1784
+ * Gets the event class for a given key.
1785
+ */
1786
+ getEventClass(key) {
1787
+ const entry = this.entries.get(key);
1788
+ if (entry) return entry.eventClass;
1789
+ const aliasKey = this.aliases.get(key);
1790
+ if (aliasKey) {
1791
+ return this.entries.get(aliasKey)?.eventClass;
1792
+ }
1793
+ return void 0;
1794
+ }
1795
+ /**
1796
+ * Gets all subscribers for an event key.
1797
+ */
1798
+ getSubscribers(eventKey) {
1799
+ const entry = this.getEntry(eventKey);
1800
+ return entry?.subscribers ?? [];
1801
+ }
1802
+ /**
1803
+ * Gets a specific subscriber by event key and subscriber name.
1804
+ */
1805
+ getSubscriber(eventKey, subscriberName) {
1806
+ const subscribers = this.getSubscribers(eventKey);
1807
+ return subscribers.find((s) => s.name === subscriberName);
1808
+ }
1809
+ /**
1810
+ * Gets a subscriber definition for the pipeline.
1811
+ */
1812
+ getSubscriberDefinition(eventKey, subscriberName) {
1813
+ const subscriber = this.getSubscriber(eventKey, subscriberName);
1814
+ if (!subscriber) return void 0;
1815
+ const def = {
1816
+ name: subscriber.name,
1817
+ description: subscriber.description,
1818
+ idempotent: subscriber.idempotent ?? "unknown",
1819
+ importance: subscriber.importance ?? "should-investigate"
1820
+ };
1821
+ if (subscriber.targetQueue !== void 0) {
1822
+ def.targetQueue = subscriber.targetQueue;
1823
+ }
1824
+ return def;
1825
+ }
1826
+ /**
1827
+ * Gets a subscriber with callback for execution.
1828
+ */
1829
+ getExecutableSubscriber(eventKey, subscriberName) {
1830
+ const subscriber = this.getSubscriber(eventKey, subscriberName);
1831
+ if (!subscriber || !isSubscriber(subscriber)) return void 0;
1832
+ return subscriber;
1833
+ }
1834
+ /**
1835
+ * Gets the event class by alias.
1836
+ */
1837
+ getEventByAlias(alias) {
1838
+ const key = this.aliases.get(alias);
1839
+ if (!key) return void 0;
1840
+ return this.entries.get(key)?.eventClass;
1841
+ }
1842
+ /**
1843
+ * Checks if an event key is registered.
1844
+ */
1845
+ hasEvent(key) {
1846
+ return this.entries.has(key) || this.aliases.has(key);
1847
+ }
1848
+ /**
1849
+ * Gets all registered event keys.
1850
+ */
1851
+ getEventKeys() {
1852
+ return [...this.entries.keys()];
1853
+ }
1854
+ /**
1855
+ * Validates the schema for issues.
1856
+ */
1857
+ validate() {
1858
+ const issues = [];
1859
+ for (const [key, entry] of this.entries) {
1860
+ const names = /* @__PURE__ */ new Set();
1861
+ for (const subscriber of entry.subscribers) {
1862
+ if (names.has(subscriber.name)) {
1863
+ issues.push({
1864
+ severity: "error",
1865
+ eventKey: key,
1866
+ message: `Duplicate subscriber name: "${subscriber.name}"`
1867
+ });
1868
+ }
1869
+ names.add(subscriber.name);
1870
+ }
1871
+ const stubs = entry.subscribers.filter((s) => !isSubscriber(s));
1872
+ if (stubs.length === entry.subscribers.length && stubs.length > 0) {
1873
+ issues.push({
1874
+ severity: "warning",
1875
+ eventKey: key,
1876
+ message: "All subscribers are stubs, no local handlers"
1877
+ });
1878
+ }
1879
+ }
1880
+ return {
1881
+ valid: issues.filter((i) => i.severity === "error").length === 0,
1882
+ issues
1883
+ };
1884
+ }
1885
+ /**
1886
+ * Clears all registered entries.
1887
+ */
1888
+ clear() {
1889
+ this.entries.clear();
1890
+ this.aliases.clear();
1891
+ }
1892
+ getEntry(eventKey) {
1893
+ const entry = this.entries.get(eventKey);
1894
+ if (entry) return entry;
1895
+ const aliasKey = this.aliases.get(eventKey);
1896
+ if (aliasKey) {
1897
+ return this.entries.get(aliasKey);
1898
+ }
1899
+ return void 0;
1900
+ }
1901
+ };
1902
+
1903
+ // src/core/matador.ts
1904
+ var Matador = class {
1905
+ transport;
1906
+ topology;
1907
+ schema;
1908
+ codec;
1909
+ retryPolicy;
1910
+ hooks;
1911
+ pipeline;
1912
+ fanout;
1913
+ shutdownManager;
1914
+ consumeFrom;
1915
+ subscriptions = [];
1916
+ started = false;
1917
+ /**
1918
+ * Creates a new Matador instance.
1919
+ *
1920
+ * @param config - Static configuration (transport, topology, schema, etc.)
1921
+ * @param hooks - Lifecycle hooks for logging, monitoring, and dynamic configuration.
1922
+ * Passed separately to support NestJS dependency injection.
1923
+ */
1924
+ constructor(config, hooks) {
1925
+ this.transport = config.transport;
1926
+ this.topology = config.topology;
1927
+ this.consumeFrom = config.consumeFrom ?? [];
1928
+ this.schema = new SchemaRegistry();
1929
+ this.codec = config.codec ?? new JsonCodec();
1930
+ this.retryPolicy = config.retryPolicy ?? new StandardRetryPolicy();
1931
+ this.hooks = new SafeHooks(hooks);
1932
+ this.registerSchema(config.schema);
1933
+ this.pipeline = new ProcessingPipeline({
1934
+ transport: this.transport,
1935
+ schema: this.schema,
1936
+ codec: this.codec,
1937
+ retryPolicy: this.retryPolicy,
1938
+ hooks: this.hooks,
1939
+ checkpointStore: config.checkpointStore
1940
+ });
1941
+ const defaultQueue = this.topology.queues[0]?.name ?? "default";
1942
+ this.fanout = new FanoutEngine({
1943
+ transport: this.transport,
1944
+ schema: this.schema,
1945
+ hooks: this.hooks,
1946
+ namespace: this.topology.namespace,
1947
+ defaultQueue
1948
+ });
1949
+ this.shutdownManager = new ShutdownManager(
1950
+ () => this.fanout.eventsBeingEnqueuedCount,
1951
+ () => this.stopReceiving(),
1952
+ () => this.transport.disconnect(),
1953
+ config.shutdownConfig
1954
+ );
1955
+ }
1956
+ /**
1957
+ * Registers an event class with its subscribers.
1958
+ */
1959
+ register(eventClass, subscribers) {
1960
+ this.schema.register(eventClass, subscribers);
1961
+ return this;
1962
+ }
1963
+ /**
1964
+ * Registers events from a schema object.
1965
+ * Supports both object format and tuple format entries.
1966
+ *
1967
+ * @example
1968
+ * ```typescript
1969
+ * // Tuple format
1970
+ * matador.registerSchema({
1971
+ * [UserCreatedEvent.key]: [UserCreatedEvent, [emailSubscriber]],
1972
+ * [OrderPlacedEvent.key]: [OrderPlacedEvent, [invoiceSubscriber]],
1973
+ * });
1974
+ *
1975
+ * // Object format
1976
+ * matador.registerSchema({
1977
+ * [UserCreatedEvent.key]: { eventClass: UserCreatedEvent, subscribers: [emailSubscriber] },
1978
+ * });
1979
+ * ```
1980
+ */
1981
+ registerSchema(schema) {
1982
+ for (const entry of Object.values(schema)) {
1983
+ if (isSchemaEntryTuple(entry)) {
1984
+ const [eventClass, subscribers] = entry;
1985
+ this.schema.register(eventClass, subscribers);
1986
+ } else {
1987
+ this.schema.register(entry.eventClass, entry.subscribers);
1988
+ }
1989
+ }
1990
+ return this;
1991
+ }
1992
+ /**
1993
+ * Starts Matador - connects transport and begins consuming.
1994
+ * This method is idempotent - calling it multiple times is safe.
1995
+ */
1996
+ async start() {
1997
+ if (this.started) {
1998
+ return;
1999
+ }
2000
+ const validation = this.schema.validate();
2001
+ if (!validation.valid) {
2002
+ const errors = validation.issues.filter((i) => i.severity === "error");
2003
+ throw new InvalidSchemaError(
2004
+ "Schema validation failed",
2005
+ errors.map((e) => e.message).join(", ")
2006
+ );
2007
+ }
2008
+ await this.transport.connect();
2009
+ await this.transport.applyTopology(this.topology);
2010
+ for (const queueName of this.consumeFrom) {
2011
+ const qualifiedName = getQualifiedQueueName(
2012
+ this.topology.namespace,
2013
+ queueName
2014
+ );
2015
+ const queueDef = this.topology.queues.find((q) => q.name === queueName);
2016
+ const subscription = await this.transport.subscribe(
2017
+ qualifiedName,
2018
+ async (envelope, receipt) => {
2019
+ this.shutdownManager.incrementProcessing();
2020
+ try {
2021
+ const rawMessage = this.codec.encode(envelope);
2022
+ await this.pipeline.process(rawMessage, receipt);
2023
+ } finally {
2024
+ this.shutdownManager.decrementProcessing();
2025
+ }
2026
+ },
2027
+ queueDef?.concurrency !== void 0 ? { concurrency: queueDef.concurrency } : void 0
2028
+ );
2029
+ this.subscriptions.push(subscription);
2030
+ }
2031
+ this.started = true;
2032
+ }
2033
+ async send(eventOrClass, dataOrOptions, maybeOptions) {
2034
+ if (!this.started) {
2035
+ throw new NotStartedError("send");
2036
+ }
2037
+ if (!this.shutdownManager.isEnqueueAllowed) {
2038
+ throw new ShutdownInProgressError();
2039
+ }
2040
+ const isEventClass = typeof eventOrClass === "function" && "key" in eventOrClass;
2041
+ if (isEventClass) {
2042
+ const eventClass = eventOrClass;
2043
+ const data = dataOrOptions;
2044
+ const options = maybeOptions;
2045
+ const event = new eventClass(data);
2046
+ return this.fanout.send(eventClass, event, options);
2047
+ } else {
2048
+ const event = eventOrClass;
2049
+ const options = dataOrOptions;
2050
+ const eventClass = event.constructor;
2051
+ return this.fanout.send(eventClass, event, options);
2052
+ }
2053
+ }
2054
+ /**
2055
+ * Gets current handler state.
2056
+ */
2057
+ getHandlersState() {
2058
+ return this.shutdownManager.getHandlersState();
2059
+ }
2060
+ /**
2061
+ * Checks if Matador is idle (no processing or enqueuing).
2062
+ */
2063
+ isIdle() {
2064
+ return this.shutdownManager.getHandlersState().isIdle;
2065
+ }
2066
+ /**
2067
+ * Waits for all handlers to become idle.
2068
+ */
2069
+ async waitForIdle(timeoutMs = 3e4) {
2070
+ const deadline = Date.now() + timeoutMs;
2071
+ while (!this.isIdle()) {
2072
+ if (Date.now() > deadline) {
2073
+ return false;
2074
+ }
2075
+ await new Promise((resolve) => setTimeout(resolve, 100));
2076
+ }
2077
+ return true;
2078
+ }
2079
+ /**
2080
+ * Gracefully shuts down Matador.
2081
+ */
2082
+ async shutdown() {
2083
+ if (!this.started) {
2084
+ return;
2085
+ }
2086
+ await this.shutdownManager.shutdown();
2087
+ this.started = false;
2088
+ }
2089
+ /**
2090
+ * Checks if transport is connected.
2091
+ */
2092
+ isConnected() {
2093
+ return this.transport.isConnected();
2094
+ }
2095
+ async stopReceiving() {
2096
+ for (const subscription of this.subscriptions) {
2097
+ await subscription.unsubscribe();
2098
+ }
2099
+ this.subscriptions.length = 0;
2100
+ }
2101
+ };
2102
+
2103
+ // src/transport/capabilities.ts
2104
+ function supportsDeliveryMode(capabilities, mode) {
2105
+ return capabilities.deliveryModes.includes(mode);
2106
+ }
2107
+ function supportsDelayedMessages(capabilities) {
2108
+ return capabilities.delayedMessages;
2109
+ }
2110
+ function hasNativeDeadLetter(capabilities) {
2111
+ return capabilities.deadLetterRouting === "native";
2112
+ }
2113
+
2114
+ // src/transport/connection-manager.ts
2115
+ var defaultConnectionConfig = {
2116
+ maxReconnectAttempts: 0,
2117
+ // Infinite
2118
+ initialReconnectDelay: 1e3,
2119
+ maxReconnectDelay: 3e4,
2120
+ backoffMultiplier: 2
2121
+ };
2122
+ var ConnectionManager = class {
2123
+ constructor(connectFn, disconnectFn, config = {}) {
2124
+ this.connectFn = connectFn;
2125
+ this.disconnectFn = disconnectFn;
2126
+ this.config = { ...defaultConnectionConfig, ...config };
2127
+ }
2128
+ _state = { status: "disconnected" };
2129
+ listeners = /* @__PURE__ */ new Set();
2130
+ reconnectTimer = null;
2131
+ config;
2132
+ /**
2133
+ * Current connection state.
2134
+ */
2135
+ get state() {
2136
+ return this._state;
2137
+ }
2138
+ /**
2139
+ * Whether currently connected.
2140
+ */
2141
+ isConnected() {
2142
+ return this._state.status === "connected";
2143
+ }
2144
+ /**
2145
+ * Register a callback for connection state changes.
2146
+ */
2147
+ onStateChange(callback) {
2148
+ this.listeners.add(callback);
2149
+ return () => this.listeners.delete(callback);
2150
+ }
2151
+ /**
2152
+ * Attempt to establish connection.
2153
+ */
2154
+ async connect() {
2155
+ if (this._state.status === "connected") {
2156
+ return;
2157
+ }
2158
+ this.setState({ status: "connecting", attempt: 1 });
2159
+ try {
2160
+ await this.connectFn();
2161
+ this.setState({ status: "connected" });
2162
+ } catch (error) {
2163
+ const err = error instanceof Error ? error : new Error(String(error));
2164
+ await this.handleConnectionFailure(err, 1);
2165
+ }
2166
+ }
2167
+ /**
2168
+ * Gracefully disconnect.
2169
+ */
2170
+ async disconnect() {
2171
+ this.clearReconnectTimer();
2172
+ if (this._state.status === "disconnected") {
2173
+ return;
2174
+ }
2175
+ try {
2176
+ await this.disconnectFn();
2177
+ } finally {
2178
+ this.setState({ status: "disconnected" });
2179
+ }
2180
+ }
2181
+ /**
2182
+ * Called by transport when connection is lost unexpectedly.
2183
+ */
2184
+ async handleConnectionLost(error) {
2185
+ if (this._state.status === "disconnected") {
2186
+ return;
2187
+ }
2188
+ this.setState({ status: "reconnecting", attempt: 1, lastError: error });
2189
+ await this.attemptReconnect(1);
2190
+ }
2191
+ /**
2192
+ * Calculate delay for a given attempt using exponential backoff.
2193
+ */
2194
+ calculateDelay(attempt) {
2195
+ const delay = this.config.initialReconnectDelay * Math.pow(this.config.backoffMultiplier, attempt - 1);
2196
+ return Math.min(delay, this.config.maxReconnectDelay);
2197
+ }
2198
+ setState(state) {
2199
+ this._state = state;
2200
+ for (const listener of this.listeners) {
2201
+ try {
2202
+ listener(state);
2203
+ } catch {
2204
+ }
2205
+ }
2206
+ }
2207
+ async handleConnectionFailure(error, attempt) {
2208
+ if (this.shouldGiveUp(attempt)) {
2209
+ this.setState({ status: "failed", error });
2210
+ throw error;
2211
+ }
2212
+ this.setState({
2213
+ status: "reconnecting",
2214
+ attempt: attempt + 1,
2215
+ lastError: error
2216
+ });
2217
+ await this.attemptReconnect(attempt + 1);
2218
+ }
2219
+ shouldGiveUp(attempt) {
2220
+ return this.config.maxReconnectAttempts > 0 && attempt >= this.config.maxReconnectAttempts;
2221
+ }
2222
+ async attemptReconnect(attempt) {
2223
+ const delay = this.calculateDelay(attempt);
2224
+ await new Promise((resolve) => {
2225
+ this.reconnectTimer = setTimeout(resolve, delay);
2226
+ });
2227
+ if (this._state.status === "disconnected") {
2228
+ return;
2229
+ }
2230
+ try {
2231
+ await this.connectFn();
2232
+ this.setState({ status: "connected" });
2233
+ } catch (error) {
2234
+ const err = error instanceof Error ? error : new Error(String(error));
2235
+ await this.handleConnectionFailure(err, attempt);
2236
+ }
2237
+ }
2238
+ clearReconnectTimer() {
2239
+ if (this.reconnectTimer !== null) {
2240
+ clearTimeout(this.reconnectTimer);
2241
+ this.reconnectTimer = null;
2242
+ }
2243
+ }
2244
+ };
2245
+
2246
+ // src/transport/local/local-transport.ts
2247
+ var localCapabilities = {
2248
+ deliveryModes: ["at-least-once", "at-most-once"],
2249
+ delayedMessages: true,
2250
+ // Implemented with setTimeout
2251
+ deadLetterRouting: "manual",
2252
+ attemptTracking: false,
2253
+ concurrencyModel: "none",
2254
+ ordering: "queue",
2255
+ priorities: false
2256
+ };
2257
+ var LocalTransport = class {
2258
+ name = "local";
2259
+ capabilities = localCapabilities;
2260
+ connected = false;
2261
+ queues = /* @__PURE__ */ new Map();
2262
+ subscriptions = /* @__PURE__ */ new Map();
2263
+ completedMessages = [];
2264
+ delayedTimers = /* @__PURE__ */ new Set();
2265
+ messageIdCounter = 0;
2266
+ logger;
2267
+ constructor(logger) {
2268
+ this.logger = logger ?? consoleLogger;
2269
+ }
2270
+ async connect() {
2271
+ this.connected = true;
2272
+ }
2273
+ async disconnect() {
2274
+ for (const timer of this.delayedTimers) {
2275
+ clearTimeout(timer);
2276
+ }
2277
+ this.delayedTimers.clear();
2278
+ for (const subs of this.subscriptions.values()) {
2279
+ for (const sub of subs) {
2280
+ sub.active = false;
2281
+ }
2282
+ }
2283
+ this.connected = false;
2284
+ }
2285
+ isConnected() {
2286
+ return this.connected;
2287
+ }
2288
+ async applyTopology(topology) {
2289
+ if (!this.connected) {
2290
+ throw new TransportNotConnectedError(this.name, "applyTopology");
2291
+ }
2292
+ for (const queueDef of topology.queues) {
2293
+ const queueName = `${topology.namespace}.${queueDef.name}`;
2294
+ if (!this.queues.has(queueName)) {
2295
+ this.queues.set(queueName, []);
2296
+ }
2297
+ }
2298
+ }
2299
+ async send(queue, envelope, options) {
2300
+ if (!this.connected) {
2301
+ throw new TransportNotConnectedError(this.name, "send");
2302
+ }
2303
+ if (options?.delay !== void 0 && options.delay > 0) {
2304
+ this.scheduleDelayedMessage(queue, envelope, options.delay);
2305
+ return this.name;
2306
+ }
2307
+ await this.enqueue(queue, envelope);
2308
+ return this.name;
2309
+ }
2310
+ /**
2311
+ * Schedules a message for delayed delivery.
2312
+ * Returns immediately (non-blocking) to match real transport behavior.
2313
+ */
2314
+ scheduleDelayedMessage(queue, envelope, delayMs) {
2315
+ const timer = setTimeout(() => {
2316
+ this.delayedTimers.delete(timer);
2317
+ this.enqueue(queue, envelope).catch((error) => {
2318
+ this.logger.error(
2319
+ "[Matador] \u{1F534} Failed to enqueue delayed message",
2320
+ error
2321
+ );
2322
+ });
2323
+ }, delayMs);
2324
+ this.delayedTimers.add(timer);
2325
+ }
2326
+ async enqueue(queue, envelope) {
2327
+ const messages = this.getOrCreateQueue(queue);
2328
+ const messageId = `${++this.messageIdCounter}`;
2329
+ const queuedMessage = {
2330
+ envelope,
2331
+ id: messageId,
2332
+ completed: false
2333
+ };
2334
+ messages.push(queuedMessage);
2335
+ await this.deliverToSubscribers(queue, queuedMessage);
2336
+ }
2337
+ async deliverToSubscribers(queue, message) {
2338
+ const subs = this.subscriptions.get(queue);
2339
+ if (!subs) return;
2340
+ for (const sub of subs) {
2341
+ if (!sub.active || message.completed) continue;
2342
+ const receipt = {
2343
+ handle: message,
2344
+ redelivered: false,
2345
+ attemptNumber: message.envelope.docket.attempts,
2346
+ deliveryCount: message.envelope.docket.attempts,
2347
+ sourceQueue: queue,
2348
+ sourceTransport: this.name
2349
+ };
2350
+ try {
2351
+ await sub.handler(message.envelope, receipt);
2352
+ } catch (error) {
2353
+ this.logger.error("\u{1F534} Handler error in message processing", error);
2354
+ }
2355
+ }
2356
+ }
2357
+ async subscribe(queue, handler, options = {}) {
2358
+ if (!this.connected) {
2359
+ throw new TransportNotConnectedError(this.name, "subscribe");
2360
+ }
2361
+ const subscription = {
2362
+ handler,
2363
+ options,
2364
+ active: true
2365
+ };
2366
+ const subs = this.subscriptions.get(queue) ?? [];
2367
+ subs.push(subscription);
2368
+ this.subscriptions.set(queue, subs);
2369
+ const messages = this.queues.get(queue) ?? [];
2370
+ for (const message of messages) {
2371
+ if (message.completed) continue;
2372
+ await this.deliverToSubscribers(queue, message);
2373
+ }
2374
+ return {
2375
+ unsubscribe: async () => {
2376
+ subscription.active = false;
2377
+ const remaining = subs.filter((s) => s !== subscription);
2378
+ if (remaining.length > 0) {
2379
+ this.subscriptions.set(queue, remaining);
2380
+ } else {
2381
+ this.subscriptions.delete(queue);
2382
+ }
2383
+ },
2384
+ get isActive() {
2385
+ return subscription.active;
2386
+ }
2387
+ };
2388
+ }
2389
+ async complete(receipt) {
2390
+ const message = receipt.handle;
2391
+ message.completed = true;
2392
+ this.completedMessages.push(receipt);
2393
+ }
2394
+ async sendToDeadLetter(receipt, dlqName, envelope, _reason) {
2395
+ const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
2396
+ await this.enqueue(dlqQueueName, envelope);
2397
+ await this.complete(receipt);
2398
+ }
2399
+ // Test helpers
2400
+ /**
2401
+ * Gets the current size of a queue.
2402
+ */
2403
+ getQueueSize(queue) {
2404
+ const messages = this.queues.get(queue);
2405
+ if (!messages) return 0;
2406
+ return messages.filter((m) => !m.completed).length;
2407
+ }
2408
+ /**
2409
+ * Gets all completed message receipts.
2410
+ */
2411
+ getCompleted() {
2412
+ return this.completedMessages;
2413
+ }
2414
+ /**
2415
+ * Gets pending (uncompleted) messages from a queue.
2416
+ */
2417
+ getPendingMessages(queue) {
2418
+ const messages = this.queues.get(queue);
2419
+ if (!messages) return [];
2420
+ return messages.filter((m) => !m.completed).map((m) => m.envelope);
2421
+ }
2422
+ /**
2423
+ * Clears all state (for test isolation).
2424
+ */
2425
+ clear() {
2426
+ this.queues.clear();
2427
+ this.subscriptions.clear();
2428
+ this.completedMessages.length = 0;
2429
+ this.messageIdCounter = 0;
2430
+ for (const timer of this.delayedTimers) {
2431
+ clearTimeout(timer);
2432
+ }
2433
+ this.delayedTimers.clear();
2434
+ }
2435
+ /**
2436
+ * Receives one message from the queue without a subscription.
2437
+ * Useful for testing.
2438
+ */
2439
+ async receiveOne(queue) {
2440
+ const messages = this.queues.get(queue);
2441
+ if (!messages) return null;
2442
+ const pending = messages.find((m) => !m.completed);
2443
+ if (!pending) return null;
2444
+ const receipt = {
2445
+ handle: pending,
2446
+ redelivered: false,
2447
+ attemptNumber: pending.envelope.docket.attempts,
2448
+ deliveryCount: pending.envelope.docket.attempts,
2449
+ sourceQueue: queue,
2450
+ sourceTransport: this.name
2451
+ };
2452
+ return { envelope: pending.envelope, receipt };
2453
+ }
2454
+ getOrCreateQueue(queue) {
2455
+ let messages = this.queues.get(queue);
2456
+ if (!messages) {
2457
+ messages = [];
2458
+ this.queues.set(queue, messages);
2459
+ }
2460
+ return messages;
2461
+ }
2462
+ };
2463
+
2464
+ // src/transport/multi/multi-transport.ts
2465
+ var SOURCE_TRANSPORT = /* @__PURE__ */ Symbol("sourceTransport");
2466
+ function getSourceTransport(receipt) {
2467
+ return receipt[SOURCE_TRANSPORT];
2468
+ }
2469
+ function mergeCapabilities(transports) {
2470
+ const primary = transports[0];
2471
+ if (!primary) {
2472
+ throw new Error("At least one transport is required");
2473
+ }
2474
+ return primary.capabilities;
2475
+ }
2476
+ var MultiTransport = class {
2477
+ name;
2478
+ capabilities;
2479
+ primary;
2480
+ /** All available transports, in order of preference (primary first). */
2481
+ transports;
2482
+ /** Whether fallback to secondary transports is enabled. */
2483
+ fallbackEnabled;
2484
+ hooks;
2485
+ connected = false;
2486
+ constructor(config, hooks = {}) {
2487
+ const primary = config.transports[0];
2488
+ if (!primary) {
2489
+ throw new Error("At least one transport is required");
2490
+ }
2491
+ this.primary = primary;
2492
+ this.transports = config.transports;
2493
+ this.fallbackEnabled = config.fallbackEnabled ?? true;
2494
+ this.hooks = hooks;
2495
+ this.name = `multi(${this.transports.map((t) => t.name).join(",")})`;
2496
+ this.capabilities = mergeCapabilities(this.transports);
2497
+ }
2498
+ async connect() {
2499
+ await Promise.all(this.transports.map((t) => t.connect()));
2500
+ this.connected = true;
2501
+ }
2502
+ async disconnect() {
2503
+ await Promise.all(this.transports.map((t) => t.disconnect()));
2504
+ this.connected = false;
2505
+ }
2506
+ isConnected() {
2507
+ return this.connected && this.primary.isConnected();
2508
+ }
2509
+ async applyTopology(topology) {
2510
+ await Promise.all(this.transports.map((t) => t.applyTopology(topology)));
2511
+ }
2512
+ async send(queue, envelope, options) {
2513
+ const selectedTransport = await this.selectTransport();
2514
+ if (!this.fallbackEnabled) {
2515
+ return selectedTransport.send(queue, envelope, options);
2516
+ }
2517
+ const transportOrder = this.getTransportOrder(selectedTransport);
2518
+ const errors = [];
2519
+ for (let i = 0; i < transportOrder.length; i++) {
2520
+ const transport = transportOrder[i];
2521
+ try {
2522
+ return await transport.send(queue, envelope, options);
2523
+ } catch (error) {
2524
+ const err = error instanceof Error ? error : new Error(String(error));
2525
+ errors.push(err);
2526
+ const nextTransport = transportOrder[i + 1];
2527
+ if (nextTransport && this.hooks.onEnqueueFallback) {
2528
+ this.hooks.onEnqueueFallback({
2529
+ envelope,
2530
+ queue,
2531
+ failedTransport: transport.name,
2532
+ nextTransport: nextTransport.name,
2533
+ error: err
2534
+ });
2535
+ }
2536
+ }
2537
+ }
2538
+ throw new AllTransportsFailedError(queue, errors);
2539
+ }
2540
+ /**
2541
+ * Selects the transport to use based on the getDesiredBackend hook.
2542
+ */
2543
+ async selectTransport() {
2544
+ if (!this.hooks.getDesiredBackend) {
2545
+ return this.primary;
2546
+ }
2547
+ try {
2548
+ const desiredBackend = await this.hooks.getDesiredBackend();
2549
+ if (!desiredBackend) {
2550
+ return this.primary;
2551
+ }
2552
+ const selected = this.transports.find((t) => t.name === desiredBackend);
2553
+ if (selected) {
2554
+ return selected;
2555
+ }
2556
+ return this.primary;
2557
+ } catch {
2558
+ return this.primary;
2559
+ }
2560
+ }
2561
+ /**
2562
+ * Returns transports in order, with the selected transport first.
2563
+ */
2564
+ getTransportOrder(selected) {
2565
+ if (selected === this.primary) {
2566
+ return this.transports;
2567
+ }
2568
+ return [selected, ...this.transports.filter((t) => t !== selected)];
2569
+ }
2570
+ async subscribe(queue, handler, options) {
2571
+ const subscriptions = await Promise.all(
2572
+ this.transports.map((transport) => {
2573
+ const wrappedHandler = (envelope, receipt) => {
2574
+ const taggedReceipt = Object.assign(receipt, {
2575
+ [SOURCE_TRANSPORT]: transport
2576
+ });
2577
+ return handler(envelope, taggedReceipt);
2578
+ };
2579
+ return transport.subscribe(queue, wrappedHandler, options);
2580
+ })
2581
+ );
2582
+ return {
2583
+ unsubscribe: async () => {
2584
+ await Promise.all(subscriptions.map((s) => s.unsubscribe()));
2585
+ },
2586
+ get isActive() {
2587
+ return subscriptions.some((s) => s.isActive);
2588
+ }
2589
+ };
2590
+ }
2591
+ async complete(receipt) {
2592
+ const transport = getSourceTransport(receipt) ?? this.primary;
2593
+ return transport.complete(receipt);
2594
+ }
2595
+ async sendToDeadLetter(receipt, dlqName, envelope, reason) {
2596
+ const transport = getSourceTransport(receipt) ?? this.primary;
2597
+ if (transport.sendToDeadLetter) {
2598
+ return transport.sendToDeadLetter(receipt, dlqName, envelope, reason);
2599
+ }
2600
+ throw new Error("Transport does not support dead letter routing");
2601
+ }
2602
+ };
2603
+ var RabbitMQTransport = class {
2604
+ name = "rabbitmq";
2605
+ _capabilities = {
2606
+ deliveryModes: ["at-least-once"],
2607
+ delayedMessages: false,
2608
+ deadLetterRouting: "native",
2609
+ attemptTracking: true,
2610
+ concurrencyModel: "prefetch",
2611
+ ordering: "none",
2612
+ priorities: true
2613
+ };
2614
+ get capabilities() {
2615
+ return this._capabilities;
2616
+ }
2617
+ connection = null;
2618
+ publishChannel = null;
2619
+ connectionManager;
2620
+ queueChannels = /* @__PURE__ */ new Map();
2621
+ topology = null;
2622
+ codec = new RabbitMQCodec();
2623
+ config;
2624
+ logger;
2625
+ delayedExchangeAvailable = false;
2626
+ constructor(config) {
2627
+ this.logger = config.logger ?? consoleLogger;
2628
+ this.config = {
2629
+ url: config.url,
2630
+ connection: config.connection ?? {},
2631
+ quorumQueues: config.quorumQueues ?? true,
2632
+ defaultPrefetch: config.defaultPrefetch ?? 10,
2633
+ enableDelayedMessages: config.enableDelayedMessages ?? true
2634
+ };
2635
+ this.connectionManager = new ConnectionManager(
2636
+ () => this.doConnect(),
2637
+ () => this.doDisconnect(),
2638
+ this.config.connection
2639
+ );
2640
+ }
2641
+ async connect() {
2642
+ await this.connectionManager.connect();
2643
+ }
2644
+ async disconnect() {
2645
+ await this.connectionManager.disconnect();
2646
+ }
2647
+ isConnected() {
2648
+ return this.connectionManager.isConnected();
2649
+ }
2650
+ async applyTopology(topology) {
2651
+ this.topology = topology;
2652
+ if (!this.publishChannel) {
2653
+ throw new TransportNotConnectedError(this.name, "applyTopology");
2654
+ }
2655
+ const channel = this.publishChannel;
2656
+ const mainExchange = this.getMainExchangeName(topology.namespace);
2657
+ await channel.assertExchange(mainExchange, "direct", { durable: true });
2658
+ const dlxExchange = this.getDLXExchangeName(topology.namespace);
2659
+ if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
2660
+ await channel.assertExchange(dlxExchange, "direct", { durable: true });
2661
+ }
2662
+ if (this.config.enableDelayedMessages) {
2663
+ await this.setupDelayedExchange(topology.namespace);
2664
+ }
2665
+ for (const queueDef of topology.queues) {
2666
+ await this.assertWorkQueue(channel, topology, queueDef);
2667
+ }
2668
+ if (topology.deadLetter.unhandled.enabled) {
2669
+ await this.assertDeadLetterQueues(channel, topology, "unhandled");
2670
+ }
2671
+ if (topology.deadLetter.undeliverable.enabled) {
2672
+ await this.assertDeadLetterQueues(channel, topology, "undeliverable");
2673
+ }
2674
+ }
2675
+ async send(queue, envelope, options) {
2676
+ if (!this.publishChannel || !this.topology) {
2677
+ throw new TransportNotConnectedError(this.name, "send");
2678
+ }
2679
+ const encoded = this.codec.encode(envelope);
2680
+ const buffer = Buffer.from(encoded.body);
2681
+ const publishOptions = {
2682
+ persistent: true,
2683
+ contentType: encoded.contentType,
2684
+ messageId: envelope.id,
2685
+ timestamp: Date.now(),
2686
+ headers: encoded.headers
2687
+ };
2688
+ if (options?.priority !== void 0) {
2689
+ publishOptions.priority = options.priority;
2690
+ }
2691
+ if (options?.delay !== void 0 && options.delay > 0) {
2692
+ if (!this.delayedExchangeAvailable) {
2693
+ throw new DelayedMessagesNotSupportedError(this.name);
2694
+ }
2695
+ const delayedExchange = this.getDelayedExchangeName(
2696
+ this.topology.namespace
2697
+ );
2698
+ publishOptions.headers = {
2699
+ ...publishOptions.headers,
2700
+ "x-delay": options.delay
2701
+ };
2702
+ this.publishChannel.publish(
2703
+ delayedExchange,
2704
+ queue,
2705
+ buffer,
2706
+ publishOptions
2707
+ );
2708
+ return this.name;
2709
+ }
2710
+ if (options?.transport?.rabbitmq?.expiration !== void 0) {
2711
+ publishOptions.expiration = String(options.transport.rabbitmq.expiration);
2712
+ }
2713
+ if (options?.transport?.rabbitmq?.persistent !== void 0) {
2714
+ publishOptions.persistent = options.transport.rabbitmq.persistent;
2715
+ }
2716
+ const routingKey = options?.transport?.rabbitmq?.routingKey ?? queue;
2717
+ const exchange = this.getMainExchangeName(this.topology.namespace);
2718
+ this.publishChannel.publish(exchange, routingKey, buffer, publishOptions);
2719
+ return this.name;
2720
+ }
2721
+ async subscribe(queue, handler, options = {}) {
2722
+ if (!this.connection || !this.topology) {
2723
+ throw new TransportNotConnectedError(this.name, "subscribe");
2724
+ }
2725
+ const queueChannel = await this.getOrCreateQueueChannel(queue, options);
2726
+ const { channel } = queueChannel;
2727
+ const consumer = {
2728
+ consumerTag: "",
2729
+ queue,
2730
+ active: true
2731
+ };
2732
+ const { consumerTag } = await channel.consume(
2733
+ queue,
2734
+ async (msg) => {
2735
+ if (!msg || !consumer.active) return;
2736
+ const attemptNumber = this.getAttemptNumber(msg);
2737
+ const receipt = {
2738
+ handle: { channel, msg },
2739
+ redelivered: msg.fields.redelivered,
2740
+ attemptNumber,
2741
+ deliveryCount: this.getDeliveryCount(msg, attemptNumber),
2742
+ sourceQueue: queue,
2743
+ sourceTransport: this.name
2744
+ };
2745
+ try {
2746
+ const headers = msg.properties.headers ?? {};
2747
+ const envelope = this.codec.decode(
2748
+ new Uint8Array(msg.content),
2749
+ headers
2750
+ );
2751
+ await handler(envelope, receipt);
2752
+ } catch (error) {
2753
+ this.logger.error(
2754
+ "[Matador] \u{1F534} Handler error in message processing",
2755
+ error
2756
+ );
2757
+ }
2758
+ },
2759
+ { noAck: false }
2760
+ // Always manually ack
2761
+ );
2762
+ consumer.consumerTag = consumerTag;
2763
+ queueChannel.consumers.push(consumer);
2764
+ return {
2765
+ unsubscribe: async () => {
2766
+ consumer.active = false;
2767
+ try {
2768
+ await channel.cancel(consumerTag);
2769
+ } catch {
2770
+ }
2771
+ const idx = queueChannel.consumers.indexOf(consumer);
2772
+ if (idx !== -1) {
2773
+ queueChannel.consumers.splice(idx, 1);
2774
+ }
2775
+ if (queueChannel.consumers.length === 0) {
2776
+ try {
2777
+ await channel.close();
2778
+ } catch {
2779
+ }
2780
+ this.queueChannels.delete(queue);
2781
+ }
2782
+ },
2783
+ get isActive() {
2784
+ return consumer.active;
2785
+ }
2786
+ };
2787
+ }
2788
+ async complete(receipt) {
2789
+ const { channel, msg } = receipt.handle;
2790
+ try {
2791
+ channel.ack(msg);
2792
+ } catch {
2793
+ }
2794
+ }
2795
+ async sendToDeadLetter(receipt, dlqName, envelope, reason) {
2796
+ if (!this.publishChannel || !this.topology) {
2797
+ throw new TransportNotConnectedError(this.name, "sendToDeadLetter");
2798
+ }
2799
+ const dlqEnvelope = {
2800
+ ...envelope,
2801
+ docket: {
2802
+ ...envelope.docket,
2803
+ lastError: reason,
2804
+ firstError: envelope.docket.firstError ?? reason,
2805
+ originalQueue: receipt.sourceQueue
2806
+ }
2807
+ };
2808
+ const encoded = this.codec.encode(dlqEnvelope);
2809
+ const buffer = Buffer.from(encoded.body);
2810
+ const dlxExchange = this.getDLXExchangeName(this.topology.namespace);
2811
+ const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
2812
+ const publishOptions = {
2813
+ persistent: true,
2814
+ contentType: encoded.contentType,
2815
+ messageId: envelope.id,
2816
+ timestamp: Date.now(),
2817
+ headers: {
2818
+ ...encoded.headers,
2819
+ "x-matador-dead-letter-reason": reason
2820
+ }
2821
+ };
2822
+ this.publishChannel.publish(
2823
+ dlxExchange,
2824
+ dlqQueueName,
2825
+ buffer,
2826
+ publishOptions
2827
+ );
2828
+ await this.complete(receipt);
2829
+ }
2830
+ // Private methods
2831
+ /**
2832
+ * Gets or creates a dedicated channel for a queue subscription.
2833
+ *
2834
+ * We create separate channels per subscribed queue to enable independent
2835
+ * prefetch/concurrency control.
2836
+ */
2837
+ async getOrCreateQueueChannel(queue, options) {
2838
+ const existing = this.queueChannels.get(queue);
2839
+ if (existing) {
2840
+ return existing;
2841
+ }
2842
+ if (!this.connection) {
2843
+ throw new TransportNotConnectedError(
2844
+ this.name,
2845
+ "getOrCreateQueueChannel"
2846
+ );
2847
+ }
2848
+ const channel = await this.connection.createChannel();
2849
+ const prefetch = options.transport?.rabbitmq?.prefetch ?? options.concurrency ?? this.config.defaultPrefetch ?? 10;
2850
+ await channel.prefetch(prefetch);
2851
+ const queueChannel = {
2852
+ channel,
2853
+ consumers: []
2854
+ };
2855
+ this.queueChannels.set(queue, queueChannel);
2856
+ return queueChannel;
2857
+ }
2858
+ async doConnect() {
2859
+ const connection = await amqplib__default.default.connect(this.config.url);
2860
+ this.connection = connection;
2861
+ connection.on("error", (err) => {
2862
+ this.logger.error("[Matador] \u{1F534} RabbitMQ connection error", err);
2863
+ });
2864
+ connection.on("close", () => {
2865
+ if (this.connectionManager.isConnected()) {
2866
+ this.connectionManager.handleConnectionLost(
2867
+ new Error("Connection closed unexpectedly")
2868
+ );
2869
+ }
2870
+ });
2871
+ this.publishChannel = await connection.createChannel();
2872
+ this.publishChannel.on("error", (err) => {
2873
+ this.logger.error("[Matador] \u{1F534} RabbitMQ publish channel error", err);
2874
+ });
2875
+ if (this.topology) {
2876
+ await this.applyTopology(this.topology);
2877
+ }
2878
+ }
2879
+ async doDisconnect() {
2880
+ for (const queueChannel of this.queueChannels.values()) {
2881
+ for (const consumer of queueChannel.consumers) {
2882
+ consumer.active = false;
2883
+ try {
2884
+ await queueChannel.channel.cancel(consumer.consumerTag);
2885
+ } catch {
2886
+ }
2887
+ }
2888
+ try {
2889
+ await queueChannel.channel.close();
2890
+ } catch {
2891
+ }
2892
+ }
2893
+ this.queueChannels.clear();
2894
+ if (this.publishChannel) {
2895
+ try {
2896
+ await this.publishChannel.close();
2897
+ } catch {
2898
+ }
2899
+ this.publishChannel = null;
2900
+ }
2901
+ if (this.connection) {
2902
+ try {
2903
+ await this.connection.close();
2904
+ } catch {
2905
+ }
2906
+ this.connection = null;
2907
+ }
2908
+ this.delayedExchangeAvailable = false;
2909
+ this._capabilities = {
2910
+ ...this._capabilities,
2911
+ delayedMessages: false
2912
+ };
2913
+ }
2914
+ async setupDelayedExchange(namespace) {
2915
+ if (!this.connection) {
2916
+ return;
2917
+ }
2918
+ this.delayedExchangeAvailable = false;
2919
+ const delayedExchange = this.getDelayedExchangeName(namespace);
2920
+ const connection = this.connection;
2921
+ return new Promise((resolve) => {
2922
+ let resolved = false;
2923
+ const safeResolve = () => {
2924
+ if (!resolved) {
2925
+ resolved = true;
2926
+ resolve();
2927
+ }
2928
+ };
2929
+ connection.createChannel().then((probeChannel) => {
2930
+ probeChannel.on("error", () => {
2931
+ safeResolve();
2932
+ });
2933
+ probeChannel.on("close", () => {
2934
+ safeResolve();
2935
+ });
2936
+ probeChannel.assertExchange(delayedExchange, "x-delayed-message", {
2937
+ durable: true,
2938
+ arguments: { "x-delayed-type": "direct" }
2939
+ }).then(() => {
2940
+ this.delayedExchangeAvailable = true;
2941
+ this._capabilities = {
2942
+ ...this._capabilities,
2943
+ delayedMessages: true
2944
+ };
2945
+ this.logger.debug(
2946
+ "[Matador] \u{1F50C} Delayed message exchange plugin detected"
2947
+ );
2948
+ probeChannel.close().catch(() => {
2949
+ });
2950
+ safeResolve();
2951
+ }).catch(() => {
2952
+ this.logger.warn(
2953
+ "[Matador] \u{1F7E1} RabbitMQ delayed message exchange plugin not available. Delayed messages will not be supported."
2954
+ );
2955
+ safeResolve();
2956
+ });
2957
+ }).catch(() => {
2958
+ safeResolve();
2959
+ });
2960
+ });
2961
+ }
2962
+ async assertWorkQueue(channel, topology, queueDef) {
2963
+ const queueName = queueDef.exact ? queueDef.name : `${topology.namespace}.${queueDef.name}`;
2964
+ const queueOptions = {
2965
+ durable: true,
2966
+ arguments: {}
2967
+ };
2968
+ if (this.config.quorumQueues && !queueDef.exact) {
2969
+ queueOptions.arguments["x-queue-type"] = "quorum";
2970
+ }
2971
+ const dlxExchange = this.getDLXExchangeName(topology.namespace);
2972
+ if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
2973
+ queueOptions.arguments["x-dead-letter-exchange"] = dlxExchange;
2974
+ }
2975
+ if (queueDef.priorities) {
2976
+ queueOptions.arguments["x-max-priority"] = 10;
2977
+ }
2978
+ if (queueDef.consumerTimeout) {
2979
+ queueOptions.arguments["x-consumer-timeout"] = queueDef.consumerTimeout;
2980
+ }
2981
+ await channel.assertQueue(queueName, queueOptions);
2982
+ const mainExchange = this.getMainExchangeName(topology.namespace);
2983
+ await channel.bindQueue(queueName, mainExchange, queueName);
2984
+ if (this.delayedExchangeAvailable) {
2985
+ const delayedExchange = this.getDelayedExchangeName(topology.namespace);
2986
+ await channel.bindQueue(queueName, delayedExchange, queueName);
2987
+ }
2988
+ if (topology.retry.enabled) {
2989
+ await this.assertRetryQueue(channel, topology, queueName);
2990
+ }
2991
+ }
2992
+ async assertRetryQueue(channel, topology, workQueueName) {
2993
+ const retryQueueName = `${workQueueName}.retry`;
2994
+ const mainExchange = this.getMainExchangeName(topology.namespace);
2995
+ const retryQueueOptions = {
2996
+ durable: true,
2997
+ arguments: {
2998
+ "x-dead-letter-exchange": mainExchange,
2999
+ "x-dead-letter-routing-key": workQueueName,
3000
+ "x-message-ttl": topology.retry.defaultDelayMs
3001
+ }
3002
+ };
3003
+ if (this.config.quorumQueues) {
3004
+ retryQueueOptions.arguments["x-queue-type"] = "quorum";
3005
+ }
3006
+ await channel.assertQueue(retryQueueName, retryQueueOptions);
3007
+ await channel.bindQueue(retryQueueName, mainExchange, retryQueueName);
3008
+ }
3009
+ async assertDeadLetterQueues(channel, topology, dlqType) {
3010
+ const dlxExchange = this.getDLXExchangeName(topology.namespace);
3011
+ const dlConfig = topology.deadLetter[dlqType];
3012
+ for (const queueDef of topology.queues) {
3013
+ if (queueDef.exact) continue;
3014
+ const workQueueName = `${topology.namespace}.${queueDef.name}`;
3015
+ const dlqName = `${workQueueName}.${dlqType}`;
3016
+ const dlqOptions = {
3017
+ durable: true,
3018
+ arguments: {}
3019
+ };
3020
+ if (dlConfig.maxLength) {
3021
+ dlqOptions.arguments["x-max-length"] = dlConfig.maxLength;
3022
+ }
3023
+ await channel.assertQueue(dlqName, dlqOptions);
3024
+ await channel.bindQueue(dlqName, dlxExchange, dlqName);
3025
+ }
3026
+ }
3027
+ getMainExchangeName(namespace) {
3028
+ return `${namespace}.exchange`;
3029
+ }
3030
+ getDLXExchangeName(namespace) {
3031
+ return `${namespace}.dlx`;
3032
+ }
3033
+ getDelayedExchangeName(namespace) {
3034
+ return `${namespace}.delayed`;
3035
+ }
3036
+ getAttemptNumber(msg) {
3037
+ const headerValue = msg.properties.headers?.["x-matador-attempts"];
3038
+ if (typeof headerValue === "number") {
3039
+ return headerValue;
3040
+ }
3041
+ const xDeath = msg.properties.headers?.["x-death"];
3042
+ if (Array.isArray(xDeath) && xDeath.length > 0) {
3043
+ const deathCount = xDeath.reduce(
3044
+ (sum, death) => sum + (death.count ?? 0),
3045
+ 0
3046
+ );
3047
+ return deathCount + 1;
3048
+ }
3049
+ return 1;
3050
+ }
3051
+ /**
3052
+ * Gets the native delivery count for poison message detection.
3053
+ * This tracks how many times the message was delivered without acknowledgment,
3054
+ * which helps detect crash loops.
3055
+ */
3056
+ getDeliveryCount(msg, attemptNumber) {
3057
+ const deliveryCount = msg.properties.headers?.["x-delivery-count"];
3058
+ if (typeof deliveryCount === "number") {
3059
+ return deliveryCount;
3060
+ }
3061
+ const xDeath = msg.properties.headers?.["x-death"];
3062
+ if (Array.isArray(xDeath) && xDeath.length > 0) {
3063
+ const deathCount = xDeath.reduce(
3064
+ (sum, death) => sum + (death.count ?? 0),
3065
+ 0
3066
+ );
3067
+ return deathCount + 1;
3068
+ }
3069
+ if (msg.fields.redelivered) {
3070
+ return Math.max(2, attemptNumber);
3071
+ }
3072
+ return attemptNumber;
3073
+ }
3074
+ };
3075
+
3076
+ exports.AllTransportsFailedError = AllTransportsFailedError;
3077
+ exports.CheckpointStoreError = CheckpointStoreError;
3078
+ exports.CodecDecodeError = CodecDecodeError;
3079
+ exports.ConnectionManager = ConnectionManager;
3080
+ exports.DelayedMessagesNotSupportedError = DelayedMessagesNotSupportedError;
3081
+ exports.DoRetry = DoRetry;
3082
+ exports.DontRetry = DontRetry;
3083
+ exports.DuplicateIoKeyError = DuplicateIoKeyError;
3084
+ exports.EventAssertionError = EventAssertionError;
3085
+ exports.EventNotRegisteredError = EventNotRegisteredError;
3086
+ exports.FanoutEngine = FanoutEngine;
3087
+ exports.IdempotentMessageCannotRetryError = IdempotentMessageCannotRetryError;
3088
+ exports.InvalidEventError = InvalidEventError;
3089
+ exports.InvalidSchemaError = InvalidSchemaError;
3090
+ exports.JsonCodec = JsonCodec;
3091
+ exports.LocalTransport = LocalTransport;
3092
+ exports.LocalTransportCannotProcessStubError = LocalTransportCannotProcessStubError;
3093
+ exports.Matador = Matador;
3094
+ exports.MatadorError = MatadorError;
3095
+ exports.MatadorEvent = MatadorEvent;
3096
+ exports.MemoryCheckpointStore = MemoryCheckpointStore;
3097
+ exports.MessageMaybePoisonedError = MessageMaybePoisonedError;
3098
+ exports.MultiTransport = MultiTransport;
3099
+ exports.NoOpCheckpointStore = NoOpCheckpointStore;
3100
+ exports.NoSubscribersExistError = NoSubscribersExistError;
3101
+ exports.NotStartedError = NotStartedError;
3102
+ exports.ProcessingPipeline = ProcessingPipeline;
3103
+ exports.QueueNotFoundError = QueueNotFoundError;
3104
+ exports.RabbitMQCodec = RabbitMQCodec;
3105
+ exports.RabbitMQTransport = RabbitMQTransport;
3106
+ exports.ResumableContext = ResumableContext;
3107
+ exports.RetryControlError = RetryControlError;
3108
+ exports.SafeHooks = SafeHooks;
3109
+ exports.SchemaError = SchemaError;
3110
+ exports.SchemaRegistry = SchemaRegistry;
3111
+ exports.ShutdownInProgressError = ShutdownInProgressError;
3112
+ exports.ShutdownManager = ShutdownManager;
3113
+ exports.StandardRetryPolicy = StandardRetryPolicy;
3114
+ exports.SubscriberIsStubError = SubscriberIsStubError;
3115
+ exports.SubscriberNotRegisteredError = SubscriberNotRegisteredError;
3116
+ exports.TimeoutError = TimeoutError;
3117
+ exports.TopologyBuilder = TopologyBuilder;
3118
+ exports.TopologyValidationError = TopologyValidationError;
3119
+ exports.TransportClosedError = TransportClosedError;
3120
+ exports.TransportNotConnectedError = TransportNotConnectedError;
3121
+ exports.TransportSendError = TransportSendError;
3122
+ exports.bind = bind;
3123
+ exports.consoleLogger = consoleLogger;
3124
+ exports.createEnvelope = createEnvelope;
3125
+ exports.createSubscriber = createSubscriber;
3126
+ exports.createSubscriberStub = createSubscriberStub;
3127
+ exports.defaultConnectionConfig = defaultConnectionConfig;
3128
+ exports.defaultRetryConfig = defaultRetryConfig;
3129
+ exports.defaultShutdownConfig = defaultShutdownConfig;
3130
+ exports.getDeadLetterQueueName = getDeadLetterQueueName;
3131
+ exports.getQualifiedQueueName = getQualifiedQueueName;
3132
+ exports.getRetryQueueName = getRetryQueueName;
3133
+ exports.hasDescription = hasDescription;
3134
+ exports.hasNativeDeadLetter = hasNativeDeadLetter;
3135
+ exports.installPlugins = installPlugins;
3136
+ exports.invalidResult = invalidResult;
3137
+ exports.isAssertionError = isAssertionError;
3138
+ exports.isCheckpointStoreError = isCheckpointStoreError;
3139
+ exports.isDoRetry = isDoRetry;
3140
+ exports.isDontRetry = isDontRetry;
3141
+ exports.isDuplicateIoKeyError = isDuplicateIoKeyError;
3142
+ exports.isEventNotRegisteredError = isEventNotRegisteredError;
3143
+ exports.isIdempotentMessageCannotRetryError = isIdempotentMessageCannotRetryError;
3144
+ exports.isMatadorError = isMatadorError;
3145
+ exports.isMessageMaybePoisonedError = isMessageMaybePoisonedError;
3146
+ exports.isNotStartedError = isNotStartedError;
3147
+ exports.isSchemaEntryTuple = isSchemaEntryTuple;
3148
+ exports.isSubscriber = isSubscriber;
3149
+ exports.isSubscriberNotRegisteredError = isSubscriberNotRegisteredError;
3150
+ exports.isSubscriberStub = isSubscriberStub;
3151
+ exports.isTransportNotConnectedError = isTransportNotConnectedError;
3152
+ exports.supportsDelayedMessages = supportsDelayedMessages;
3153
+ exports.supportsDeliveryMode = supportsDeliveryMode;
3154
+ exports.validResult = validResult;
3155
+ //# sourceMappingURL=index.cjs.map
3156
+ //# sourceMappingURL=index.cjs.map