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