@saga-bus/transport-rabbitmq 0.1.2

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.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @saga-bus/transport-rabbitmq
2
+
3
+ RabbitMQ transport for production saga-bus deployments.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @saga-bus/transport-rabbitmq amqplib
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { RabbitMqTransport } from "@saga-bus/transport-rabbitmq";
15
+ import { createBus } from "@saga-bus/core";
16
+
17
+ const transport = new RabbitMqTransport({
18
+ uri: "amqp://guest:guest@localhost:5672",
19
+ exchange: "saga-bus",
20
+ exchangeType: "topic",
21
+ durable: true,
22
+ });
23
+
24
+ const bus = createBus({
25
+ transport,
26
+ sagas: [...],
27
+ });
28
+
29
+ await bus.start();
30
+ ```
31
+
32
+ ## Features
33
+
34
+ - Automatic reconnection with exponential backoff
35
+ - Topic exchange routing
36
+ - Prefetch-based concurrency control
37
+ - Durable queues and messages by default
38
+ - Queue prefix for multi-tenant setups
39
+
40
+ ## Configuration
41
+
42
+ | Option | Type | Default | Description |
43
+ |--------|------|---------|-------------|
44
+ | `uri` | `string` | required | AMQP connection URL |
45
+ | `exchange` | `string` | required | Exchange name |
46
+ | `exchangeType` | `string` | `"topic"` | Exchange type |
47
+ | `durable` | `boolean` | `true` | Durable exchanges/queues |
48
+ | `queuePrefix` | `string` | `""` | Queue name prefix |
49
+ | `reconnect.maxAttempts` | `number` | `10` | Max reconnect attempts |
50
+ | `reconnect.initialDelayMs` | `number` | `1000` | Initial retry delay |
51
+ | `reconnect.maxDelayMs` | `number` | `30000` | Max retry delay |
52
+
53
+ ## License
54
+
55
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ConnectionManager: () => ConnectionManager,
34
+ RabbitMqTransport: () => RabbitMqTransport
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/RabbitMqTransport.ts
39
+ var import_node_crypto = require("crypto");
40
+
41
+ // src/ConnectionManager.ts
42
+ var amqp = __toESM(require("amqplib"), 1);
43
+ var ConnectionManager = class {
44
+ options;
45
+ connection = null;
46
+ channel = null;
47
+ reconnectAttempt = 0;
48
+ isConnecting = false;
49
+ isClosed = false;
50
+ onConnectedCallbacks = [];
51
+ onDisconnectedCallbacks = [];
52
+ constructor(options) {
53
+ this.options = options;
54
+ }
55
+ /**
56
+ * Connect to RabbitMQ and set up the channel.
57
+ */
58
+ async connect() {
59
+ if (this.connection && this.channel) {
60
+ return;
61
+ }
62
+ if (this.isConnecting) {
63
+ return new Promise((resolve) => {
64
+ this.onConnectedCallbacks.push(resolve);
65
+ });
66
+ }
67
+ this.isConnecting = true;
68
+ this.isClosed = false;
69
+ try {
70
+ this.connection = await amqp.connect(this.options.uri);
71
+ this.connection.on("error", (error) => {
72
+ console.error("[RabbitMQ] Connection error:", error.message);
73
+ this.handleDisconnect(error);
74
+ });
75
+ this.connection.on("close", () => {
76
+ if (!this.isClosed) {
77
+ console.warn("[RabbitMQ] Connection closed unexpectedly");
78
+ this.handleDisconnect();
79
+ }
80
+ });
81
+ this.channel = await this.connection.createChannel();
82
+ this.channel.on("error", (error) => {
83
+ console.error("[RabbitMQ] Channel error:", error.message);
84
+ });
85
+ this.channel.on("close", () => {
86
+ if (!this.isClosed) {
87
+ console.warn("[RabbitMQ] Channel closed");
88
+ }
89
+ });
90
+ const exchangeType = this.options.exchangeType ?? "topic";
91
+ const durable = this.options.durable ?? true;
92
+ await this.channel.assertExchange(this.options.exchange, exchangeType, {
93
+ durable
94
+ });
95
+ this.reconnectAttempt = 0;
96
+ this.isConnecting = false;
97
+ for (const callback of this.onConnectedCallbacks) {
98
+ callback();
99
+ }
100
+ this.onConnectedCallbacks.length = 0;
101
+ console.info("[RabbitMQ] Connected successfully");
102
+ } catch (error) {
103
+ this.isConnecting = false;
104
+ throw error;
105
+ }
106
+ }
107
+ /**
108
+ * Handle disconnection and attempt reconnection.
109
+ */
110
+ handleDisconnect(error) {
111
+ this.connection = null;
112
+ this.channel = null;
113
+ for (const callback of this.onDisconnectedCallbacks) {
114
+ callback(error);
115
+ }
116
+ if (!this.isClosed) {
117
+ void this.reconnect();
118
+ }
119
+ }
120
+ /**
121
+ * Attempt to reconnect with exponential backoff.
122
+ */
123
+ async reconnect() {
124
+ const maxAttempts = this.options.reconnect?.maxAttempts ?? 10;
125
+ const initialDelay = this.options.reconnect?.initialDelayMs ?? 1e3;
126
+ const maxDelay = this.options.reconnect?.maxDelayMs ?? 3e4;
127
+ if (this.reconnectAttempt >= maxAttempts) {
128
+ console.error("[RabbitMQ] Max reconnection attempts reached");
129
+ return;
130
+ }
131
+ this.reconnectAttempt++;
132
+ const delay = Math.min(
133
+ initialDelay * Math.pow(2, this.reconnectAttempt - 1),
134
+ maxDelay
135
+ );
136
+ console.info(
137
+ `[RabbitMQ] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt}/${maxAttempts})`
138
+ );
139
+ await new Promise((resolve) => setTimeout(resolve, delay));
140
+ if (this.isClosed) {
141
+ return;
142
+ }
143
+ try {
144
+ await this.connect();
145
+ } catch (error) {
146
+ console.error("[RabbitMQ] Reconnection failed:", error);
147
+ void this.reconnect();
148
+ }
149
+ }
150
+ /**
151
+ * Close the connection.
152
+ */
153
+ async close() {
154
+ this.isClosed = true;
155
+ if (this.channel) {
156
+ try {
157
+ await this.channel.close();
158
+ } catch {
159
+ }
160
+ this.channel = null;
161
+ }
162
+ if (this.connection) {
163
+ try {
164
+ await this.connection.close();
165
+ } catch {
166
+ }
167
+ this.connection = null;
168
+ }
169
+ }
170
+ /**
171
+ * Get the current channel.
172
+ * @throws if not connected
173
+ */
174
+ getChannel() {
175
+ if (!this.channel) {
176
+ throw new Error("Not connected to RabbitMQ");
177
+ }
178
+ return this.channel;
179
+ }
180
+ /**
181
+ * Check if connected.
182
+ */
183
+ isConnected() {
184
+ return this.connection !== null && this.channel !== null;
185
+ }
186
+ /**
187
+ * Register a callback for when connection is established.
188
+ */
189
+ onConnected(callback) {
190
+ this.onConnectedCallbacks.push(callback);
191
+ }
192
+ /**
193
+ * Register a callback for when connection is lost.
194
+ */
195
+ onDisconnected(callback) {
196
+ this.onDisconnectedCallbacks.push(callback);
197
+ }
198
+ };
199
+
200
+ // src/RabbitMqTransport.ts
201
+ var RabbitMqTransport = class {
202
+ options;
203
+ connectionManager;
204
+ subscriptions = [];
205
+ handlers = /* @__PURE__ */ new Map();
206
+ started = false;
207
+ constructor(options) {
208
+ this.options = options;
209
+ this.connectionManager = new ConnectionManager(options);
210
+ this.connectionManager.onConnected(() => {
211
+ if (this.started) {
212
+ void this.resubscribeAll();
213
+ }
214
+ });
215
+ }
216
+ async start() {
217
+ if (this.started) {
218
+ return;
219
+ }
220
+ await this.connectionManager.connect();
221
+ this.started = true;
222
+ }
223
+ async stop() {
224
+ if (!this.started) {
225
+ return;
226
+ }
227
+ const channel = this.connectionManager.getChannel();
228
+ for (const sub of this.subscriptions) {
229
+ if (sub.consumerTag) {
230
+ try {
231
+ await channel.cancel(sub.consumerTag);
232
+ } catch {
233
+ }
234
+ }
235
+ }
236
+ await this.connectionManager.close();
237
+ this.started = false;
238
+ this.subscriptions.length = 0;
239
+ this.handlers.clear();
240
+ }
241
+ async subscribe(options, handler) {
242
+ const channel = this.connectionManager.getChannel();
243
+ const { endpoint, concurrency = 1, group } = options;
244
+ const queuePrefix = this.options.queuePrefix ?? "";
245
+ const queueName = group ? `${queuePrefix}${endpoint}.${group}` : `${queuePrefix}${endpoint}`;
246
+ await channel.assertQueue(queueName, {
247
+ durable: this.options.durable ?? true
248
+ });
249
+ await channel.bindQueue(queueName, this.options.exchange, endpoint);
250
+ await channel.prefetch(concurrency);
251
+ this.handlers.set(
252
+ queueName,
253
+ handler
254
+ );
255
+ const { consumerTag } = await channel.consume(
256
+ queueName,
257
+ (msg) => {
258
+ if (!msg) return;
259
+ void this.handleMessage(queueName, msg);
260
+ },
261
+ { noAck: false }
262
+ );
263
+ this.subscriptions.push({
264
+ endpoint,
265
+ queueName,
266
+ concurrency,
267
+ consumerTag
268
+ });
269
+ }
270
+ async publish(message, options) {
271
+ const channel = this.connectionManager.getChannel();
272
+ const { endpoint, headers = {}, delayMs, key } = options;
273
+ const envelope = {
274
+ id: (0, import_node_crypto.randomUUID)(),
275
+ type: message.type,
276
+ payload: message,
277
+ headers: { ...headers },
278
+ timestamp: /* @__PURE__ */ new Date(),
279
+ partitionKey: key
280
+ };
281
+ const content = Buffer.from(JSON.stringify(envelope));
282
+ const persistent = this.options.messageOptions?.persistent ?? true;
283
+ const contentType = this.options.messageOptions?.contentType ?? "application/json";
284
+ const publishOptions = {
285
+ persistent,
286
+ contentType,
287
+ messageId: envelope.id,
288
+ timestamp: envelope.timestamp.getTime(),
289
+ headers: {
290
+ ...headers,
291
+ "x-message-type": message.type
292
+ }
293
+ };
294
+ if (delayMs && delayMs > 0) {
295
+ publishOptions.headers["x-delay"] = delayMs;
296
+ }
297
+ channel.publish(
298
+ this.options.exchange,
299
+ endpoint,
300
+ content,
301
+ publishOptions
302
+ );
303
+ }
304
+ /**
305
+ * Handle an incoming message.
306
+ */
307
+ async handleMessage(queueName, msg) {
308
+ const channel = this.connectionManager.getChannel();
309
+ const handler = this.handlers.get(queueName);
310
+ if (!handler) {
311
+ channel.nack(msg, false, false);
312
+ return;
313
+ }
314
+ try {
315
+ const parsed = JSON.parse(msg.content.toString());
316
+ const envelope = {
317
+ ...parsed,
318
+ timestamp: new Date(parsed.timestamp)
319
+ };
320
+ await handler(envelope);
321
+ channel.ack(msg);
322
+ } catch (error) {
323
+ console.error("[RabbitMQ] Message handler error:", error);
324
+ channel.nack(msg, false, false);
325
+ throw error;
326
+ }
327
+ }
328
+ /**
329
+ * Re-establish all subscriptions after reconnection.
330
+ */
331
+ async resubscribeAll() {
332
+ const channel = this.connectionManager.getChannel();
333
+ for (const sub of this.subscriptions) {
334
+ try {
335
+ await channel.assertQueue(sub.queueName, {
336
+ durable: this.options.durable ?? true
337
+ });
338
+ await channel.bindQueue(
339
+ sub.queueName,
340
+ this.options.exchange,
341
+ sub.endpoint
342
+ );
343
+ await channel.prefetch(sub.concurrency);
344
+ const handler = this.handlers.get(sub.queueName);
345
+ if (handler) {
346
+ const { consumerTag } = await channel.consume(
347
+ sub.queueName,
348
+ (msg) => {
349
+ if (!msg) return;
350
+ void this.handleMessage(sub.queueName, msg);
351
+ },
352
+ { noAck: false }
353
+ );
354
+ sub.consumerTag = consumerTag;
355
+ }
356
+ } catch (error) {
357
+ console.error(
358
+ `[RabbitMQ] Failed to resubscribe to ${sub.queueName}:`,
359
+ error
360
+ );
361
+ }
362
+ }
363
+ }
364
+ /**
365
+ * Check if the transport is connected.
366
+ */
367
+ isConnected() {
368
+ return this.connectionManager.isConnected();
369
+ }
370
+ /**
371
+ * Get transport statistics.
372
+ */
373
+ getStats() {
374
+ return {
375
+ subscriptionCount: this.subscriptions.length,
376
+ isConnected: this.isConnected()
377
+ };
378
+ }
379
+ };
380
+ // Annotate the CommonJS export names for ESM import in node:
381
+ 0 && (module.exports = {
382
+ ConnectionManager,
383
+ RabbitMqTransport
384
+ });
385
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/RabbitMqTransport.ts","../src/ConnectionManager.ts"],"sourcesContent":["export { RabbitMqTransport } from \"./RabbitMqTransport.js\";\nexport { ConnectionManager } from \"./ConnectionManager.js\";\nexport type { RabbitMqTransportOptions, Subscription } from \"./types.js\";\n","import { randomUUID } from \"node:crypto\";\nimport type { ConsumeMessage } from \"amqplib\";\nimport type {\n Transport,\n TransportSubscribeOptions,\n TransportPublishOptions,\n BaseMessage,\n MessageEnvelope,\n} from \"@saga-bus/core\";\nimport type { RabbitMqTransportOptions, Subscription } from \"./types.js\";\nimport { ConnectionManager } from \"./ConnectionManager.js\";\n\n/**\n * RabbitMQ transport implementation using amqplib.\n *\n * @example\n * ```typescript\n * const transport = new RabbitMqTransport({\n * uri: \"amqp://localhost:5672\",\n * exchange: \"saga-bus\",\n * });\n *\n * await transport.start();\n *\n * await transport.subscribe(\n * { endpoint: \"OrderSubmitted\", concurrency: 5 },\n * async (envelope) => { ... }\n * );\n *\n * await transport.publish(\n * { type: \"OrderSubmitted\", orderId: \"123\" },\n * { endpoint: \"OrderSubmitted\" }\n * );\n * ```\n */\nexport class RabbitMqTransport implements Transport {\n private readonly options: RabbitMqTransportOptions;\n private readonly connectionManager: ConnectionManager;\n private readonly subscriptions: Subscription[] = [];\n private readonly handlers = new Map<\n string,\n (envelope: MessageEnvelope) => Promise<void>\n >();\n private started = false;\n\n constructor(options: RabbitMqTransportOptions) {\n this.options = options;\n this.connectionManager = new ConnectionManager(options);\n\n // Re-establish subscriptions on reconnect\n this.connectionManager.onConnected(() => {\n if (this.started) {\n void this.resubscribeAll();\n }\n });\n }\n\n async start(): Promise<void> {\n if (this.started) {\n return;\n }\n\n await this.connectionManager.connect();\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started) {\n return;\n }\n\n // Cancel all consumers\n const channel = this.connectionManager.getChannel();\n for (const sub of this.subscriptions) {\n if (sub.consumerTag) {\n try {\n await channel.cancel(sub.consumerTag);\n } catch {\n // Ignore cancel errors\n }\n }\n }\n\n await this.connectionManager.close();\n this.started = false;\n this.subscriptions.length = 0;\n this.handlers.clear();\n }\n\n async subscribe<TMessage extends BaseMessage>(\n options: TransportSubscribeOptions,\n handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>\n ): Promise<void> {\n const channel = this.connectionManager.getChannel();\n const { endpoint, concurrency = 1, group } = options;\n\n // Build queue name\n const queuePrefix = this.options.queuePrefix ?? \"\";\n const queueName = group\n ? `${queuePrefix}${endpoint}.${group}`\n : `${queuePrefix}${endpoint}`;\n\n // Assert queue\n await channel.assertQueue(queueName, {\n durable: this.options.durable ?? true,\n });\n\n // Bind queue to exchange with routing key = endpoint\n await channel.bindQueue(queueName, this.options.exchange, endpoint);\n\n // Set prefetch for concurrency control\n await channel.prefetch(concurrency);\n\n // Store handler\n this.handlers.set(\n queueName,\n handler as (envelope: MessageEnvelope) => Promise<void>\n );\n\n // Start consuming\n const { consumerTag } = await channel.consume(\n queueName,\n (msg) => {\n if (!msg) return;\n void this.handleMessage(queueName, msg);\n },\n { noAck: false }\n );\n\n // Track subscription\n this.subscriptions.push({\n endpoint,\n queueName,\n concurrency,\n consumerTag,\n });\n }\n\n async publish<TMessage extends BaseMessage>(\n message: TMessage,\n options: TransportPublishOptions\n ): Promise<void> {\n const channel = this.connectionManager.getChannel();\n const { endpoint, headers = {}, delayMs, key } = options;\n\n // Create envelope\n const envelope: MessageEnvelope<TMessage> = {\n id: randomUUID(),\n type: message.type,\n payload: message,\n headers: { ...headers },\n timestamp: new Date(),\n partitionKey: key,\n };\n\n // Serialize message\n const content = Buffer.from(JSON.stringify(envelope));\n\n // Build publish options\n const persistent = this.options.messageOptions?.persistent ?? true;\n const contentType =\n this.options.messageOptions?.contentType ?? \"application/json\";\n\n const publishOptions: {\n persistent: boolean;\n contentType: string;\n messageId: string;\n timestamp: number;\n headers: Record<string, unknown>;\n } = {\n persistent,\n contentType,\n messageId: envelope.id,\n timestamp: envelope.timestamp.getTime(),\n headers: {\n ...headers,\n \"x-message-type\": message.type,\n },\n };\n\n // Handle delayed messages using RabbitMQ delayed message plugin\n // or fall back to TTL + dead-letter pattern\n if (delayMs && delayMs > 0) {\n // Using x-delay header (requires rabbitmq_delayed_message_exchange plugin)\n // Alternative: use separate delay queues with TTL\n publishOptions.headers[\"x-delay\"] = delayMs;\n }\n\n // Publish to exchange with routing key = endpoint\n channel.publish(\n this.options.exchange,\n endpoint,\n content,\n publishOptions\n );\n }\n\n /**\n * Handle an incoming message.\n */\n private async handleMessage(\n queueName: string,\n msg: ConsumeMessage\n ): Promise<void> {\n const channel = this.connectionManager.getChannel();\n const handler = this.handlers.get(queueName);\n\n if (!handler) {\n // No handler, nack without requeue\n channel.nack(msg, false, false);\n return;\n }\n\n try {\n // Parse envelope\n const parsed = JSON.parse(msg.content.toString()) as MessageEnvelope;\n\n // Ensure timestamp is a Date\n const envelope: MessageEnvelope = {\n ...parsed,\n timestamp: new Date(parsed.timestamp),\n };\n\n // Execute handler\n await handler(envelope);\n\n // Acknowledge success\n channel.ack(msg);\n } catch (error) {\n console.error(\"[RabbitMQ] Message handler error:\", error);\n\n // Nack with requeue for retry\n // The bus runtime will handle retry logic via republishing\n channel.nack(msg, false, false);\n\n // Re-throw for error handling\n throw error;\n }\n }\n\n /**\n * Re-establish all subscriptions after reconnection.\n */\n private async resubscribeAll(): Promise<void> {\n const channel = this.connectionManager.getChannel();\n\n for (const sub of this.subscriptions) {\n try {\n // Re-assert queue\n await channel.assertQueue(sub.queueName, {\n durable: this.options.durable ?? true,\n });\n\n // Re-bind queue\n await channel.bindQueue(\n sub.queueName,\n this.options.exchange,\n sub.endpoint\n );\n\n // Set prefetch\n await channel.prefetch(sub.concurrency);\n\n // Re-consume\n const handler = this.handlers.get(sub.queueName);\n if (handler) {\n const { consumerTag } = await channel.consume(\n sub.queueName,\n (msg) => {\n if (!msg) return;\n void this.handleMessage(sub.queueName, msg);\n },\n { noAck: false }\n );\n sub.consumerTag = consumerTag;\n }\n } catch (error) {\n console.error(\n `[RabbitMQ] Failed to resubscribe to ${sub.queueName}:`,\n error\n );\n }\n }\n }\n\n /**\n * Check if the transport is connected.\n */\n isConnected(): boolean {\n return this.connectionManager.isConnected();\n }\n\n /**\n * Get transport statistics.\n */\n getStats(): { subscriptionCount: number; isConnected: boolean } {\n return {\n subscriptionCount: this.subscriptions.length,\n isConnected: this.isConnected(),\n };\n }\n}\n","import * as amqp from \"amqplib\";\nimport type { Channel } from \"amqplib\";\nimport type { RabbitMqTransportOptions } from \"./types.js\";\n\n// The amqplib types export ChannelModel which has the connection methods we need\ntype AmqpConnection = Awaited<ReturnType<typeof amqp.connect>>;\n\n/**\n * Manages AMQP connection and channel lifecycle with auto-reconnection.\n */\nexport class ConnectionManager {\n private readonly options: RabbitMqTransportOptions;\n private connection: AmqpConnection | null = null;\n private channel: Channel | null = null;\n private reconnectAttempt = 0;\n private isConnecting = false;\n private isClosed = false;\n\n private readonly onConnectedCallbacks: Array<() => void> = [];\n private readonly onDisconnectedCallbacks: Array<(error?: Error) => void> = [];\n\n constructor(options: RabbitMqTransportOptions) {\n this.options = options;\n }\n\n /**\n * Connect to RabbitMQ and set up the channel.\n */\n async connect(): Promise<void> {\n if (this.connection && this.channel) {\n return;\n }\n\n if (this.isConnecting) {\n // Wait for existing connection attempt\n return new Promise((resolve) => {\n this.onConnectedCallbacks.push(resolve);\n });\n }\n\n this.isConnecting = true;\n this.isClosed = false;\n\n try {\n this.connection = await amqp.connect(this.options.uri);\n\n // Set up connection error handling\n this.connection.on(\"error\", (error: Error) => {\n console.error(\"[RabbitMQ] Connection error:\", error.message);\n this.handleDisconnect(error);\n });\n\n this.connection.on(\"close\", () => {\n if (!this.isClosed) {\n console.warn(\"[RabbitMQ] Connection closed unexpectedly\");\n this.handleDisconnect();\n }\n });\n\n // Create channel\n this.channel = await this.connection.createChannel();\n\n // Set up channel error handling\n this.channel.on(\"error\", (error: Error) => {\n console.error(\"[RabbitMQ] Channel error:\", error.message);\n });\n\n this.channel.on(\"close\", () => {\n if (!this.isClosed) {\n console.warn(\"[RabbitMQ] Channel closed\");\n }\n });\n\n // Assert exchange\n const exchangeType = this.options.exchangeType ?? \"topic\";\n const durable = this.options.durable ?? true;\n\n await this.channel.assertExchange(this.options.exchange, exchangeType, {\n durable,\n });\n\n this.reconnectAttempt = 0;\n this.isConnecting = false;\n\n // Notify listeners\n for (const callback of this.onConnectedCallbacks) {\n callback();\n }\n this.onConnectedCallbacks.length = 0;\n\n console.info(\"[RabbitMQ] Connected successfully\");\n } catch (error) {\n this.isConnecting = false;\n throw error;\n }\n }\n\n /**\n * Handle disconnection and attempt reconnection.\n */\n private handleDisconnect(error?: Error): void {\n this.connection = null;\n this.channel = null;\n\n // Notify listeners\n for (const callback of this.onDisconnectedCallbacks) {\n callback(error);\n }\n\n if (!this.isClosed) {\n void this.reconnect();\n }\n }\n\n /**\n * Attempt to reconnect with exponential backoff.\n */\n private async reconnect(): Promise<void> {\n const maxAttempts = this.options.reconnect?.maxAttempts ?? 10;\n const initialDelay = this.options.reconnect?.initialDelayMs ?? 1000;\n const maxDelay = this.options.reconnect?.maxDelayMs ?? 30000;\n\n if (this.reconnectAttempt >= maxAttempts) {\n console.error(\"[RabbitMQ] Max reconnection attempts reached\");\n return;\n }\n\n this.reconnectAttempt++;\n\n const delay = Math.min(\n initialDelay * Math.pow(2, this.reconnectAttempt - 1),\n maxDelay\n );\n\n console.info(\n `[RabbitMQ] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt}/${maxAttempts})`\n );\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n\n if (this.isClosed) {\n return;\n }\n\n try {\n await this.connect();\n } catch (error) {\n console.error(\"[RabbitMQ] Reconnection failed:\", error);\n void this.reconnect();\n }\n }\n\n /**\n * Close the connection.\n */\n async close(): Promise<void> {\n this.isClosed = true;\n\n if (this.channel) {\n try {\n await this.channel.close();\n } catch {\n // Ignore close errors\n }\n this.channel = null;\n }\n\n if (this.connection) {\n try {\n await this.connection.close();\n } catch {\n // Ignore close errors\n }\n this.connection = null;\n }\n }\n\n /**\n * Get the current channel.\n * @throws if not connected\n */\n getChannel(): Channel {\n if (!this.channel) {\n throw new Error(\"Not connected to RabbitMQ\");\n }\n return this.channel;\n }\n\n /**\n * Check if connected.\n */\n isConnected(): boolean {\n return this.connection !== null && this.channel !== null;\n }\n\n /**\n * Register a callback for when connection is established.\n */\n onConnected(callback: () => void): void {\n this.onConnectedCallbacks.push(callback);\n }\n\n /**\n * Register a callback for when connection is lost.\n */\n onDisconnected(callback: (error?: Error) => void): void {\n this.onDisconnectedCallbacks.push(callback);\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,163 @@
1
+ import { Transport, BaseMessage, TransportSubscribeOptions, MessageEnvelope, TransportPublishOptions } from '@saga-bus/core';
2
+ import { Channel } from 'amqplib';
3
+
4
+ /**
5
+ * RabbitMQ transport configuration options.
6
+ */
7
+ interface RabbitMqTransportOptions {
8
+ /**
9
+ * AMQP connection URI.
10
+ * @example "amqp://guest:guest@localhost:5672"
11
+ */
12
+ uri: string;
13
+ /**
14
+ * Exchange name for publishing messages.
15
+ */
16
+ exchange: string;
17
+ /**
18
+ * Exchange type. Default: "topic"
19
+ */
20
+ exchangeType?: "topic" | "direct" | "fanout";
21
+ /**
22
+ * Whether the exchange should be durable. Default: true
23
+ */
24
+ durable?: boolean;
25
+ /**
26
+ * Prefix for queue names. Default: ""
27
+ */
28
+ queuePrefix?: string;
29
+ /**
30
+ * Reconnection options.
31
+ */
32
+ reconnect?: {
33
+ /** Maximum reconnection attempts. Default: 10 */
34
+ maxAttempts?: number;
35
+ /** Initial delay between reconnection attempts in ms. Default: 1000 */
36
+ initialDelayMs?: number;
37
+ /** Maximum delay between reconnection attempts in ms. Default: 30000 */
38
+ maxDelayMs?: number;
39
+ };
40
+ /**
41
+ * Message serialization options.
42
+ */
43
+ messageOptions?: {
44
+ /** Whether messages should be persistent. Default: true */
45
+ persistent?: boolean;
46
+ /** Content type header. Default: "application/json" */
47
+ contentType?: string;
48
+ };
49
+ }
50
+ /**
51
+ * Internal subscription state.
52
+ */
53
+ interface Subscription {
54
+ endpoint: string;
55
+ queueName: string;
56
+ concurrency: number;
57
+ consumerTag?: string;
58
+ }
59
+
60
+ /**
61
+ * RabbitMQ transport implementation using amqplib.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const transport = new RabbitMqTransport({
66
+ * uri: "amqp://localhost:5672",
67
+ * exchange: "saga-bus",
68
+ * });
69
+ *
70
+ * await transport.start();
71
+ *
72
+ * await transport.subscribe(
73
+ * { endpoint: "OrderSubmitted", concurrency: 5 },
74
+ * async (envelope) => { ... }
75
+ * );
76
+ *
77
+ * await transport.publish(
78
+ * { type: "OrderSubmitted", orderId: "123" },
79
+ * { endpoint: "OrderSubmitted" }
80
+ * );
81
+ * ```
82
+ */
83
+ declare class RabbitMqTransport implements Transport {
84
+ private readonly options;
85
+ private readonly connectionManager;
86
+ private readonly subscriptions;
87
+ private readonly handlers;
88
+ private started;
89
+ constructor(options: RabbitMqTransportOptions);
90
+ start(): Promise<void>;
91
+ stop(): Promise<void>;
92
+ subscribe<TMessage extends BaseMessage>(options: TransportSubscribeOptions, handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>): Promise<void>;
93
+ publish<TMessage extends BaseMessage>(message: TMessage, options: TransportPublishOptions): Promise<void>;
94
+ /**
95
+ * Handle an incoming message.
96
+ */
97
+ private handleMessage;
98
+ /**
99
+ * Re-establish all subscriptions after reconnection.
100
+ */
101
+ private resubscribeAll;
102
+ /**
103
+ * Check if the transport is connected.
104
+ */
105
+ isConnected(): boolean;
106
+ /**
107
+ * Get transport statistics.
108
+ */
109
+ getStats(): {
110
+ subscriptionCount: number;
111
+ isConnected: boolean;
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Manages AMQP connection and channel lifecycle with auto-reconnection.
117
+ */
118
+ declare class ConnectionManager {
119
+ private readonly options;
120
+ private connection;
121
+ private channel;
122
+ private reconnectAttempt;
123
+ private isConnecting;
124
+ private isClosed;
125
+ private readonly onConnectedCallbacks;
126
+ private readonly onDisconnectedCallbacks;
127
+ constructor(options: RabbitMqTransportOptions);
128
+ /**
129
+ * Connect to RabbitMQ and set up the channel.
130
+ */
131
+ connect(): Promise<void>;
132
+ /**
133
+ * Handle disconnection and attempt reconnection.
134
+ */
135
+ private handleDisconnect;
136
+ /**
137
+ * Attempt to reconnect with exponential backoff.
138
+ */
139
+ private reconnect;
140
+ /**
141
+ * Close the connection.
142
+ */
143
+ close(): Promise<void>;
144
+ /**
145
+ * Get the current channel.
146
+ * @throws if not connected
147
+ */
148
+ getChannel(): Channel;
149
+ /**
150
+ * Check if connected.
151
+ */
152
+ isConnected(): boolean;
153
+ /**
154
+ * Register a callback for when connection is established.
155
+ */
156
+ onConnected(callback: () => void): void;
157
+ /**
158
+ * Register a callback for when connection is lost.
159
+ */
160
+ onDisconnected(callback: (error?: Error) => void): void;
161
+ }
162
+
163
+ export { ConnectionManager, RabbitMqTransport, type RabbitMqTransportOptions, type Subscription };
@@ -0,0 +1,163 @@
1
+ import { Transport, BaseMessage, TransportSubscribeOptions, MessageEnvelope, TransportPublishOptions } from '@saga-bus/core';
2
+ import { Channel } from 'amqplib';
3
+
4
+ /**
5
+ * RabbitMQ transport configuration options.
6
+ */
7
+ interface RabbitMqTransportOptions {
8
+ /**
9
+ * AMQP connection URI.
10
+ * @example "amqp://guest:guest@localhost:5672"
11
+ */
12
+ uri: string;
13
+ /**
14
+ * Exchange name for publishing messages.
15
+ */
16
+ exchange: string;
17
+ /**
18
+ * Exchange type. Default: "topic"
19
+ */
20
+ exchangeType?: "topic" | "direct" | "fanout";
21
+ /**
22
+ * Whether the exchange should be durable. Default: true
23
+ */
24
+ durable?: boolean;
25
+ /**
26
+ * Prefix for queue names. Default: ""
27
+ */
28
+ queuePrefix?: string;
29
+ /**
30
+ * Reconnection options.
31
+ */
32
+ reconnect?: {
33
+ /** Maximum reconnection attempts. Default: 10 */
34
+ maxAttempts?: number;
35
+ /** Initial delay between reconnection attempts in ms. Default: 1000 */
36
+ initialDelayMs?: number;
37
+ /** Maximum delay between reconnection attempts in ms. Default: 30000 */
38
+ maxDelayMs?: number;
39
+ };
40
+ /**
41
+ * Message serialization options.
42
+ */
43
+ messageOptions?: {
44
+ /** Whether messages should be persistent. Default: true */
45
+ persistent?: boolean;
46
+ /** Content type header. Default: "application/json" */
47
+ contentType?: string;
48
+ };
49
+ }
50
+ /**
51
+ * Internal subscription state.
52
+ */
53
+ interface Subscription {
54
+ endpoint: string;
55
+ queueName: string;
56
+ concurrency: number;
57
+ consumerTag?: string;
58
+ }
59
+
60
+ /**
61
+ * RabbitMQ transport implementation using amqplib.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const transport = new RabbitMqTransport({
66
+ * uri: "amqp://localhost:5672",
67
+ * exchange: "saga-bus",
68
+ * });
69
+ *
70
+ * await transport.start();
71
+ *
72
+ * await transport.subscribe(
73
+ * { endpoint: "OrderSubmitted", concurrency: 5 },
74
+ * async (envelope) => { ... }
75
+ * );
76
+ *
77
+ * await transport.publish(
78
+ * { type: "OrderSubmitted", orderId: "123" },
79
+ * { endpoint: "OrderSubmitted" }
80
+ * );
81
+ * ```
82
+ */
83
+ declare class RabbitMqTransport implements Transport {
84
+ private readonly options;
85
+ private readonly connectionManager;
86
+ private readonly subscriptions;
87
+ private readonly handlers;
88
+ private started;
89
+ constructor(options: RabbitMqTransportOptions);
90
+ start(): Promise<void>;
91
+ stop(): Promise<void>;
92
+ subscribe<TMessage extends BaseMessage>(options: TransportSubscribeOptions, handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>): Promise<void>;
93
+ publish<TMessage extends BaseMessage>(message: TMessage, options: TransportPublishOptions): Promise<void>;
94
+ /**
95
+ * Handle an incoming message.
96
+ */
97
+ private handleMessage;
98
+ /**
99
+ * Re-establish all subscriptions after reconnection.
100
+ */
101
+ private resubscribeAll;
102
+ /**
103
+ * Check if the transport is connected.
104
+ */
105
+ isConnected(): boolean;
106
+ /**
107
+ * Get transport statistics.
108
+ */
109
+ getStats(): {
110
+ subscriptionCount: number;
111
+ isConnected: boolean;
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Manages AMQP connection and channel lifecycle with auto-reconnection.
117
+ */
118
+ declare class ConnectionManager {
119
+ private readonly options;
120
+ private connection;
121
+ private channel;
122
+ private reconnectAttempt;
123
+ private isConnecting;
124
+ private isClosed;
125
+ private readonly onConnectedCallbacks;
126
+ private readonly onDisconnectedCallbacks;
127
+ constructor(options: RabbitMqTransportOptions);
128
+ /**
129
+ * Connect to RabbitMQ and set up the channel.
130
+ */
131
+ connect(): Promise<void>;
132
+ /**
133
+ * Handle disconnection and attempt reconnection.
134
+ */
135
+ private handleDisconnect;
136
+ /**
137
+ * Attempt to reconnect with exponential backoff.
138
+ */
139
+ private reconnect;
140
+ /**
141
+ * Close the connection.
142
+ */
143
+ close(): Promise<void>;
144
+ /**
145
+ * Get the current channel.
146
+ * @throws if not connected
147
+ */
148
+ getChannel(): Channel;
149
+ /**
150
+ * Check if connected.
151
+ */
152
+ isConnected(): boolean;
153
+ /**
154
+ * Register a callback for when connection is established.
155
+ */
156
+ onConnected(callback: () => void): void;
157
+ /**
158
+ * Register a callback for when connection is lost.
159
+ */
160
+ onDisconnected(callback: (error?: Error) => void): void;
161
+ }
162
+
163
+ export { ConnectionManager, RabbitMqTransport, type RabbitMqTransportOptions, type Subscription };
package/dist/index.js ADDED
@@ -0,0 +1,347 @@
1
+ // src/RabbitMqTransport.ts
2
+ import { randomUUID } from "crypto";
3
+
4
+ // src/ConnectionManager.ts
5
+ import * as amqp from "amqplib";
6
+ var ConnectionManager = class {
7
+ options;
8
+ connection = null;
9
+ channel = null;
10
+ reconnectAttempt = 0;
11
+ isConnecting = false;
12
+ isClosed = false;
13
+ onConnectedCallbacks = [];
14
+ onDisconnectedCallbacks = [];
15
+ constructor(options) {
16
+ this.options = options;
17
+ }
18
+ /**
19
+ * Connect to RabbitMQ and set up the channel.
20
+ */
21
+ async connect() {
22
+ if (this.connection && this.channel) {
23
+ return;
24
+ }
25
+ if (this.isConnecting) {
26
+ return new Promise((resolve) => {
27
+ this.onConnectedCallbacks.push(resolve);
28
+ });
29
+ }
30
+ this.isConnecting = true;
31
+ this.isClosed = false;
32
+ try {
33
+ this.connection = await amqp.connect(this.options.uri);
34
+ this.connection.on("error", (error) => {
35
+ console.error("[RabbitMQ] Connection error:", error.message);
36
+ this.handleDisconnect(error);
37
+ });
38
+ this.connection.on("close", () => {
39
+ if (!this.isClosed) {
40
+ console.warn("[RabbitMQ] Connection closed unexpectedly");
41
+ this.handleDisconnect();
42
+ }
43
+ });
44
+ this.channel = await this.connection.createChannel();
45
+ this.channel.on("error", (error) => {
46
+ console.error("[RabbitMQ] Channel error:", error.message);
47
+ });
48
+ this.channel.on("close", () => {
49
+ if (!this.isClosed) {
50
+ console.warn("[RabbitMQ] Channel closed");
51
+ }
52
+ });
53
+ const exchangeType = this.options.exchangeType ?? "topic";
54
+ const durable = this.options.durable ?? true;
55
+ await this.channel.assertExchange(this.options.exchange, exchangeType, {
56
+ durable
57
+ });
58
+ this.reconnectAttempt = 0;
59
+ this.isConnecting = false;
60
+ for (const callback of this.onConnectedCallbacks) {
61
+ callback();
62
+ }
63
+ this.onConnectedCallbacks.length = 0;
64
+ console.info("[RabbitMQ] Connected successfully");
65
+ } catch (error) {
66
+ this.isConnecting = false;
67
+ throw error;
68
+ }
69
+ }
70
+ /**
71
+ * Handle disconnection and attempt reconnection.
72
+ */
73
+ handleDisconnect(error) {
74
+ this.connection = null;
75
+ this.channel = null;
76
+ for (const callback of this.onDisconnectedCallbacks) {
77
+ callback(error);
78
+ }
79
+ if (!this.isClosed) {
80
+ void this.reconnect();
81
+ }
82
+ }
83
+ /**
84
+ * Attempt to reconnect with exponential backoff.
85
+ */
86
+ async reconnect() {
87
+ const maxAttempts = this.options.reconnect?.maxAttempts ?? 10;
88
+ const initialDelay = this.options.reconnect?.initialDelayMs ?? 1e3;
89
+ const maxDelay = this.options.reconnect?.maxDelayMs ?? 3e4;
90
+ if (this.reconnectAttempt >= maxAttempts) {
91
+ console.error("[RabbitMQ] Max reconnection attempts reached");
92
+ return;
93
+ }
94
+ this.reconnectAttempt++;
95
+ const delay = Math.min(
96
+ initialDelay * Math.pow(2, this.reconnectAttempt - 1),
97
+ maxDelay
98
+ );
99
+ console.info(
100
+ `[RabbitMQ] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt}/${maxAttempts})`
101
+ );
102
+ await new Promise((resolve) => setTimeout(resolve, delay));
103
+ if (this.isClosed) {
104
+ return;
105
+ }
106
+ try {
107
+ await this.connect();
108
+ } catch (error) {
109
+ console.error("[RabbitMQ] Reconnection failed:", error);
110
+ void this.reconnect();
111
+ }
112
+ }
113
+ /**
114
+ * Close the connection.
115
+ */
116
+ async close() {
117
+ this.isClosed = true;
118
+ if (this.channel) {
119
+ try {
120
+ await this.channel.close();
121
+ } catch {
122
+ }
123
+ this.channel = null;
124
+ }
125
+ if (this.connection) {
126
+ try {
127
+ await this.connection.close();
128
+ } catch {
129
+ }
130
+ this.connection = null;
131
+ }
132
+ }
133
+ /**
134
+ * Get the current channel.
135
+ * @throws if not connected
136
+ */
137
+ getChannel() {
138
+ if (!this.channel) {
139
+ throw new Error("Not connected to RabbitMQ");
140
+ }
141
+ return this.channel;
142
+ }
143
+ /**
144
+ * Check if connected.
145
+ */
146
+ isConnected() {
147
+ return this.connection !== null && this.channel !== null;
148
+ }
149
+ /**
150
+ * Register a callback for when connection is established.
151
+ */
152
+ onConnected(callback) {
153
+ this.onConnectedCallbacks.push(callback);
154
+ }
155
+ /**
156
+ * Register a callback for when connection is lost.
157
+ */
158
+ onDisconnected(callback) {
159
+ this.onDisconnectedCallbacks.push(callback);
160
+ }
161
+ };
162
+
163
+ // src/RabbitMqTransport.ts
164
+ var RabbitMqTransport = class {
165
+ options;
166
+ connectionManager;
167
+ subscriptions = [];
168
+ handlers = /* @__PURE__ */ new Map();
169
+ started = false;
170
+ constructor(options) {
171
+ this.options = options;
172
+ this.connectionManager = new ConnectionManager(options);
173
+ this.connectionManager.onConnected(() => {
174
+ if (this.started) {
175
+ void this.resubscribeAll();
176
+ }
177
+ });
178
+ }
179
+ async start() {
180
+ if (this.started) {
181
+ return;
182
+ }
183
+ await this.connectionManager.connect();
184
+ this.started = true;
185
+ }
186
+ async stop() {
187
+ if (!this.started) {
188
+ return;
189
+ }
190
+ const channel = this.connectionManager.getChannel();
191
+ for (const sub of this.subscriptions) {
192
+ if (sub.consumerTag) {
193
+ try {
194
+ await channel.cancel(sub.consumerTag);
195
+ } catch {
196
+ }
197
+ }
198
+ }
199
+ await this.connectionManager.close();
200
+ this.started = false;
201
+ this.subscriptions.length = 0;
202
+ this.handlers.clear();
203
+ }
204
+ async subscribe(options, handler) {
205
+ const channel = this.connectionManager.getChannel();
206
+ const { endpoint, concurrency = 1, group } = options;
207
+ const queuePrefix = this.options.queuePrefix ?? "";
208
+ const queueName = group ? `${queuePrefix}${endpoint}.${group}` : `${queuePrefix}${endpoint}`;
209
+ await channel.assertQueue(queueName, {
210
+ durable: this.options.durable ?? true
211
+ });
212
+ await channel.bindQueue(queueName, this.options.exchange, endpoint);
213
+ await channel.prefetch(concurrency);
214
+ this.handlers.set(
215
+ queueName,
216
+ handler
217
+ );
218
+ const { consumerTag } = await channel.consume(
219
+ queueName,
220
+ (msg) => {
221
+ if (!msg) return;
222
+ void this.handleMessage(queueName, msg);
223
+ },
224
+ { noAck: false }
225
+ );
226
+ this.subscriptions.push({
227
+ endpoint,
228
+ queueName,
229
+ concurrency,
230
+ consumerTag
231
+ });
232
+ }
233
+ async publish(message, options) {
234
+ const channel = this.connectionManager.getChannel();
235
+ const { endpoint, headers = {}, delayMs, key } = options;
236
+ const envelope = {
237
+ id: randomUUID(),
238
+ type: message.type,
239
+ payload: message,
240
+ headers: { ...headers },
241
+ timestamp: /* @__PURE__ */ new Date(),
242
+ partitionKey: key
243
+ };
244
+ const content = Buffer.from(JSON.stringify(envelope));
245
+ const persistent = this.options.messageOptions?.persistent ?? true;
246
+ const contentType = this.options.messageOptions?.contentType ?? "application/json";
247
+ const publishOptions = {
248
+ persistent,
249
+ contentType,
250
+ messageId: envelope.id,
251
+ timestamp: envelope.timestamp.getTime(),
252
+ headers: {
253
+ ...headers,
254
+ "x-message-type": message.type
255
+ }
256
+ };
257
+ if (delayMs && delayMs > 0) {
258
+ publishOptions.headers["x-delay"] = delayMs;
259
+ }
260
+ channel.publish(
261
+ this.options.exchange,
262
+ endpoint,
263
+ content,
264
+ publishOptions
265
+ );
266
+ }
267
+ /**
268
+ * Handle an incoming message.
269
+ */
270
+ async handleMessage(queueName, msg) {
271
+ const channel = this.connectionManager.getChannel();
272
+ const handler = this.handlers.get(queueName);
273
+ if (!handler) {
274
+ channel.nack(msg, false, false);
275
+ return;
276
+ }
277
+ try {
278
+ const parsed = JSON.parse(msg.content.toString());
279
+ const envelope = {
280
+ ...parsed,
281
+ timestamp: new Date(parsed.timestamp)
282
+ };
283
+ await handler(envelope);
284
+ channel.ack(msg);
285
+ } catch (error) {
286
+ console.error("[RabbitMQ] Message handler error:", error);
287
+ channel.nack(msg, false, false);
288
+ throw error;
289
+ }
290
+ }
291
+ /**
292
+ * Re-establish all subscriptions after reconnection.
293
+ */
294
+ async resubscribeAll() {
295
+ const channel = this.connectionManager.getChannel();
296
+ for (const sub of this.subscriptions) {
297
+ try {
298
+ await channel.assertQueue(sub.queueName, {
299
+ durable: this.options.durable ?? true
300
+ });
301
+ await channel.bindQueue(
302
+ sub.queueName,
303
+ this.options.exchange,
304
+ sub.endpoint
305
+ );
306
+ await channel.prefetch(sub.concurrency);
307
+ const handler = this.handlers.get(sub.queueName);
308
+ if (handler) {
309
+ const { consumerTag } = await channel.consume(
310
+ sub.queueName,
311
+ (msg) => {
312
+ if (!msg) return;
313
+ void this.handleMessage(sub.queueName, msg);
314
+ },
315
+ { noAck: false }
316
+ );
317
+ sub.consumerTag = consumerTag;
318
+ }
319
+ } catch (error) {
320
+ console.error(
321
+ `[RabbitMQ] Failed to resubscribe to ${sub.queueName}:`,
322
+ error
323
+ );
324
+ }
325
+ }
326
+ }
327
+ /**
328
+ * Check if the transport is connected.
329
+ */
330
+ isConnected() {
331
+ return this.connectionManager.isConnected();
332
+ }
333
+ /**
334
+ * Get transport statistics.
335
+ */
336
+ getStats() {
337
+ return {
338
+ subscriptionCount: this.subscriptions.length,
339
+ isConnected: this.isConnected()
340
+ };
341
+ }
342
+ };
343
+ export {
344
+ ConnectionManager,
345
+ RabbitMqTransport
346
+ };
347
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/RabbitMqTransport.ts","../src/ConnectionManager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { ConsumeMessage } from \"amqplib\";\nimport type {\n Transport,\n TransportSubscribeOptions,\n TransportPublishOptions,\n BaseMessage,\n MessageEnvelope,\n} from \"@saga-bus/core\";\nimport type { RabbitMqTransportOptions, Subscription } from \"./types.js\";\nimport { ConnectionManager } from \"./ConnectionManager.js\";\n\n/**\n * RabbitMQ transport implementation using amqplib.\n *\n * @example\n * ```typescript\n * const transport = new RabbitMqTransport({\n * uri: \"amqp://localhost:5672\",\n * exchange: \"saga-bus\",\n * });\n *\n * await transport.start();\n *\n * await transport.subscribe(\n * { endpoint: \"OrderSubmitted\", concurrency: 5 },\n * async (envelope) => { ... }\n * );\n *\n * await transport.publish(\n * { type: \"OrderSubmitted\", orderId: \"123\" },\n * { endpoint: \"OrderSubmitted\" }\n * );\n * ```\n */\nexport class RabbitMqTransport implements Transport {\n private readonly options: RabbitMqTransportOptions;\n private readonly connectionManager: ConnectionManager;\n private readonly subscriptions: Subscription[] = [];\n private readonly handlers = new Map<\n string,\n (envelope: MessageEnvelope) => Promise<void>\n >();\n private started = false;\n\n constructor(options: RabbitMqTransportOptions) {\n this.options = options;\n this.connectionManager = new ConnectionManager(options);\n\n // Re-establish subscriptions on reconnect\n this.connectionManager.onConnected(() => {\n if (this.started) {\n void this.resubscribeAll();\n }\n });\n }\n\n async start(): Promise<void> {\n if (this.started) {\n return;\n }\n\n await this.connectionManager.connect();\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started) {\n return;\n }\n\n // Cancel all consumers\n const channel = this.connectionManager.getChannel();\n for (const sub of this.subscriptions) {\n if (sub.consumerTag) {\n try {\n await channel.cancel(sub.consumerTag);\n } catch {\n // Ignore cancel errors\n }\n }\n }\n\n await this.connectionManager.close();\n this.started = false;\n this.subscriptions.length = 0;\n this.handlers.clear();\n }\n\n async subscribe<TMessage extends BaseMessage>(\n options: TransportSubscribeOptions,\n handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>\n ): Promise<void> {\n const channel = this.connectionManager.getChannel();\n const { endpoint, concurrency = 1, group } = options;\n\n // Build queue name\n const queuePrefix = this.options.queuePrefix ?? \"\";\n const queueName = group\n ? `${queuePrefix}${endpoint}.${group}`\n : `${queuePrefix}${endpoint}`;\n\n // Assert queue\n await channel.assertQueue(queueName, {\n durable: this.options.durable ?? true,\n });\n\n // Bind queue to exchange with routing key = endpoint\n await channel.bindQueue(queueName, this.options.exchange, endpoint);\n\n // Set prefetch for concurrency control\n await channel.prefetch(concurrency);\n\n // Store handler\n this.handlers.set(\n queueName,\n handler as (envelope: MessageEnvelope) => Promise<void>\n );\n\n // Start consuming\n const { consumerTag } = await channel.consume(\n queueName,\n (msg) => {\n if (!msg) return;\n void this.handleMessage(queueName, msg);\n },\n { noAck: false }\n );\n\n // Track subscription\n this.subscriptions.push({\n endpoint,\n queueName,\n concurrency,\n consumerTag,\n });\n }\n\n async publish<TMessage extends BaseMessage>(\n message: TMessage,\n options: TransportPublishOptions\n ): Promise<void> {\n const channel = this.connectionManager.getChannel();\n const { endpoint, headers = {}, delayMs, key } = options;\n\n // Create envelope\n const envelope: MessageEnvelope<TMessage> = {\n id: randomUUID(),\n type: message.type,\n payload: message,\n headers: { ...headers },\n timestamp: new Date(),\n partitionKey: key,\n };\n\n // Serialize message\n const content = Buffer.from(JSON.stringify(envelope));\n\n // Build publish options\n const persistent = this.options.messageOptions?.persistent ?? true;\n const contentType =\n this.options.messageOptions?.contentType ?? \"application/json\";\n\n const publishOptions: {\n persistent: boolean;\n contentType: string;\n messageId: string;\n timestamp: number;\n headers: Record<string, unknown>;\n } = {\n persistent,\n contentType,\n messageId: envelope.id,\n timestamp: envelope.timestamp.getTime(),\n headers: {\n ...headers,\n \"x-message-type\": message.type,\n },\n };\n\n // Handle delayed messages using RabbitMQ delayed message plugin\n // or fall back to TTL + dead-letter pattern\n if (delayMs && delayMs > 0) {\n // Using x-delay header (requires rabbitmq_delayed_message_exchange plugin)\n // Alternative: use separate delay queues with TTL\n publishOptions.headers[\"x-delay\"] = delayMs;\n }\n\n // Publish to exchange with routing key = endpoint\n channel.publish(\n this.options.exchange,\n endpoint,\n content,\n publishOptions\n );\n }\n\n /**\n * Handle an incoming message.\n */\n private async handleMessage(\n queueName: string,\n msg: ConsumeMessage\n ): Promise<void> {\n const channel = this.connectionManager.getChannel();\n const handler = this.handlers.get(queueName);\n\n if (!handler) {\n // No handler, nack without requeue\n channel.nack(msg, false, false);\n return;\n }\n\n try {\n // Parse envelope\n const parsed = JSON.parse(msg.content.toString()) as MessageEnvelope;\n\n // Ensure timestamp is a Date\n const envelope: MessageEnvelope = {\n ...parsed,\n timestamp: new Date(parsed.timestamp),\n };\n\n // Execute handler\n await handler(envelope);\n\n // Acknowledge success\n channel.ack(msg);\n } catch (error) {\n console.error(\"[RabbitMQ] Message handler error:\", error);\n\n // Nack with requeue for retry\n // The bus runtime will handle retry logic via republishing\n channel.nack(msg, false, false);\n\n // Re-throw for error handling\n throw error;\n }\n }\n\n /**\n * Re-establish all subscriptions after reconnection.\n */\n private async resubscribeAll(): Promise<void> {\n const channel = this.connectionManager.getChannel();\n\n for (const sub of this.subscriptions) {\n try {\n // Re-assert queue\n await channel.assertQueue(sub.queueName, {\n durable: this.options.durable ?? true,\n });\n\n // Re-bind queue\n await channel.bindQueue(\n sub.queueName,\n this.options.exchange,\n sub.endpoint\n );\n\n // Set prefetch\n await channel.prefetch(sub.concurrency);\n\n // Re-consume\n const handler = this.handlers.get(sub.queueName);\n if (handler) {\n const { consumerTag } = await channel.consume(\n sub.queueName,\n (msg) => {\n if (!msg) return;\n void this.handleMessage(sub.queueName, msg);\n },\n { noAck: false }\n );\n sub.consumerTag = consumerTag;\n }\n } catch (error) {\n console.error(\n `[RabbitMQ] Failed to resubscribe to ${sub.queueName}:`,\n error\n );\n }\n }\n }\n\n /**\n * Check if the transport is connected.\n */\n isConnected(): boolean {\n return this.connectionManager.isConnected();\n }\n\n /**\n * Get transport statistics.\n */\n getStats(): { subscriptionCount: number; isConnected: boolean } {\n return {\n subscriptionCount: this.subscriptions.length,\n isConnected: this.isConnected(),\n };\n }\n}\n","import * as amqp from \"amqplib\";\nimport type { Channel } from \"amqplib\";\nimport type { RabbitMqTransportOptions } from \"./types.js\";\n\n// The amqplib types export ChannelModel which has the connection methods we need\ntype AmqpConnection = Awaited<ReturnType<typeof amqp.connect>>;\n\n/**\n * Manages AMQP connection and channel lifecycle with auto-reconnection.\n */\nexport class ConnectionManager {\n private readonly options: RabbitMqTransportOptions;\n private connection: AmqpConnection | null = null;\n private channel: Channel | null = null;\n private reconnectAttempt = 0;\n private isConnecting = false;\n private isClosed = false;\n\n private readonly onConnectedCallbacks: Array<() => void> = [];\n private readonly onDisconnectedCallbacks: Array<(error?: Error) => void> = [];\n\n constructor(options: RabbitMqTransportOptions) {\n this.options = options;\n }\n\n /**\n * Connect to RabbitMQ and set up the channel.\n */\n async connect(): Promise<void> {\n if (this.connection && this.channel) {\n return;\n }\n\n if (this.isConnecting) {\n // Wait for existing connection attempt\n return new Promise((resolve) => {\n this.onConnectedCallbacks.push(resolve);\n });\n }\n\n this.isConnecting = true;\n this.isClosed = false;\n\n try {\n this.connection = await amqp.connect(this.options.uri);\n\n // Set up connection error handling\n this.connection.on(\"error\", (error: Error) => {\n console.error(\"[RabbitMQ] Connection error:\", error.message);\n this.handleDisconnect(error);\n });\n\n this.connection.on(\"close\", () => {\n if (!this.isClosed) {\n console.warn(\"[RabbitMQ] Connection closed unexpectedly\");\n this.handleDisconnect();\n }\n });\n\n // Create channel\n this.channel = await this.connection.createChannel();\n\n // Set up channel error handling\n this.channel.on(\"error\", (error: Error) => {\n console.error(\"[RabbitMQ] Channel error:\", error.message);\n });\n\n this.channel.on(\"close\", () => {\n if (!this.isClosed) {\n console.warn(\"[RabbitMQ] Channel closed\");\n }\n });\n\n // Assert exchange\n const exchangeType = this.options.exchangeType ?? \"topic\";\n const durable = this.options.durable ?? true;\n\n await this.channel.assertExchange(this.options.exchange, exchangeType, {\n durable,\n });\n\n this.reconnectAttempt = 0;\n this.isConnecting = false;\n\n // Notify listeners\n for (const callback of this.onConnectedCallbacks) {\n callback();\n }\n this.onConnectedCallbacks.length = 0;\n\n console.info(\"[RabbitMQ] Connected successfully\");\n } catch (error) {\n this.isConnecting = false;\n throw error;\n }\n }\n\n /**\n * Handle disconnection and attempt reconnection.\n */\n private handleDisconnect(error?: Error): void {\n this.connection = null;\n this.channel = null;\n\n // Notify listeners\n for (const callback of this.onDisconnectedCallbacks) {\n callback(error);\n }\n\n if (!this.isClosed) {\n void this.reconnect();\n }\n }\n\n /**\n * Attempt to reconnect with exponential backoff.\n */\n private async reconnect(): Promise<void> {\n const maxAttempts = this.options.reconnect?.maxAttempts ?? 10;\n const initialDelay = this.options.reconnect?.initialDelayMs ?? 1000;\n const maxDelay = this.options.reconnect?.maxDelayMs ?? 30000;\n\n if (this.reconnectAttempt >= maxAttempts) {\n console.error(\"[RabbitMQ] Max reconnection attempts reached\");\n return;\n }\n\n this.reconnectAttempt++;\n\n const delay = Math.min(\n initialDelay * Math.pow(2, this.reconnectAttempt - 1),\n maxDelay\n );\n\n console.info(\n `[RabbitMQ] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt}/${maxAttempts})`\n );\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n\n if (this.isClosed) {\n return;\n }\n\n try {\n await this.connect();\n } catch (error) {\n console.error(\"[RabbitMQ] Reconnection failed:\", error);\n void this.reconnect();\n }\n }\n\n /**\n * Close the connection.\n */\n async close(): Promise<void> {\n this.isClosed = true;\n\n if (this.channel) {\n try {\n await this.channel.close();\n } catch {\n // Ignore close errors\n }\n this.channel = null;\n }\n\n if (this.connection) {\n try {\n await this.connection.close();\n } catch {\n // Ignore close errors\n }\n this.connection = null;\n }\n }\n\n /**\n * Get the current channel.\n * @throws if not connected\n */\n getChannel(): Channel {\n if (!this.channel) {\n throw new Error(\"Not connected to RabbitMQ\");\n }\n return this.channel;\n }\n\n /**\n * Check if connected.\n */\n isConnected(): boolean {\n return this.connection !== null && this.channel !== null;\n }\n\n /**\n * Register a callback for when connection is established.\n */\n onConnected(callback: () => void): void {\n this.onConnectedCallbacks.push(callback);\n }\n\n /**\n * Register a callback for when connection is lost.\n */\n onDisconnected(callback: (error?: Error) => void): void {\n this.onDisconnectedCallbacks.push(callback);\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;;;ACA3B,YAAY,UAAU;AAUf,IAAM,oBAAN,MAAwB;AAAA,EACZ;AAAA,EACT,aAAoC;AAAA,EACpC,UAA0B;AAAA,EAC1B,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,WAAW;AAAA,EAEF,uBAA0C,CAAC;AAAA,EAC3C,0BAA0D,CAAC;AAAA,EAE5E,YAAY,SAAmC;AAC7C,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAyB;AAC7B,QAAI,KAAK,cAAc,KAAK,SAAS;AACnC;AAAA,IACF;AAEA,QAAI,KAAK,cAAc;AAErB,aAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,aAAK,qBAAqB,KAAK,OAAO;AAAA,MACxC,CAAC;AAAA,IACH;AAEA,SAAK,eAAe;AACpB,SAAK,WAAW;AAEhB,QAAI;AACF,WAAK,aAAa,MAAW,aAAQ,KAAK,QAAQ,GAAG;AAGrD,WAAK,WAAW,GAAG,SAAS,CAAC,UAAiB;AAC5C,gBAAQ,MAAM,gCAAgC,MAAM,OAAO;AAC3D,aAAK,iBAAiB,KAAK;AAAA,MAC7B,CAAC;AAED,WAAK,WAAW,GAAG,SAAS,MAAM;AAChC,YAAI,CAAC,KAAK,UAAU;AAClB,kBAAQ,KAAK,2CAA2C;AACxD,eAAK,iBAAiB;AAAA,QACxB;AAAA,MACF,CAAC;AAGD,WAAK,UAAU,MAAM,KAAK,WAAW,cAAc;AAGnD,WAAK,QAAQ,GAAG,SAAS,CAAC,UAAiB;AACzC,gBAAQ,MAAM,6BAA6B,MAAM,OAAO;AAAA,MAC1D,CAAC;AAED,WAAK,QAAQ,GAAG,SAAS,MAAM;AAC7B,YAAI,CAAC,KAAK,UAAU;AAClB,kBAAQ,KAAK,2BAA2B;AAAA,QAC1C;AAAA,MACF,CAAC;AAGD,YAAM,eAAe,KAAK,QAAQ,gBAAgB;AAClD,YAAM,UAAU,KAAK,QAAQ,WAAW;AAExC,YAAM,KAAK,QAAQ,eAAe,KAAK,QAAQ,UAAU,cAAc;AAAA,QACrE;AAAA,MACF,CAAC;AAED,WAAK,mBAAmB;AACxB,WAAK,eAAe;AAGpB,iBAAW,YAAY,KAAK,sBAAsB;AAChD,iBAAS;AAAA,MACX;AACA,WAAK,qBAAqB,SAAS;AAEnC,cAAQ,KAAK,mCAAmC;AAAA,IAClD,SAAS,OAAO;AACd,WAAK,eAAe;AACpB,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAiB,OAAqB;AAC5C,SAAK,aAAa;AAClB,SAAK,UAAU;AAGf,eAAW,YAAY,KAAK,yBAAyB;AACnD,eAAS,KAAK;AAAA,IAChB;AAEA,QAAI,CAAC,KAAK,UAAU;AAClB,WAAK,KAAK,UAAU;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,YAA2B;AACvC,UAAM,cAAc,KAAK,QAAQ,WAAW,eAAe;AAC3D,UAAM,eAAe,KAAK,QAAQ,WAAW,kBAAkB;AAC/D,UAAM,WAAW,KAAK,QAAQ,WAAW,cAAc;AAEvD,QAAI,KAAK,oBAAoB,aAAa;AACxC,cAAQ,MAAM,8CAA8C;AAC5D;AAAA,IACF;AAEA,SAAK;AAEL,UAAM,QAAQ,KAAK;AAAA,MACjB,eAAe,KAAK,IAAI,GAAG,KAAK,mBAAmB,CAAC;AAAA,MACpD;AAAA,IACF;AAEA,YAAQ;AAAA,MACN,8BAA8B,KAAK,eAAe,KAAK,gBAAgB,IAAI,WAAW;AAAA,IACxF;AAEA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,CAAC;AAEzD,QAAI,KAAK,UAAU;AACjB;AAAA,IACF;AAEA,QAAI;AACF,YAAM,KAAK,QAAQ;AAAA,IACrB,SAAS,OAAO;AACd,cAAQ,MAAM,mCAAmC,KAAK;AACtD,WAAK,KAAK,UAAU;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,SAAK,WAAW;AAEhB,QAAI,KAAK,SAAS;AAChB,UAAI;AACF,cAAM,KAAK,QAAQ,MAAM;AAAA,MAC3B,QAAQ;AAAA,MAER;AACA,WAAK,UAAU;AAAA,IACjB;AAEA,QAAI,KAAK,YAAY;AACnB,UAAI;AACF,cAAM,KAAK,WAAW,MAAM;AAAA,MAC9B,QAAQ;AAAA,MAER;AACA,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAsB;AACpB,QAAI,CAAC,KAAK,SAAS;AACjB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,cAAuB;AACrB,WAAO,KAAK,eAAe,QAAQ,KAAK,YAAY;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,UAA4B;AACtC,SAAK,qBAAqB,KAAK,QAAQ;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,UAAyC;AACtD,SAAK,wBAAwB,KAAK,QAAQ;AAAA,EAC5C;AACF;;;AD7KO,IAAM,oBAAN,MAA6C;AAAA,EACjC;AAAA,EACA;AAAA,EACA,gBAAgC,CAAC;AAAA,EACjC,WAAW,oBAAI,IAG9B;AAAA,EACM,UAAU;AAAA,EAElB,YAAY,SAAmC;AAC7C,SAAK,UAAU;AACf,SAAK,oBAAoB,IAAI,kBAAkB,OAAO;AAGtD,SAAK,kBAAkB,YAAY,MAAM;AACvC,UAAI,KAAK,SAAS;AAChB,aAAK,KAAK,eAAe;AAAA,MAC3B;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI,KAAK,SAAS;AAChB;AAAA,IACF;AAEA,UAAM,KAAK,kBAAkB,QAAQ;AACrC,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAM,OAAsB;AAC1B,QAAI,CAAC,KAAK,SAAS;AACjB;AAAA,IACF;AAGA,UAAM,UAAU,KAAK,kBAAkB,WAAW;AAClD,eAAW,OAAO,KAAK,eAAe;AACpC,UAAI,IAAI,aAAa;AACnB,YAAI;AACF,gBAAM,QAAQ,OAAO,IAAI,WAAW;AAAA,QACtC,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,UAAM,KAAK,kBAAkB,MAAM;AACnC,SAAK,UAAU;AACf,SAAK,cAAc,SAAS;AAC5B,SAAK,SAAS,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,UACJ,SACA,SACe;AACf,UAAM,UAAU,KAAK,kBAAkB,WAAW;AAClD,UAAM,EAAE,UAAU,cAAc,GAAG,MAAM,IAAI;AAG7C,UAAM,cAAc,KAAK,QAAQ,eAAe;AAChD,UAAM,YAAY,QACd,GAAG,WAAW,GAAG,QAAQ,IAAI,KAAK,KAClC,GAAG,WAAW,GAAG,QAAQ;AAG7B,UAAM,QAAQ,YAAY,WAAW;AAAA,MACnC,SAAS,KAAK,QAAQ,WAAW;AAAA,IACnC,CAAC;AAGD,UAAM,QAAQ,UAAU,WAAW,KAAK,QAAQ,UAAU,QAAQ;AAGlE,UAAM,QAAQ,SAAS,WAAW;AAGlC,SAAK,SAAS;AAAA,MACZ;AAAA,MACA;AAAA,IACF;AAGA,UAAM,EAAE,YAAY,IAAI,MAAM,QAAQ;AAAA,MACpC;AAAA,MACA,CAAC,QAAQ;AACP,YAAI,CAAC,IAAK;AACV,aAAK,KAAK,cAAc,WAAW,GAAG;AAAA,MACxC;AAAA,MACA,EAAE,OAAO,MAAM;AAAA,IACjB;AAGA,SAAK,cAAc,KAAK;AAAA,MACtB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QACJ,SACA,SACe;AACf,UAAM,UAAU,KAAK,kBAAkB,WAAW;AAClD,UAAM,EAAE,UAAU,UAAU,CAAC,GAAG,SAAS,IAAI,IAAI;AAGjD,UAAM,WAAsC;AAAA,MAC1C,IAAI,WAAW;AAAA,MACf,MAAM,QAAQ;AAAA,MACd,SAAS;AAAA,MACT,SAAS,EAAE,GAAG,QAAQ;AAAA,MACtB,WAAW,oBAAI,KAAK;AAAA,MACpB,cAAc;AAAA,IAChB;AAGA,UAAM,UAAU,OAAO,KAAK,KAAK,UAAU,QAAQ,CAAC;AAGpD,UAAM,aAAa,KAAK,QAAQ,gBAAgB,cAAc;AAC9D,UAAM,cACJ,KAAK,QAAQ,gBAAgB,eAAe;AAE9C,UAAM,iBAMF;AAAA,MACF;AAAA,MACA;AAAA,MACA,WAAW,SAAS;AAAA,MACpB,WAAW,SAAS,UAAU,QAAQ;AAAA,MACtC,SAAS;AAAA,QACP,GAAG;AAAA,QACH,kBAAkB,QAAQ;AAAA,MAC5B;AAAA,IACF;AAIA,QAAI,WAAW,UAAU,GAAG;AAG1B,qBAAe,QAAQ,SAAS,IAAI;AAAA,IACtC;AAGA,YAAQ;AAAA,MACN,KAAK,QAAQ;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,cACZ,WACA,KACe;AACf,UAAM,UAAU,KAAK,kBAAkB,WAAW;AAClD,UAAM,UAAU,KAAK,SAAS,IAAI,SAAS;AAE3C,QAAI,CAAC,SAAS;AAEZ,cAAQ,KAAK,KAAK,OAAO,KAAK;AAC9B;AAAA,IACF;AAEA,QAAI;AAEF,YAAM,SAAS,KAAK,MAAM,IAAI,QAAQ,SAAS,CAAC;AAGhD,YAAM,WAA4B;AAAA,QAChC,GAAG;AAAA,QACH,WAAW,IAAI,KAAK,OAAO,SAAS;AAAA,MACtC;AAGA,YAAM,QAAQ,QAAQ;AAGtB,cAAQ,IAAI,GAAG;AAAA,IACjB,SAAS,OAAO;AACd,cAAQ,MAAM,qCAAqC,KAAK;AAIxD,cAAQ,KAAK,KAAK,OAAO,KAAK;AAG9B,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,iBAAgC;AAC5C,UAAM,UAAU,KAAK,kBAAkB,WAAW;AAElD,eAAW,OAAO,KAAK,eAAe;AACpC,UAAI;AAEF,cAAM,QAAQ,YAAY,IAAI,WAAW;AAAA,UACvC,SAAS,KAAK,QAAQ,WAAW;AAAA,QACnC,CAAC;AAGD,cAAM,QAAQ;AAAA,UACZ,IAAI;AAAA,UACJ,KAAK,QAAQ;AAAA,UACb,IAAI;AAAA,QACN;AAGA,cAAM,QAAQ,SAAS,IAAI,WAAW;AAGtC,cAAM,UAAU,KAAK,SAAS,IAAI,IAAI,SAAS;AAC/C,YAAI,SAAS;AACX,gBAAM,EAAE,YAAY,IAAI,MAAM,QAAQ;AAAA,YACpC,IAAI;AAAA,YACJ,CAAC,QAAQ;AACP,kBAAI,CAAC,IAAK;AACV,mBAAK,KAAK,cAAc,IAAI,WAAW,GAAG;AAAA,YAC5C;AAAA,YACA,EAAE,OAAO,MAAM;AAAA,UACjB;AACA,cAAI,cAAc;AAAA,QACpB;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ;AAAA,UACN,uCAAuC,IAAI,SAAS;AAAA,UACpD;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,cAAuB;AACrB,WAAO,KAAK,kBAAkB,YAAY;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA,EAKA,WAAgE;AAC9D,WAAO;AAAA,MACL,mBAAmB,KAAK,cAAc;AAAA,MACtC,aAAa,KAAK,YAAY;AAAA,IAChC;AAAA,EACF;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@saga-bus/transport-rabbitmq",
3
+ "version": "0.1.2",
4
+ "description": "RabbitMQ transport for saga-bus production deployments",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/d-e-a-n-f/saga-bus.git",
26
+ "directory": "packages/transport-rabbitmq"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/d-e-a-n-f/saga-bus/issues"
30
+ },
31
+ "homepage": "https://github.com/d-e-a-n-f/saga-bus#readme",
32
+ "keywords": [
33
+ "saga",
34
+ "message-bus",
35
+ "transport",
36
+ "rabbitmq",
37
+ "amqp"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "lint": "eslint src/",
43
+ "check-types": "tsc --noEmit",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest"
46
+ },
47
+ "dependencies": {
48
+ "@saga-bus/core": "workspace:*",
49
+ "amqplib": "^0.10.5"
50
+ },
51
+ "devDependencies": {
52
+ "@repo/eslint-config": "workspace:*",
53
+ "@repo/typescript-config": "workspace:*",
54
+ "@testcontainers/rabbitmq": "^10.24.2",
55
+ "@types/amqplib": "^0.10.7",
56
+ "tsup": "^8.5.0",
57
+ "typescript": "^5.9.2",
58
+ "vitest": "^3.2.4"
59
+ },
60
+ "peerDependencies": {
61
+ "amqplib": ">=0.10.0"
62
+ }
63
+ }