@zdavison/matador 2.0.7 → 2.0.9

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