@zdavison/matador 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/cli.ts +453 -0
  2. package/dist/checkpoint/context.d.ts +59 -0
  3. package/dist/checkpoint/context.d.ts.map +1 -0
  4. package/dist/checkpoint/context.js +140 -0
  5. package/dist/checkpoint/context.test.d.ts +2 -0
  6. package/dist/checkpoint/context.test.d.ts.map +1 -0
  7. package/dist/checkpoint/context.test.js +424 -0
  8. package/dist/checkpoint/index.d.ts +7 -0
  9. package/dist/checkpoint/index.d.ts.map +1 -0
  10. package/dist/checkpoint/index.js +6 -0
  11. package/dist/checkpoint/stores/memory.d.ts +29 -0
  12. package/dist/checkpoint/stores/memory.d.ts.map +1 -0
  13. package/dist/checkpoint/stores/memory.js +39 -0
  14. package/dist/checkpoint/stores/noop.d.ts +14 -0
  15. package/dist/checkpoint/stores/noop.d.ts.map +1 -0
  16. package/dist/checkpoint/stores/noop.js +18 -0
  17. package/dist/checkpoint/stores/stores.test.d.ts +2 -0
  18. package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
  19. package/dist/checkpoint/stores/stores.test.js +146 -0
  20. package/dist/checkpoint/types.d.ts +119 -0
  21. package/dist/checkpoint/types.d.ts.map +1 -0
  22. package/dist/checkpoint/types.js +1 -0
  23. package/dist/codec/codec.d.ts +29 -0
  24. package/dist/codec/codec.d.ts.map +1 -0
  25. package/dist/codec/codec.js +15 -0
  26. package/dist/codec/header-aware-codec.d.ts +36 -0
  27. package/dist/codec/header-aware-codec.d.ts.map +1 -0
  28. package/dist/codec/header-aware-codec.js +1 -0
  29. package/dist/codec/index.d.ts +6 -0
  30. package/dist/codec/index.d.ts.map +1 -0
  31. package/dist/codec/index.js +3 -0
  32. package/dist/codec/json-codec.d.ts +13 -0
  33. package/dist/codec/json-codec.d.ts.map +1 -0
  34. package/dist/codec/json-codec.js +64 -0
  35. package/dist/codec/rabbitmq-codec.d.ts +28 -0
  36. package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
  37. package/dist/codec/rabbitmq-codec.js +242 -0
  38. package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
  39. package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
  40. package/dist/codec/rabbitmq-codec.test.js +433 -0
  41. package/dist/core/fanout.d.ts +59 -0
  42. package/dist/core/fanout.d.ts.map +1 -0
  43. package/dist/core/fanout.js +121 -0
  44. package/dist/core/fanout.test.d.ts +2 -0
  45. package/dist/core/fanout.test.d.ts.map +1 -0
  46. package/dist/core/fanout.test.js +1055 -0
  47. package/dist/core/index.d.ts +7 -0
  48. package/dist/core/index.d.ts.map +1 -0
  49. package/dist/core/index.js +3 -0
  50. package/dist/core/matador.d.ts +133 -0
  51. package/dist/core/matador.d.ts.map +1 -0
  52. package/dist/core/matador.js +222 -0
  53. package/dist/core/matador.test.d.ts +2 -0
  54. package/dist/core/matador.test.d.ts.map +1 -0
  55. package/dist/core/matador.test.js +446 -0
  56. package/dist/core/shutdown.d.ts +78 -0
  57. package/dist/core/shutdown.d.ts.map +1 -0
  58. package/dist/core/shutdown.js +111 -0
  59. package/dist/core/shutdown.test.d.ts +2 -0
  60. package/dist/core/shutdown.test.d.ts.map +1 -0
  61. package/dist/core/shutdown.test.js +524 -0
  62. package/dist/errors/checkpoint-errors.d.ts +30 -0
  63. package/dist/errors/checkpoint-errors.d.ts.map +1 -0
  64. package/dist/errors/checkpoint-errors.js +49 -0
  65. package/dist/errors/has-description.d.ts +18 -0
  66. package/dist/errors/has-description.d.ts.map +1 -0
  67. package/dist/errors/has-description.js +9 -0
  68. package/dist/errors/index.d.ts +6 -0
  69. package/dist/errors/index.d.ts.map +1 -0
  70. package/dist/errors/index.js +22 -0
  71. package/dist/errors/matador-errors.d.ts +183 -0
  72. package/dist/errors/matador-errors.d.ts.map +1 -0
  73. package/dist/errors/matador-errors.js +376 -0
  74. package/dist/errors/retry-errors.d.ts +67 -0
  75. package/dist/errors/retry-errors.d.ts.map +1 -0
  76. package/dist/errors/retry-errors.js +108 -0
  77. package/dist/hooks/index.d.ts +4 -0
  78. package/dist/hooks/index.d.ts.map +1 -0
  79. package/dist/hooks/index.js +2 -0
  80. package/dist/hooks/safe-hooks.d.ts +34 -0
  81. package/dist/hooks/safe-hooks.d.ts.map +1 -0
  82. package/dist/hooks/safe-hooks.js +135 -0
  83. package/dist/hooks/types.d.ts +189 -0
  84. package/dist/hooks/types.d.ts.map +1 -0
  85. package/dist/hooks/types.js +9 -0
  86. package/dist/index.cjs +3156 -0
  87. package/dist/index.cjs.map +1 -0
  88. package/dist/index.d.cts +23 -0
  89. package/dist/index.d.ts +23 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +31 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/pipeline/index.d.ts +3 -0
  94. package/dist/pipeline/index.d.ts.map +1 -0
  95. package/dist/pipeline/index.js +1 -0
  96. package/dist/pipeline/pipeline.d.ts +60 -0
  97. package/dist/pipeline/pipeline.d.ts.map +1 -0
  98. package/dist/pipeline/pipeline.js +261 -0
  99. package/dist/pipeline/pipeline.test.d.ts +2 -0
  100. package/dist/pipeline/pipeline.test.d.ts.map +1 -0
  101. package/dist/pipeline/pipeline.test.js +1065 -0
  102. package/dist/retry/index.d.ts +4 -0
  103. package/dist/retry/index.d.ts.map +1 -0
  104. package/dist/retry/index.js +1 -0
  105. package/dist/retry/policy.d.ts +43 -0
  106. package/dist/retry/policy.d.ts.map +1 -0
  107. package/dist/retry/policy.js +1 -0
  108. package/dist/retry/standard-policy.d.ts +44 -0
  109. package/dist/retry/standard-policy.d.ts.map +1 -0
  110. package/dist/retry/standard-policy.js +102 -0
  111. package/dist/retry/standard-policy.test.d.ts +2 -0
  112. package/dist/retry/standard-policy.test.d.ts.map +1 -0
  113. package/dist/retry/standard-policy.test.js +190 -0
  114. package/dist/schema/index.d.ts +4 -0
  115. package/dist/schema/index.d.ts.map +1 -0
  116. package/dist/schema/index.js +2 -0
  117. package/dist/schema/registry.d.ts +63 -0
  118. package/dist/schema/registry.d.ts.map +1 -0
  119. package/dist/schema/registry.js +171 -0
  120. package/dist/schema/registry.test.d.ts +2 -0
  121. package/dist/schema/registry.test.d.ts.map +1 -0
  122. package/dist/schema/registry.test.js +278 -0
  123. package/dist/schema/types.d.ts +158 -0
  124. package/dist/schema/types.d.ts.map +1 -0
  125. package/dist/schema/types.js +74 -0
  126. package/dist/schema/types.test.d.ts +2 -0
  127. package/dist/schema/types.test.d.ts.map +1 -0
  128. package/dist/schema/types.test.js +243 -0
  129. package/dist/topology/builder.d.ts +66 -0
  130. package/dist/topology/builder.d.ts.map +1 -0
  131. package/dist/topology/builder.js +156 -0
  132. package/dist/topology/builder.test.d.ts +2 -0
  133. package/dist/topology/builder.test.d.ts.map +1 -0
  134. package/dist/topology/builder.test.js +222 -0
  135. package/dist/topology/index.d.ts +5 -0
  136. package/dist/topology/index.d.ts.map +1 -0
  137. package/dist/topology/index.js +2 -0
  138. package/dist/topology/types.d.ts +76 -0
  139. package/dist/topology/types.d.ts.map +1 -0
  140. package/dist/topology/types.js +18 -0
  141. package/dist/transport/capabilities.d.ts +66 -0
  142. package/dist/transport/capabilities.d.ts.map +1 -0
  143. package/dist/transport/capabilities.js +18 -0
  144. package/dist/transport/connection-manager.d.ts +95 -0
  145. package/dist/transport/connection-manager.d.ts.map +1 -0
  146. package/dist/transport/connection-manager.js +144 -0
  147. package/dist/transport/index.d.ts +11 -0
  148. package/dist/transport/index.d.ts.map +1 -0
  149. package/dist/transport/index.js +5 -0
  150. package/dist/transport/local/local-transport.d.ts +62 -0
  151. package/dist/transport/local/local-transport.d.ts.map +1 -0
  152. package/dist/transport/local/local-transport.js +241 -0
  153. package/dist/transport/local/local-transport.test.d.ts +2 -0
  154. package/dist/transport/local/local-transport.test.d.ts.map +1 -0
  155. package/dist/transport/local/local-transport.test.js +192 -0
  156. package/dist/transport/multi/multi-transport.d.ts +94 -0
  157. package/dist/transport/multi/multi-transport.d.ts.map +1 -0
  158. package/dist/transport/multi/multi-transport.js +184 -0
  159. package/dist/transport/multi/multi-transport.test.d.ts +2 -0
  160. package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
  161. package/dist/transport/multi/multi-transport.test.js +236 -0
  162. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +73 -0
  163. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
  164. package/dist/transport/rabbitmq/rabbitmq-transport.js +518 -0
  165. package/dist/transport/transport.d.ts +152 -0
  166. package/dist/transport/transport.d.ts.map +1 -0
  167. package/dist/transport/transport.js +1 -0
  168. package/dist/types/common.d.ts +41 -0
  169. package/dist/types/common.d.ts.map +1 -0
  170. package/dist/types/common.js +12 -0
  171. package/dist/types/envelope.d.ts +82 -0
  172. package/dist/types/envelope.d.ts.map +1 -0
  173. package/dist/types/envelope.js +36 -0
  174. package/dist/types/event.d.ts +96 -0
  175. package/dist/types/event.d.ts.map +1 -0
  176. package/dist/types/event.js +26 -0
  177. package/dist/types/event.test.d.ts +2 -0
  178. package/dist/types/event.test.d.ts.map +1 -0
  179. package/dist/types/event.test.js +130 -0
  180. package/dist/types/index.d.ts +9 -0
  181. package/dist/types/index.d.ts.map +1 -0
  182. package/dist/types/index.js +4 -0
  183. package/dist/types/subscriber.d.ts +207 -0
  184. package/dist/types/subscriber.d.ts.map +1 -0
  185. package/dist/types/subscriber.js +99 -0
  186. package/examples/config.ts +126 -0
  187. package/examples/event.ts +26 -0
  188. package/examples/order-event.json +19 -0
  189. package/package.json +66 -0
  190. package/src/checkpoint/context.test.ts +510 -0
  191. package/src/checkpoint/context.ts +213 -0
  192. package/src/checkpoint/index.ts +30 -0
  193. package/src/checkpoint/stores/memory.ts +47 -0
  194. package/src/checkpoint/stores/noop.ts +22 -0
  195. package/src/checkpoint/stores/stores.test.ts +177 -0
  196. package/src/checkpoint/types.ts +147 -0
  197. package/src/codec/codec.ts +42 -0
  198. package/src/codec/header-aware-codec.ts +41 -0
  199. package/src/codec/index.ts +11 -0
  200. package/src/codec/json-codec.ts +69 -0
  201. package/src/codec/rabbitmq-codec.test.ts +516 -0
  202. package/src/codec/rabbitmq-codec.ts +336 -0
  203. package/src/core/fanout.test.ts +1351 -0
  204. package/src/core/fanout.ts +184 -0
  205. package/src/core/index.ts +12 -0
  206. package/src/core/matador.test.ts +575 -0
  207. package/src/core/matador.ts +344 -0
  208. package/src/core/shutdown.test.ts +853 -0
  209. package/src/core/shutdown.ts +165 -0
  210. package/src/errors/checkpoint-errors.ts +62 -0
  211. package/src/errors/has-description.ts +25 -0
  212. package/src/errors/index.ts +57 -0
  213. package/src/errors/matador-errors.ts +477 -0
  214. package/src/errors/retry-errors.ts +134 -0
  215. package/src/hooks/index.ts +15 -0
  216. package/src/hooks/safe-hooks.ts +223 -0
  217. package/src/hooks/types.ts +248 -0
  218. package/src/index.ts +231 -0
  219. package/src/pipeline/index.ts +2 -0
  220. package/src/pipeline/pipeline.test.ts +1317 -0
  221. package/src/pipeline/pipeline.ts +371 -0
  222. package/src/retry/index.ts +4 -0
  223. package/src/retry/policy.ts +46 -0
  224. package/src/retry/standard-policy.test.ts +282 -0
  225. package/src/retry/standard-policy.ts +156 -0
  226. package/src/schema/index.ts +16 -0
  227. package/src/schema/registry.test.ts +339 -0
  228. package/src/schema/registry.ts +226 -0
  229. package/src/schema/types.test.ts +281 -0
  230. package/src/schema/types.ts +217 -0
  231. package/src/topology/builder.test.ts +275 -0
  232. package/src/topology/builder.ts +199 -0
  233. package/src/topology/index.ts +15 -0
  234. package/src/topology/types.ts +109 -0
  235. package/src/transport/capabilities.ts +88 -0
  236. package/src/transport/connection-manager.ts +218 -0
  237. package/src/transport/index.ts +42 -0
  238. package/src/transport/local/local-transport.test.ts +262 -0
  239. package/src/transport/local/local-transport.ts +327 -0
  240. package/src/transport/multi/multi-transport.test.ts +320 -0
  241. package/src/transport/multi/multi-transport.ts +294 -0
  242. package/src/transport/rabbitmq/rabbitmq-transport.ts +753 -0
  243. package/src/transport/transport.ts +200 -0
  244. package/src/types/common.ts +53 -0
  245. package/src/types/envelope.ts +152 -0
  246. package/src/types/event.test.ts +157 -0
  247. package/src/types/event.ts +118 -0
  248. package/src/types/index.ts +52 -0
  249. package/src/types/subscriber.ts +310 -0
  250. package/test/e2e/multi-transport.e2e.test.ts +236 -0
  251. package/test/e2e/rabbitmq-transport.e2e.test.ts +327 -0
  252. package/test/e2e/transport-compliance.e2e.test.ts +505 -0
  253. package/test/integration/matador.integration.test.ts +634 -0
  254. package/tsconfig.json +29 -0
  255. package/tsconfig.tsbuildinfo +1 -0
  256. package/tsup.config.ts +13 -0
