@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
@@ -1,541 +0,0 @@
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
- * Redacts credentials from an AMQP URL.
8
- * Replaces username and password with '****' regardless of their length.
9
- *
10
- * @example
11
- * redactAmqpUrl('amqp://user:pass@host:5672') // 'amqp://****:****@host:5672'
12
- */
13
- export function redactAmqpUrl(url) {
14
- const regex = /^(amqps?:\/\/)[^:]+:[^@]+@/;
15
- return url.replace(regex, '$1****:****@');
16
- }
17
- /**
18
- * RabbitMQ transport implementation using amqplib.
19
- */
20
- export class RabbitMQTransport {
21
- name = 'rabbitmq';
22
- _capabilities = {
23
- deliveryModes: ['at-least-once'],
24
- delayedMessages: false,
25
- deadLetterRouting: 'native',
26
- attemptTracking: true,
27
- concurrencyModel: 'prefetch',
28
- ordering: 'none',
29
- priorities: true,
30
- };
31
- get capabilities() {
32
- return this._capabilities;
33
- }
34
- connection = null;
35
- publishChannel = null;
36
- connectionManager;
37
- queueChannels = new Map();
38
- topology = null;
39
- codec = new RabbitMQCodec();
40
- config;
41
- logger;
42
- delayedExchangeAvailable = false;
43
- constructor(config) {
44
- this.logger = config.logger ?? consoleLogger;
45
- this.config = {
46
- url: config.url,
47
- connectionName: config.connectionName,
48
- connection: config.connection ?? {},
49
- quorumQueues: config.quorumQueues ?? true,
50
- defaultPrefetch: config.defaultPrefetch ?? 10,
51
- enableDelayedMessages: config.enableDelayedMessages ?? true,
52
- };
53
- this.connectionManager = new ConnectionManager(() => this.doConnect(), () => this.doDisconnect(), this.config.connection);
54
- }
55
- async connect() {
56
- await this.connectionManager.connect();
57
- }
58
- async disconnect() {
59
- await this.connectionManager.disconnect();
60
- }
61
- isConnected() {
62
- return this.connectionManager.isConnected();
63
- }
64
- async applyTopology(topology) {
65
- this.topology = topology;
66
- if (!this.publishChannel) {
67
- throw new TransportNotConnectedError(this.name, 'applyTopology');
68
- }
69
- const channel = this.publishChannel;
70
- // Create the main exchange for routing messages to queues
71
- const mainExchange = this.getMainExchangeName(topology.namespace);
72
- await channel.assertExchange(mainExchange, 'direct', { durable: true });
73
- // Create dead-letter exchange if DLQ is enabled
74
- const dlxExchange = this.getDLXExchangeName(topology.namespace);
75
- if (topology.deadLetter.unhandled.enabled ||
76
- topology.deadLetter.undeliverable.enabled) {
77
- await channel.assertExchange(dlxExchange, 'direct', { durable: true });
78
- }
79
- // Check for delayed message exchange plugin
80
- if (this.config.enableDelayedMessages) {
81
- await this.setupDelayedExchange(topology.namespace);
82
- }
83
- // Create work queues
84
- for (const queueDef of topology.queues) {
85
- await this.assertWorkQueue(channel, topology, queueDef);
86
- }
87
- // Create DLQs
88
- if (topology.deadLetter.unhandled.enabled) {
89
- await this.assertDeadLetterQueues(channel, topology, 'unhandled');
90
- }
91
- if (topology.deadLetter.undeliverable.enabled) {
92
- await this.assertDeadLetterQueues(channel, topology, 'undeliverable');
93
- }
94
- }
95
- async send(queue, envelope, options) {
96
- if (!this.publishChannel || !this.topology) {
97
- throw new TransportNotConnectedError(this.name, 'send');
98
- }
99
- const encoded = this.codec.encode(envelope);
100
- const buffer = Buffer.from(encoded.body);
101
- const publishOptions = {
102
- persistent: true,
103
- contentType: encoded.contentType,
104
- messageId: envelope.id,
105
- timestamp: Date.now(),
106
- headers: encoded.headers,
107
- };
108
- if (options?.priority !== undefined) {
109
- publishOptions.priority = options.priority;
110
- }
111
- // Handle delayed messages
112
- if (options?.delay !== undefined && options.delay > 0) {
113
- if (!this.delayedExchangeAvailable) {
114
- throw new DelayedMessagesNotSupportedError(this.name);
115
- }
116
- const delayedExchange = this.getDelayedExchangeName(this.topology.namespace);
117
- publishOptions.headers = {
118
- ...publishOptions.headers,
119
- 'x-delay': options.delay,
120
- };
121
- this.publishChannel.publish(delayedExchange, queue, buffer, publishOptions);
122
- return this.name;
123
- }
124
- // Transport-specific options
125
- if (options?.transport?.rabbitmq?.expiration !== undefined) {
126
- publishOptions.expiration = String(options.transport.rabbitmq.expiration);
127
- }
128
- if (options?.transport?.rabbitmq?.persistent !== undefined) {
129
- publishOptions.persistent = options.transport.rabbitmq.persistent;
130
- }
131
- const routingKey = options?.transport?.rabbitmq?.routingKey ?? queue;
132
- const exchange = this.getMainExchangeName(this.topology.namespace);
133
- this.publishChannel.publish(exchange, routingKey, buffer, publishOptions);
134
- return this.name;
135
- }
136
- async subscribe(queue, handler, options = {}) {
137
- if (!this.connection || !this.topology) {
138
- throw new TransportNotConnectedError(this.name, 'subscribe');
139
- }
140
- // Get or create a dedicated channel for this queue
141
- const queueChannel = await this.getOrCreateQueueChannel(queue, options);
142
- const { channel } = queueChannel;
143
- const consumer = {
144
- consumerTag: '',
145
- queue,
146
- active: true,
147
- };
148
- const { consumerTag } = await channel.consume(queue, async (msg) => {
149
- if (!msg || !consumer.active)
150
- return;
151
- const attemptNumber = this.getAttemptNumber(msg);
152
- const receipt = {
153
- handle: { channel, msg },
154
- redelivered: msg.fields.redelivered,
155
- attemptNumber,
156
- deliveryCount: this.getDeliveryCount(msg, attemptNumber),
157
- sourceQueue: queue,
158
- sourceTransport: this.name,
159
- };
160
- try {
161
- const headers = (msg.properties.headers ?? {});
162
- const envelope = this.codec.decode(new Uint8Array(msg.content), headers);
163
- await handler(envelope, receipt);
164
- }
165
- catch (error) {
166
- // Handler errors should be caught in the pipeline
167
- this.logger.error('[Matador] 🔴 Handler error in message processing', error);
168
- }
169
- }, { noAck: false });
170
- // Update the consumer tag
171
- consumer.consumerTag = consumerTag;
172
- // Track the consumer
173
- queueChannel.consumers.push(consumer);
174
- return {
175
- unsubscribe: async () => {
176
- consumer.active = false;
177
- try {
178
- await channel.cancel(consumerTag);
179
- }
180
- catch {
181
- // Channel may already be closed
182
- }
183
- // Remove consumer from tracking
184
- const idx = queueChannel.consumers.indexOf(consumer);
185
- if (idx !== -1) {
186
- queueChannel.consumers.splice(idx, 1);
187
- }
188
- // Close channel if no more consumers on this queue
189
- if (queueChannel.consumers.length === 0) {
190
- try {
191
- await channel.close();
192
- }
193
- catch {
194
- // Ignore
195
- }
196
- this.queueChannels.delete(queue);
197
- }
198
- },
199
- get isActive() {
200
- return consumer.active;
201
- },
202
- };
203
- }
204
- async complete(receipt) {
205
- const { channel, msg } = receipt.handle;
206
- try {
207
- channel.ack(msg);
208
- }
209
- catch {
210
- // Channel may be closed, ignore
211
- }
212
- }
213
- async sendToDeadLetter(receipt, dlqName, envelope, reason) {
214
- if (!this.publishChannel || !this.topology) {
215
- throw new TransportNotConnectedError(this.name, 'sendToDeadLetter');
216
- }
217
- // Add error info to envelope
218
- const dlqEnvelope = {
219
- ...envelope,
220
- docket: {
221
- ...envelope.docket,
222
- lastError: reason,
223
- firstError: envelope.docket.firstError ?? reason,
224
- originalQueue: receipt.sourceQueue,
225
- },
226
- };
227
- const encoded = this.codec.encode(dlqEnvelope);
228
- const buffer = Buffer.from(encoded.body);
229
- const dlxExchange = this.getDLXExchangeName(this.topology.namespace);
230
- const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
231
- const publishOptions = {
232
- persistent: true,
233
- contentType: encoded.contentType,
234
- messageId: envelope.id,
235
- timestamp: Date.now(),
236
- headers: {
237
- ...encoded.headers,
238
- 'x-matador-dead-letter-reason': reason,
239
- },
240
- };
241
- this.publishChannel.publish(dlxExchange, dlqQueueName, buffer, publishOptions);
242
- // Complete the original message
243
- await this.complete(receipt);
244
- }
245
- // Private methods
246
- /**
247
- * Gets or creates a dedicated channel for a queue subscription.
248
- *
249
- * We create separate channels per subscribed queue to enable independent
250
- * prefetch/concurrency control.
251
- */
252
- async getOrCreateQueueChannel(queue, options) {
253
- const existing = this.queueChannels.get(queue);
254
- if (existing) {
255
- return existing;
256
- }
257
- if (!this.connection) {
258
- throw new TransportNotConnectedError(this.name, 'getOrCreateQueueChannel');
259
- }
260
- // Create a dedicated channel for this queue to control prefetch independently
261
- const channel = await this.connection.createChannel();
262
- const prefetch = options.transport?.rabbitmq?.prefetch ??
263
- options.concurrency ??
264
- this.config.defaultPrefetch ??
265
- 10;
266
- await channel.prefetch(prefetch);
267
- const queueChannel = {
268
- channel,
269
- consumers: [],
270
- };
271
- this.queueChannels.set(queue, queueChannel);
272
- return queueChannel;
273
- }
274
- async doConnect() {
275
- this.logger.info(`[Matador] ⏳ Connecting to RabbitMQ at '${redactAmqpUrl(this.config.url)}'.`);
276
- const connection = await amqplib.connect(this.config.url, {
277
- clientProperties: { connection_name: this.config.connectionName },
278
- });
279
- this.connection = connection;
280
- // Handle connection errors - let ConnectionManager handle reconnection
281
- connection.on('error', (err) => {
282
- this.logger.error('[Matador] 🔴 RabbitMQ connection error', err);
283
- });
284
- connection.on('close', () => {
285
- if (this.connectionManager.isConnected()) {
286
- // Unexpected close, trigger reconnection
287
- this.connectionManager.handleConnectionLost(new Error('Connection closed unexpectedly'));
288
- }
289
- });
290
- // Create the publish channel
291
- this.publishChannel = await connection.createChannel();
292
- // Handle publish channel errors to prevent unhandled error events
293
- this.publishChannel.on('error', (err) => {
294
- this.logger.error('[Matador] 🔴 RabbitMQ publish channel error', err);
295
- });
296
- // Re-apply topology if we have one (reconnection scenario)
297
- if (this.topology) {
298
- await this.applyTopology(this.topology);
299
- }
300
- }
301
- async doDisconnect() {
302
- // Cancel all consumers and close queue channels
303
- for (const queueChannel of this.queueChannels.values()) {
304
- for (const consumer of queueChannel.consumers) {
305
- consumer.active = false;
306
- try {
307
- await queueChannel.channel.cancel(consumer.consumerTag);
308
- }
309
- catch {
310
- // Ignore errors during cleanup
311
- }
312
- }
313
- try {
314
- await queueChannel.channel.close();
315
- }
316
- catch {
317
- // Ignore
318
- }
319
- }
320
- this.queueChannels.clear();
321
- // Close publish channel
322
- if (this.publishChannel) {
323
- try {
324
- await this.publishChannel.close();
325
- }
326
- catch {
327
- // Ignore
328
- }
329
- this.publishChannel = null;
330
- }
331
- // Close connection
332
- if (this.connection) {
333
- try {
334
- await this.connection.close();
335
- }
336
- catch {
337
- // Ignore
338
- }
339
- this.connection = null;
340
- }
341
- // Reset capabilities
342
- this.delayedExchangeAvailable = false;
343
- this._capabilities = {
344
- ...this._capabilities,
345
- delayedMessages: false,
346
- };
347
- }
348
- async setupDelayedExchange(namespace) {
349
- if (!this.connection) {
350
- return;
351
- }
352
- // Default to disabled
353
- this.delayedExchangeAvailable = false;
354
- const delayedExchange = this.getDelayedExchangeName(namespace);
355
- const connection = this.connection;
356
- // Use a promise-based approach to ensure all error paths resolve cleanly
357
- // This prevents any error from propagating and affecting other channels
358
- return new Promise((resolve) => {
359
- let resolved = false;
360
- const safeResolve = () => {
361
- if (!resolved) {
362
- resolved = true;
363
- resolve();
364
- }
365
- };
366
- connection
367
- .createChannel()
368
- .then((probeChannel) => {
369
- // Handle channel errors - this fires when RabbitMQ closes the channel
370
- probeChannel.on('error', () => {
371
- safeResolve();
372
- });
373
- // Handle channel close
374
- probeChannel.on('close', () => {
375
- safeResolve();
376
- });
377
- // Try to declare a delayed exchange
378
- // This will fail if the plugin is not installed
379
- probeChannel
380
- .assertExchange(delayedExchange, 'x-delayed-message', {
381
- durable: true,
382
- arguments: { 'x-delayed-type': 'direct' },
383
- })
384
- .then(() => {
385
- this.delayedExchangeAvailable = true;
386
- this._capabilities = {
387
- ...this._capabilities,
388
- delayedMessages: true,
389
- };
390
- this.logger.debug('[Matador] 🔌 Delayed message exchange plugin detected');
391
- // Close the probe channel gracefully
392
- probeChannel.close().catch(() => { });
393
- safeResolve();
394
- })
395
- .catch(() => {
396
- // assertExchange failed - plugin not available
397
- // Channel is already closed by RabbitMQ, no need to close
398
- this.logger.warn('[Matador] 🟡 RabbitMQ delayed message exchange plugin not available. ' +
399
- 'Delayed messages will not be supported.');
400
- safeResolve();
401
- });
402
- })
403
- .catch(() => {
404
- // Failed to create channel - shouldn't happen but handle it
405
- safeResolve();
406
- });
407
- });
408
- }
409
- async assertWorkQueue(channel, topology, queueDef) {
410
- const queueName = queueDef.exact
411
- ? queueDef.name
412
- : `${topology.namespace}.${queueDef.name}`;
413
- const rabbitmqOptions = queueDef.transport?.rabbitmq?.options;
414
- // If user provided exact RabbitMQ options, use them directly (replaces all defaults)
415
- if (rabbitmqOptions) {
416
- await channel.assertQueue(queueName, rabbitmqOptions);
417
- }
418
- else {
419
- // Use computed defaults
420
- const queueOptions = {
421
- durable: true,
422
- arguments: {},
423
- };
424
- // Use quorum queues for durability
425
- if (this.config.quorumQueues && !queueDef.exact) {
426
- queueOptions.arguments['x-queue-type'] = 'quorum';
427
- }
428
- // Set up dead-letter exchange routing
429
- const dlxExchange = this.getDLXExchangeName(topology.namespace);
430
- if (topology.deadLetter.unhandled.enabled ||
431
- topology.deadLetter.undeliverable.enabled) {
432
- queueOptions.arguments['x-dead-letter-exchange'] = dlxExchange;
433
- }
434
- // Enable priority if requested
435
- if (queueDef.priorities) {
436
- queueOptions.arguments['x-max-priority'] = 10;
437
- }
438
- // Set consumer timeout if specified
439
- if (queueDef.consumerTimeout) {
440
- queueOptions.arguments['x-consumer-timeout'] = queueDef.consumerTimeout;
441
- }
442
- await channel.assertQueue(queueName, queueOptions);
443
- }
444
- // Bind queue to main exchange
445
- const mainExchange = this.getMainExchangeName(topology.namespace);
446
- await channel.bindQueue(queueName, mainExchange, queueName);
447
- // Bind to delayed exchange if available
448
- if (this.delayedExchangeAvailable) {
449
- const delayedExchange = this.getDelayedExchangeName(topology.namespace);
450
- await channel.bindQueue(queueName, delayedExchange, queueName);
451
- }
452
- // Create retry queue if retry is enabled
453
- if (topology.retry.enabled) {
454
- await this.assertRetryQueue(channel, topology, queueName);
455
- }
456
- }
457
- async assertRetryQueue(channel, topology, workQueueName) {
458
- const retryQueueName = `${workQueueName}.retry`;
459
- const mainExchange = this.getMainExchangeName(topology.namespace);
460
- const retryQueueOptions = {
461
- durable: true,
462
- arguments: {
463
- 'x-dead-letter-exchange': mainExchange,
464
- 'x-dead-letter-routing-key': workQueueName,
465
- 'x-message-ttl': topology.retry.defaultDelayMs,
466
- },
467
- };
468
- if (this.config.quorumQueues) {
469
- retryQueueOptions.arguments['x-queue-type'] = 'quorum';
470
- }
471
- await channel.assertQueue(retryQueueName, retryQueueOptions);
472
- await channel.bindQueue(retryQueueName, mainExchange, retryQueueName);
473
- }
474
- async assertDeadLetterQueues(channel, topology, dlqType) {
475
- const dlxExchange = this.getDLXExchangeName(topology.namespace);
476
- const dlConfig = topology.deadLetter[dlqType];
477
- for (const queueDef of topology.queues) {
478
- if (queueDef.exact)
479
- continue;
480
- const workQueueName = `${topology.namespace}.${queueDef.name}`;
481
- const dlqName = `${workQueueName}.${dlqType}`;
482
- const dlqOptions = {
483
- durable: true,
484
- arguments: {},
485
- };
486
- if (dlConfig.maxLength) {
487
- dlqOptions.arguments['x-max-length'] = dlConfig.maxLength;
488
- }
489
- // DLQs use classic queues (not quorum) for simplicity
490
- await channel.assertQueue(dlqName, dlqOptions);
491
- await channel.bindQueue(dlqName, dlxExchange, dlqName);
492
- }
493
- }
494
- getMainExchangeName(namespace) {
495
- return `${namespace}.exchange`;
496
- }
497
- getDLXExchangeName(namespace) {
498
- return `${namespace}.dlx`;
499
- }
500
- getDelayedExchangeName(namespace) {
501
- return `${namespace}.delayed`;
502
- }
503
- getAttemptNumber(msg) {
504
- const headerValue = msg.properties.headers?.['x-matador-attempts'];
505
- if (typeof headerValue === 'number') {
506
- return headerValue;
507
- }
508
- // Check for x-death header (native DLX redelivery count)
509
- const xDeath = msg.properties.headers?.['x-death'];
510
- if (Array.isArray(xDeath) && xDeath.length > 0) {
511
- const deathCount = xDeath.reduce((sum, death) => sum + (death.count ?? 0), 0);
512
- return deathCount + 1;
513
- }
514
- return 1;
515
- }
516
- /**
517
- * Gets the native delivery count for poison message detection.
518
- * This tracks how many times the message was delivered without acknowledgment,
519
- * which helps detect crash loops.
520
- */
521
- getDeliveryCount(msg, attemptNumber) {
522
- // Check for explicit delivery count header (some RabbitMQ setups track this)
523
- const deliveryCount = msg.properties.headers?.['x-delivery-count'];
524
- if (typeof deliveryCount === 'number') {
525
- return deliveryCount;
526
- }
527
- // Check x-death header for dead-letter redelivery count
528
- const xDeath = msg.properties.headers?.['x-death'];
529
- if (Array.isArray(xDeath) && xDeath.length > 0) {
530
- const deathCount = xDeath.reduce((sum, death) => sum + (death.count ?? 0), 0);
531
- // Add 1 because we're currently being delivered again
532
- return deathCount + 1;
533
- }
534
- // If redelivered flag is set but no other tracking, count as 2 (first + this delivery)
535
- if (msg.fields.redelivered) {
536
- return Math.max(2, attemptNumber);
537
- }
538
- // Default to attempt number
539
- return attemptNumber;
540
- }
541
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=rabbitmq-transport.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rabbitmq-transport.test.d.ts","sourceRoot":"","sources":["../../../src/transport/rabbitmq/rabbitmq-transport.test.ts"],"names":[],"mappings":""}
@@ -1,100 +0,0 @@
1
- import { describe, expect, it, mock } from 'bun:test';
2
- import { RabbitMQTransport, redactAmqpUrl } from './rabbitmq-transport.js';
3
- describe('redactAmqpUrl', () => {
4
- it('should redact username and password with 4 asterisks', () => {
5
- const url = 'amqp://myuser:mypassword@localhost:5672';
6
- const redacted = redactAmqpUrl(url);
7
- expect(redacted).toBe('amqp://****:****@localhost:5672');
8
- });
9
- it('should redact credentials in amqps URLs', () => {
10
- const url = 'amqps://admin:secret123@rabbitmq.example.com:5671';
11
- const redacted = redactAmqpUrl(url);
12
- expect(redacted).toBe('amqps://****:****@rabbitmq.example.com:5671');
13
- });
14
- it('should redact long credentials to exactly 4 asterisks', () => {
15
- const url = 'amqp://verylongusername:verylongpassword@rabbitmq-cluster.svc.local:5672';
16
- const redacted = redactAmqpUrl(url);
17
- expect(redacted).toBe('amqp://****:****@rabbitmq-cluster.svc.local:5672');
18
- });
19
- it('should redact short credentials to exactly 4 asterisks', () => {
20
- const url = 'amqp://a:b@host:5672';
21
- const redacted = redactAmqpUrl(url);
22
- expect(redacted).toBe('amqp://****:****@host:5672');
23
- });
24
- it('should preserve vhost in URL', () => {
25
- const url = 'amqp://user:pass@host:5672/myvhost';
26
- const redacted = redactAmqpUrl(url);
27
- expect(redacted).toBe('amqp://****:****@host:5672/myvhost');
28
- });
29
- it('should not modify URL without credentials', () => {
30
- const url = 'amqp://localhost:5672';
31
- const redacted = redactAmqpUrl(url);
32
- expect(redacted).toBe('amqp://localhost:5672');
33
- });
34
- it('should not modify URL with only username (no password)', () => {
35
- const url = 'amqp://guest@localhost:5672';
36
- const redacted = redactAmqpUrl(url);
37
- expect(redacted).toBe('amqp://guest@localhost:5672');
38
- });
39
- it('should handle URL-encoded credentials', () => {
40
- // URL-encoded @ in password: p%40ssword
41
- const url = 'amqp://user:p%40ssword@host:5672';
42
- const redacted = redactAmqpUrl(url);
43
- expect(redacted).toBe('amqp://****:****@host:5672');
44
- });
45
- });
46
- describe('RabbitMQTransport', () => {
47
- describe('connection logging', () => {
48
- it('should log redacted connection URL when connecting', async () => {
49
- const mockLogger = {
50
- debug: mock(() => { }),
51
- info: mock(() => { }),
52
- warn: mock(() => { }),
53
- error: mock(() => { }),
54
- };
55
- const transport = new RabbitMQTransport({
56
- url: 'amqp://testuser:testpass@localhost:5672',
57
- connectionName: 'test-connection',
58
- logger: mockLogger,
59
- connection: {
60
- maxReconnectAttempts: 1, // Only try once to avoid long retries
61
- initialReconnectDelay: 10,
62
- },
63
- });
64
- // Attempt to connect - it will fail since there's no RabbitMQ server
65
- // but the log should still be emitted before the connection attempt
66
- try {
67
- await transport.connect();
68
- }
69
- catch {
70
- // Expected to fail - no RabbitMQ server running
71
- }
72
- // Verify the log was called with the redacted URL
73
- expect(mockLogger.info).toHaveBeenCalledWith("[Matador] \u23F3 Connecting to RabbitMQ at 'amqp://****:****@localhost:5672'.");
74
- });
75
- it('should log connection URL as-is when no credentials provided', async () => {
76
- const mockLogger = {
77
- debug: mock(() => { }),
78
- info: mock(() => { }),
79
- warn: mock(() => { }),
80
- error: mock(() => { }),
81
- };
82
- const transport = new RabbitMQTransport({
83
- url: 'amqp://localhost:5672',
84
- connectionName: 'test-connection',
85
- logger: mockLogger,
86
- connection: {
87
- maxReconnectAttempts: 1,
88
- initialReconnectDelay: 10,
89
- },
90
- });
91
- try {
92
- await transport.connect();
93
- }
94
- catch {
95
- // Expected to fail
96
- }
97
- expect(mockLogger.info).toHaveBeenCalledWith("[Matador] \u23F3 Connecting to RabbitMQ at 'amqp://localhost:5672'.");
98
- });
99
- });
100
- });