@@ -0,0 +1,518 @@
1
+ import amqplib from 'amqplib';
2
+ import { RabbitMQCodec } from '../../codec/rabbitmq-codec.js';
3
+ import { DelayedMessagesNotSupportedError, TransportNotConnectedError, } from '../../errors/index.js';
4
+ import { consoleLogger } from '../../hooks/index.js';
5
+ import { ConnectionManager, } from '../connection-manager.js';
6
+ /**
7
+ * RabbitMQ transport implementation using amqplib.
8
+ */
9
+ export class RabbitMQTransport {
10
+ name = 'rabbitmq';
11
+ _capabilities = {
12
+ deliveryModes: ['at-least-once'],
13
+ delayedMessages: false,
14
+ deadLetterRouting: 'native',
15
+ attemptTracking: true,
16
+ concurrencyModel: 'prefetch',
17
+ ordering: 'none',
18
+ priorities: true,
19
+ };
20
+ get capabilities() {
21
+ return this._capabilities;
22
+ }
23
+ connection = null;
24
+ publishChannel = null;
25
+ connectionManager;
26
+ queueChannels = new Map();
27
+ topology = null;
28
+ codec = new RabbitMQCodec();
29
+ config;
30
+ logger;
31
+ delayedExchangeAvailable = false;
32
+ constructor(config) {
33
+ this.logger = config.logger ?? consoleLogger;
34
+ this.config = {
35
+ url: config.url,
36
+ connection: config.connection ?? {},
37
+ quorumQueues: config.quorumQueues ?? true,
38
+ defaultPrefetch: config.defaultPrefetch ?? 10,
39
+ enableDelayedMessages: config.enableDelayedMessages ?? true,
40
+ };
41
+ this.connectionManager = new ConnectionManager(() => this.doConnect(), () => this.doDisconnect(), this.config.connection);
42
+ }
43
+ async connect() {
44
+ await this.connectionManager.connect();
45
+ }
46
+ async disconnect() {
47
+ await this.connectionManager.disconnect();
48
+ }
49
+ isConnected() {
50
+ return this.connectionManager.isConnected();
51
+ }
52
+ async applyTopology(topology) {
53
+ this.topology = topology;
54
+ if (!this.publishChannel) {
55
+ throw new TransportNotConnectedError(this.name, 'applyTopology');
56
+ }
57
+ const channel = this.publishChannel;
58
+ // Create the main exchange for routing messages to queues
59
+ const mainExchange = this.getMainExchangeName(topology.namespace);
60
+ await channel.assertExchange(mainExchange, 'direct', { durable: true });
61
+ // Create dead-letter exchange if DLQ is enabled
62
+ const dlxExchange = this.getDLXExchangeName(topology.namespace);
63
+ if (topology.deadLetter.unhandled.enabled ||
64
+ topology.deadLetter.undeliverable.enabled) {
65
+ await channel.assertExchange(dlxExchange, 'direct', { durable: true });
66
+ }
67
+ // Check for delayed message exchange plugin
68
+ if (this.config.enableDelayedMessages) {
69
+ await this.setupDelayedExchange(topology.namespace);
70
+ }
71
+ // Create work queues
72
+ for (const queueDef of topology.queues) {
73
+ await this.assertWorkQueue(channel, topology, queueDef);
74
+ }
75
+ // Create DLQs
76
+ if (topology.deadLetter.unhandled.enabled) {
77
+ await this.assertDeadLetterQueues(channel, topology, 'unhandled');
78
+ }
79
+ if (topology.deadLetter.undeliverable.enabled) {
80
+ await this.assertDeadLetterQueues(channel, topology, 'undeliverable');
81
+ }
82
+ }
83
+ async send(queue, envelope, options) {
84
+ if (!this.publishChannel || !this.topology) {
85
+ throw new TransportNotConnectedError(this.name, 'send');
86
+ }
87
+ const encoded = this.codec.encode(envelope);
88
+ const buffer = Buffer.from(encoded.body);
89
+ const publishOptions = {
90
+ persistent: true,
91
+ contentType: encoded.contentType,
92
+ messageId: envelope.id,
93
+ timestamp: Date.now(),
94
+ headers: encoded.headers,
95
+ };
96
+ if (options?.priority !== undefined) {
97
+ publishOptions.priority = options.priority;
98
+ }
99
+ // Handle delayed messages
100
+ if (options?.delay !== undefined && options.delay > 0) {
101
+ if (!this.delayedExchangeAvailable) {
102
+ throw new DelayedMessagesNotSupportedError(this.name);
103
+ }
104
+ const delayedExchange = this.getDelayedExchangeName(this.topology.namespace);
105
+ publishOptions.headers = {
106
+ ...publishOptions.headers,
107
+ 'x-delay': options.delay,
108
+ };
109
+ this.publishChannel.publish(delayedExchange, queue, buffer, publishOptions);
110
+ return this.name;
111
+ }
112
+ // Transport-specific options
113
+ if (options?.transport?.rabbitmq?.expiration !== undefined) {
114
+ publishOptions.expiration = String(options.transport.rabbitmq.expiration);
115
+ }
116
+ if (options?.transport?.rabbitmq?.persistent !== undefined) {
117
+ publishOptions.persistent = options.transport.rabbitmq.persistent;
118
+ }
119
+ const routingKey = options?.transport?.rabbitmq?.routingKey ?? queue;
120
+ const exchange = this.getMainExchangeName(this.topology.namespace);
121
+ this.publishChannel.publish(exchange, routingKey, buffer, publishOptions);
122
+ return this.name;
123
+ }
124
+ async subscribe(queue, handler, options = {}) {
125
+ if (!this.connection || !this.topology) {
126
+ throw new TransportNotConnectedError(this.name, 'subscribe');
127
+ }
128
+ // Get or create a dedicated channel for this queue
129
+ const queueChannel = await this.getOrCreateQueueChannel(queue, options);
130
+ const { channel } = queueChannel;
131
+ const consumer = {
132
+ consumerTag: '',
133
+ queue,
134
+ active: true,
135
+ };
136
+ const { consumerTag } = await channel.consume(queue, async (msg) => {
137
+ if (!msg || !consumer.active)
138
+ return;
139
+ const attemptNumber = this.getAttemptNumber(msg);
140
+ const receipt = {
141
+ handle: { channel, msg },
142
+ redelivered: msg.fields.redelivered,
143
+ attemptNumber,
144
+ deliveryCount: this.getDeliveryCount(msg, attemptNumber),
145
+ sourceQueue: queue,
146
+ sourceTransport: this.name,
147
+ };
148
+ try {
149
+ const headers = (msg.properties.headers ?? {});
150
+ const envelope = this.codec.decode(new Uint8Array(msg.content), headers);
151
+ await handler(envelope, receipt);
152
+ }
153
+ catch (error) {
154
+ // Handler errors should be caught in the pipeline
155
+ this.logger.error('[Matador] 🔴 Handler error in message processing', error);
156
+ }
157
+ }, { noAck: false });
158
+ // Update the consumer tag
159
+ consumer.consumerTag = consumerTag;
160
+ // Track the consumer
161
+ queueChannel.consumers.push(consumer);
162
+ return {
163
+ unsubscribe: async () => {
164
+ consumer.active = false;
165
+ try {
166
+ await channel.cancel(consumerTag);
167
+ }
168
+ catch {
169
+ // Channel may already be closed
170
+ }
171
+ // Remove consumer from tracking
172
+ const idx = queueChannel.consumers.indexOf(consumer);
173
+ if (idx !== -1) {
174
+ queueChannel.consumers.splice(idx, 1);
175
+ }
176
+ // Close channel if no more consumers on this queue
177
+ if (queueChannel.consumers.length === 0) {
178
+ try {
179
+ await channel.close();
180
+ }
181
+ catch {
182
+ // Ignore
183
+ }
184
+ this.queueChannels.delete(queue);
185
+ }
186
+ },
187
+ get isActive() {
188
+ return consumer.active;
189
+ },
190
+ };
191
+ }
192
+ async complete(receipt) {
193
+ const { channel, msg } = receipt.handle;
194
+ try {
195
+ channel.ack(msg);
196
+ }
197
+ catch {
198
+ // Channel may be closed, ignore
199
+ }
200
+ }
201
+ async sendToDeadLetter(receipt, dlqName, envelope, reason) {
202
+ if (!this.publishChannel || !this.topology) {
203
+ throw new TransportNotConnectedError(this.name, 'sendToDeadLetter');
204
+ }
205
+ // Add error info to envelope
206
+ const dlqEnvelope = {
207
+ ...envelope,
208
+ docket: {
209
+ ...envelope.docket,
210
+ lastError: reason,
211
+ firstError: envelope.docket.firstError ?? reason,
212
+ originalQueue: receipt.sourceQueue,
213
+ },
214
+ };
215
+ const encoded = this.codec.encode(dlqEnvelope);
216
+ const buffer = Buffer.from(encoded.body);
217
+ const dlxExchange = this.getDLXExchangeName(this.topology.namespace);
218
+ const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
219
+ const publishOptions = {
220
+ persistent: true,
221
+ contentType: encoded.contentType,
222
+ messageId: envelope.id,
223
+ timestamp: Date.now(),
224
+ headers: {
225
+ ...encoded.headers,
226
+ 'x-matador-dead-letter-reason': reason,
227
+ },
228
+ };
229
+ this.publishChannel.publish(dlxExchange, dlqQueueName, buffer, publishOptions);
230
+ // Complete the original message
231
+ await this.complete(receipt);
232
+ }
233
+ // Private methods
234
+ /**
235
+ * Gets or creates a dedicated channel for a queue subscription.
236
+ *
237
+ * We create separate channels per subscribed queue to enable independent
238
+ * prefetch/concurrency control.
239
+ */
240
+ async getOrCreateQueueChannel(queue, options) {
241
+ const existing = this.queueChannels.get(queue);
242
+ if (existing) {
243
+ return existing;
244
+ }
245
+ if (!this.connection) {
246
+ throw new TransportNotConnectedError(this.name, 'getOrCreateQueueChannel');
247
+ }
248
+ // Create a dedicated channel for this queue to control prefetch independently
249
+ const channel = await this.connection.createChannel();
250
+ const prefetch = options.transport?.rabbitmq?.prefetch ??
251
+ options.concurrency ??
252
+ this.config.defaultPrefetch ??
253
+ 10;
254
+ await channel.prefetch(prefetch);
255
+ const queueChannel = {
256
+ channel,
257
+ consumers: [],
258
+ };
259
+ this.queueChannels.set(queue, queueChannel);
260
+ return queueChannel;
261
+ }
262
+ async doConnect() {
263
+ const connection = await amqplib.connect(this.config.url);
264
+ this.connection = connection;
265
+ // Handle connection errors - let ConnectionManager handle reconnection
266
+ connection.on('error', (err) => {
267
+ this.logger.error('[Matador] 🔴 RabbitMQ connection error', err);
268
+ });
269
+ connection.on('close', () => {
270
+ if (this.connectionManager.isConnected()) {
271
+ // Unexpected close, trigger reconnection
272
+ this.connectionManager.handleConnectionLost(new Error('Connection closed unexpectedly'));
273
+ }
274
+ });
275
+ // Create the publish channel
276
+ this.publishChannel = await connection.createChannel();
277
+ // Handle publish channel errors to prevent unhandled error events
278
+ this.publishChannel.on('error', (err) => {
279
+ this.logger.error('[Matador] 🔴 RabbitMQ publish channel error', err);
280
+ });
281
+ // Re-apply topology if we have one (reconnection scenario)
282
+ if (this.topology) {
283
+ await this.applyTopology(this.topology);
284
+ }
285
+ }
286
+ async doDisconnect() {
287
+ // Cancel all consumers and close queue channels
288
+ for (const queueChannel of this.queueChannels.values()) {
289
+ for (const consumer of queueChannel.consumers) {
290
+ consumer.active = false;
291
+ try {
292
+ await queueChannel.channel.cancel(consumer.consumerTag);
293
+ }
294
+ catch {
295
+ // Ignore errors during cleanup
296
+ }
297
+ }
298
+ try {
299
+ await queueChannel.channel.close();
300
+ }
301
+ catch {
302
+ // Ignore
303
+ }
304
+ }
305
+ this.queueChannels.clear();
306
+ // Close publish channel
307
+ if (this.publishChannel) {
308
+ try {
309
+ await this.publishChannel.close();
310
+ }
311
+ catch {
312
+ // Ignore
313
+ }
314
+ this.publishChannel = null;
315
+ }
316
+ // Close connection
317
+ if (this.connection) {
318
+ try {
319
+ await this.connection.close();
320
+ }
321
+ catch {
322
+ // Ignore
323
+ }
324
+ this.connection = null;
325
+ }
326
+ // Reset capabilities
327
+ this.delayedExchangeAvailable = false;
328
+ this._capabilities = {
329
+ ...this._capabilities,
330
+ delayedMessages: false,
331
+ };
332
+ }
333
+ async setupDelayedExchange(namespace) {
334
+ if (!this.connection) {
335
+ return;
336
+ }
337
+ // Default to disabled
338
+ this.delayedExchangeAvailable = false;
339
+ const delayedExchange = this.getDelayedExchangeName(namespace);
340
+ const connection = this.connection;
341
+ // Use a promise-based approach to ensure all error paths resolve cleanly
342
+ // This prevents any error from propagating and affecting other channels
343
+ return new Promise((resolve) => {
344
+ let resolved = false;
345
+ const safeResolve = () => {
346
+ if (!resolved) {
347
+ resolved = true;
348
+ resolve();
349
+ }
350
+ };
351
+ connection
352
+ .createChannel()
353
+ .then((probeChannel) => {
354
+ // Handle channel errors - this fires when RabbitMQ closes the channel
355
+ probeChannel.on('error', () => {
356
+ safeResolve();
357
+ });
358
+ // Handle channel close
359
+ probeChannel.on('close', () => {
360
+ safeResolve();
361
+ });
362
+ // Try to declare a delayed exchange
363
+ // This will fail if the plugin is not installed
364
+ probeChannel
365
+ .assertExchange(delayedExchange, 'x-delayed-message', {
366
+ durable: true,
367
+ arguments: { 'x-delayed-type': 'direct' },
368
+ })
369
+ .then(() => {
370
+ this.delayedExchangeAvailable = true;
371
+ this._capabilities = {
372
+ ...this._capabilities,
373
+ delayedMessages: true,
374
+ };
375
+ this.logger.debug('[Matador] 🔌 Delayed message exchange plugin detected');
376
+ // Close the probe channel gracefully
377
+ probeChannel.close().catch(() => { });
378
+ safeResolve();
379
+ })
380
+ .catch(() => {
381
+ // assertExchange failed - plugin not available
382
+ // Channel is already closed by RabbitMQ, no need to close
383
+ this.logger.warn('[Matador] 🟡 RabbitMQ delayed message exchange plugin not available. ' +
384
+ 'Delayed messages will not be supported.');
385
+ safeResolve();
386
+ });
387
+ })
388
+ .catch(() => {
389
+ // Failed to create channel - shouldn't happen but handle it
390
+ safeResolve();
391
+ });
392
+ });
393
+ }
394
+ async assertWorkQueue(channel, topology, queueDef) {
395
+ const queueName = queueDef.exact
396
+ ? queueDef.name
397
+ : `${topology.namespace}.${queueDef.name}`;
398
+ const queueOptions = {
399
+ durable: true,
400
+ arguments: {},
401
+ };
402
+ // Use quorum queues for durability
403
+ if (this.config.quorumQueues && !queueDef.exact) {
404
+ queueOptions.arguments['x-queue-type'] = 'quorum';
405
+ }
406
+ // Set up dead-letter exchange routing
407
+ const dlxExchange = this.getDLXExchangeName(topology.namespace);
408
+ if (topology.deadLetter.unhandled.enabled ||
409
+ topology.deadLetter.undeliverable.enabled) {
410
+ queueOptions.arguments['x-dead-letter-exchange'] = dlxExchange;
411
+ }
412
+ // Enable priority if requested
413
+ if (queueDef.priorities) {
414
+ queueOptions.arguments['x-max-priority'] = 10;
415
+ }
416
+ // Set consumer timeout if specified
417
+ if (queueDef.consumerTimeout) {
418
+ queueOptions.arguments['x-consumer-timeout'] = queueDef.consumerTimeout;
419
+ }
420
+ await channel.assertQueue(queueName, queueOptions);
421
+ // Bind queue to main exchange
422
+ const mainExchange = this.getMainExchangeName(topology.namespace);
423
+ await channel.bindQueue(queueName, mainExchange, queueName);
424
+ // Bind to delayed exchange if available
425
+ if (this.delayedExchangeAvailable) {
426
+ const delayedExchange = this.getDelayedExchangeName(topology.namespace);
427
+ await channel.bindQueue(queueName, delayedExchange, queueName);
428
+ }
429
+ // Create retry queue if retry is enabled
430
+ if (topology.retry.enabled) {
431
+ await this.assertRetryQueue(channel, topology, queueName);
432
+ }
433
+ }
434
+ async assertRetryQueue(channel, topology, workQueueName) {
435
+ const retryQueueName = `${workQueueName}.retry`;
436
+ const mainExchange = this.getMainExchangeName(topology.namespace);
437
+ const retryQueueOptions = {
438
+ durable: true,
439
+ arguments: {
440
+ 'x-dead-letter-exchange': mainExchange,
441
+ 'x-dead-letter-routing-key': workQueueName,
442
+ 'x-message-ttl': topology.retry.defaultDelayMs,
443
+ },
444
+ };
445
+ if (this.config.quorumQueues) {
446
+ retryQueueOptions.arguments['x-queue-type'] = 'quorum';
447
+ }
448
+ await channel.assertQueue(retryQueueName, retryQueueOptions);
449
+ await channel.bindQueue(retryQueueName, mainExchange, retryQueueName);
450
+ }
451
+ async assertDeadLetterQueues(channel, topology, dlqType) {
452
+ const dlxExchange = this.getDLXExchangeName(topology.namespace);
453
+ const dlConfig = topology.deadLetter[dlqType];
454
+ for (const queueDef of topology.queues) {
455
+ if (queueDef.exact)
456
+ continue;
457
+ const workQueueName = `${topology.namespace}.${queueDef.name}`;
458
+ const dlqName = `${workQueueName}.${dlqType}`;
459
+ const dlqOptions = {
460
+ durable: true,
461
+ arguments: {},
462
+ };
463
+ if (dlConfig.maxLength) {
464
+ dlqOptions.arguments['x-max-length'] = dlConfig.maxLength;
465
+ }
466
+ // DLQs use classic queues (not quorum) for simplicity
467
+ await channel.assertQueue(dlqName, dlqOptions);
468
+ await channel.bindQueue(dlqName, dlxExchange, dlqName);
469
+ }
470
+ }
471
+ getMainExchangeName(namespace) {
472
+ return `${namespace}.exchange`;
473
+ }
474
+ getDLXExchangeName(namespace) {
475
+ return `${namespace}.dlx`;
476
+ }
477
+ getDelayedExchangeName(namespace) {
478
+ return `${namespace}.delayed`;
479
+ }
480
+ getAttemptNumber(msg) {
481
+ const headerValue = msg.properties.headers?.['x-matador-attempts'];
482
+ if (typeof headerValue === 'number') {
483
+ return headerValue;
484
+ }
485
+ // Check for x-death header (native DLX redelivery count)
486
+ const xDeath = msg.properties.headers?.['x-death'];
487
+ if (Array.isArray(xDeath) && xDeath.length > 0) {
488
+ const deathCount = xDeath.reduce((sum, death) => sum + (death.count ?? 0), 0);
489
+ return deathCount + 1;
490
+ }
491
+ return 1;
492
+ }
493
+ /**
494
+ * Gets the native delivery count for poison message detection.
495
+ * This tracks how many times the message was delivered without acknowledgment,
496
+ * which helps detect crash loops.
497
+ */
498
+ getDeliveryCount(msg, attemptNumber) {
499
+ // Check for explicit delivery count header (some RabbitMQ setups track this)
500
+ const deliveryCount = msg.properties.headers?.['x-delivery-count'];
501
+ if (typeof deliveryCount === 'number') {
502
+ return deliveryCount;
503
+ }
504
+ // Check x-death header for dead-letter redelivery count
505
+ const xDeath = msg.properties.headers?.['x-death'];
506
+ if (Array.isArray(xDeath) && xDeath.length > 0) {
507
+ const deathCount = xDeath.reduce((sum, death) => sum + (death.count ?? 0), 0);
508
+ // Add 1 because we're currently being delivered again
509
+ return deathCount + 1;
510
+ }
511
+ // If redelivered flag is set but no other tracking, count as 2 (first + this delivery)
512
+ if (msg.fields.redelivered) {
513
+ return Math.max(2, attemptNumber);
514
+ }
515
+ // Default to attempt number
516
+ return attemptNumber;
517
+ }
518
+ }
@@ -0,0 +1,152 @@
1
+ import type { Topology } from '../topology/types.js';
2
+ import type { Envelope } from '../types/index.js';
3
+ import type { TransportCapabilities } from './capabilities.js';
4
+ /**
5
+ * Transport-specific send options.
6
+ * Each transport can define its own options under its transport name key.
7
+ */
8
+ export interface TransportSendOptions {
9
+ /** RabbitMQ-specific send options */
10
+ readonly rabbitmq?: RabbitMQSendOptions | undefined;
11
+ }
12
+ /**
13
+ * RabbitMQ-specific options for sending messages.
14
+ */
15
+ export interface RabbitMQSendOptions {
16
+ /** Message expiration in milliseconds */
17
+ readonly expiration?: number | undefined;
18
+ /** Message persistence mode */
19
+ readonly persistent?: boolean | undefined;
20
+ /** Routing key override */
21
+ readonly routingKey?: string | undefined;
22
+ }
23
+ /**
24
+ * Options for sending a message.
25
+ */
26
+ export interface SendOptions {
27
+ /** Delay delivery by this many milliseconds */
28
+ readonly delay?: number | undefined;
29
+ /** Priority level (0-255, higher = more important) */
30
+ readonly priority?: number | undefined;
31
+ /** Transport-specific options */
32
+ readonly transport?: TransportSendOptions | undefined;
33
+ }
34
+ /**
35
+ * Transport-specific subscribe options.
36
+ * Each transport can define its own options under its transport name key.
37
+ */
38
+ export interface TransportSubscribeOptions {
39
+ /** RabbitMQ-specific subscribe options */
40
+ readonly rabbitmq?: RabbitMQSubscribeOptions | undefined;
41
+ }
42
+ /**
43
+ * RabbitMQ-specific options for subscribing.
44
+ */
45
+ export interface RabbitMQSubscribeOptions {
46
+ /** Consumer tag */
47
+ readonly consumerTag?: string | undefined;
48
+ /** Prefetch count (overrides concurrency option) */
49
+ readonly prefetch?: number | undefined;
50
+ /** Exclusive consumer */
51
+ readonly exclusive?: boolean | undefined;
52
+ }
53
+ /**
54
+ * Options for subscribing to a queue.
55
+ */
56
+ export interface SubscribeOptions {
57
+ /** Concurrency hint (number of concurrent handlers) */
58
+ readonly concurrency?: number | undefined;
59
+ /** Override default delivery semantics */
60
+ readonly deliveryMode?: 'at-least-once' | 'at-most-once' | undefined;
61
+ /** Transport-specific options */
62
+ readonly transport?: TransportSubscribeOptions | undefined;
63
+ }
64
+ /**
65
+ * Receipt for a received message, used for acknowledgment.
66
+ */
67
+ export interface MessageReceipt {
68
+ /** Opaque handle for the transport to identify the message */
69
+ readonly handle: unknown;
70
+ /** True if this is a redelivery (transport-reported if capable) */
71
+ readonly redelivered: boolean;
72
+ /** 1-based attempt number (transport-reported if capable, else from envelope) */
73
+ readonly attemptNumber: number;
74
+ /**
75
+ * Native delivery count from the transport.
76
+ * Tracks how many times this specific message was delivered without acknowledgment.
77
+ * Used for poison message detection to prevent crash loops.
78
+ * For transports that don't track this, defaults to attemptNumber.
79
+ */
80
+ readonly deliveryCount: number;
81
+ /** Original queue/topic the message came from */
82
+ readonly sourceQueue: string;
83
+ /**
84
+ * The name of the transport that received this message (e.g., 'local', 'rabbitmq').
85
+ * For MultiTransport, this is the actual underlying transport, not the wrapper name.
86
+ */
87
+ readonly sourceTransport: string;
88
+ }
89
+ /**
90
+ * Handler function for processing received messages.
91
+ */
92
+ export type MessageHandler = (envelope: Envelope, receipt: MessageReceipt) => Promise<void>;
93
+ /**
94
+ * Subscription handle for managing active subscriptions.
95
+ */
96
+ export interface Subscription {
97
+ /** Cancels the subscription */
98
+ unsubscribe(): Promise<void>;
99
+ /** Whether the subscription is currently active */
100
+ readonly isActive: boolean;
101
+ }
102
+ /**
103
+ * Transport interface - the minimal abstraction for message delivery.
104
+ * Transports handle only I/O; all business logic lives in Matador core.
105
+ */
106
+ export interface Transport {
107
+ /** Transport identifier */
108
+ readonly name: string;
109
+ /** Capabilities supported by this transport */
110
+ readonly capabilities: TransportCapabilities;
111
+ /**
112
+ * Establishes connection to the message broker.
113
+ * Should handle initial connection with retries.
114
+ */
115
+ connect(): Promise<void>;
116
+ /**
117
+ * Gracefully disconnects from the message broker.
118
+ * Should close all consumers before connection.
119
+ */
120
+ disconnect(): Promise<void>;
121
+ /**
122
+ * Returns whether the transport is currently connected.
123
+ */
124
+ isConnected(): boolean;
125
+ /**
126
+ * Translates and applies the generic topology to the transport.
127
+ * Creates necessary queues, exchanges, topics, etc.
128
+ */
129
+ applyTopology(topology: Topology): Promise<void>;
130
+ /**
131
+ * Sends a message to the specified queue.
132
+ * @returns The name of the transport that was used (useful for MultiTransport)
133
+ */
134
+ send(queue: string, envelope: Envelope, options?: SendOptions): Promise<Transport['name']>;
135
+ /**
136
+ * Subscribes to messages on the specified queue.
137
+ * The handler receives decoded envelopes and receipts.
138
+ */
139
+ subscribe(queue: string, handler: MessageHandler, options?: SubscribeOptions): Promise<Subscription>;
140
+ /**
141
+ * Acknowledges/completes a message.
142
+ * Called after processing is done (success, retry scheduled, or dead-lettered).
143
+ */
144
+ complete(receipt: MessageReceipt): Promise<void>;
145
+ /**
146
+ * Sends a message to the dead-letter queue.
147
+ * For transports with native DL routing, may use native mechanism.
148
+ * For others, sends to DLQ then completes original.
149
+ */
150
+ sendToDeadLetter?(receipt: MessageReceipt, dlqName: string, envelope: Envelope, reason: string): Promise<void>;
151
+ }
152
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/transport/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};