@a_jackie_z/event-bus 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -0
- package/README.md +1011 -0
- package/dist/chunk-5DMUVWFR.js +151 -0
- package/dist/chunk-5DMUVWFR.js.map +1 -0
- package/dist/chunk-JBE5KWYZ.js +131 -0
- package/dist/chunk-JBE5KWYZ.js.map +1 -0
- package/dist/chunk-YP7YVSEN.js +88 -0
- package/dist/chunk-YP7YVSEN.js.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tests/broadcast-consumer.d.ts +2 -0
- package/dist/lib/tests/broadcast-consumer.js +112 -0
- package/dist/lib/tests/broadcast-consumer.js.map +1 -0
- package/dist/lib/tests/broadcast-producer.d.ts +2 -0
- package/dist/lib/tests/broadcast-producer.js +57 -0
- package/dist/lib/tests/broadcast-producer.js.map +1 -0
- package/dist/lib/tests/consumer.d.ts +2 -0
- package/dist/lib/tests/consumer.js +62 -0
- package/dist/lib/tests/consumer.js.map +1 -0
- package/dist/lib/tests/mixed-consumer.d.ts +2 -0
- package/dist/lib/tests/mixed-consumer.js +117 -0
- package/dist/lib/tests/mixed-consumer.js.map +1 -0
- package/dist/lib/tests/mixed-producer.d.ts +2 -0
- package/dist/lib/tests/mixed-producer.js +59 -0
- package/dist/lib/tests/mixed-producer.js.map +1 -0
- package/dist/lib/tests/producer.d.ts +2 -0
- package/dist/lib/tests/producer.js +60 -0
- package/dist/lib/tests/producer.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseEventBusConnection
|
|
3
|
+
} from "./chunk-JBE5KWYZ.js";
|
|
4
|
+
|
|
5
|
+
// lib/consumer.ts
|
|
6
|
+
import { logger } from "@a_jackie_z/logger";
|
|
7
|
+
var EventBusConsumer = class extends BaseEventBusConnection {
|
|
8
|
+
queueHandlers;
|
|
9
|
+
exchangeBindings;
|
|
10
|
+
activeConsumerTags = /* @__PURE__ */ new Set();
|
|
11
|
+
constructor(options) {
|
|
12
|
+
super(options);
|
|
13
|
+
this.queueHandlers = options.queueHandlers;
|
|
14
|
+
if (options.exchangeBindings !== void 0) {
|
|
15
|
+
this.exchangeBindings = options.exchangeBindings;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async createChannelAndSetup() {
|
|
19
|
+
if (!this.channelModel) {
|
|
20
|
+
throw new Error("Channel model is not initialized");
|
|
21
|
+
}
|
|
22
|
+
this.channel = await this.channelModel.createChannel();
|
|
23
|
+
this.channel.prefetch(1);
|
|
24
|
+
this.activeConsumerTags.clear();
|
|
25
|
+
for (const [queueName, handlers] of this.queueHandlers.entries()) {
|
|
26
|
+
const exchangeName = this.exchangeBindings?.get(queueName);
|
|
27
|
+
if (exchangeName) {
|
|
28
|
+
await this.channel.assertExchange(exchangeName, "fanout", {
|
|
29
|
+
durable: false,
|
|
30
|
+
// Exchange doesn't need to persist (stateless routing)
|
|
31
|
+
autoDelete: false
|
|
32
|
+
// Exchange is not auto-deleted
|
|
33
|
+
});
|
|
34
|
+
const { queue: exclusiveQueueName } = await this.channel.assertQueue("", {
|
|
35
|
+
exclusive: true,
|
|
36
|
+
// Queue is exclusive to this connection
|
|
37
|
+
autoDelete: true
|
|
38
|
+
// Queue is deleted when connection closes
|
|
39
|
+
});
|
|
40
|
+
await this.channel.bindQueue(exclusiveQueueName, exchangeName, "");
|
|
41
|
+
logger.info({ queueName, exchangeName, exclusiveQueueName }, "Queue bound to exchange for broadcast");
|
|
42
|
+
const { consumerTag } = await this.channel.consume(exclusiveQueueName, async (msg) => {
|
|
43
|
+
if (msg) {
|
|
44
|
+
let successCount = 0;
|
|
45
|
+
const content = msg.content.toString();
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(content);
|
|
48
|
+
for (const handler of handlers) {
|
|
49
|
+
try {
|
|
50
|
+
await handler(data);
|
|
51
|
+
successCount++;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.error({ error, queueName, exchangeName }, "Handler failed for broadcast queue");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (successCount > 0) {
|
|
57
|
+
this.channel.ack(msg);
|
|
58
|
+
} else {
|
|
59
|
+
logger.error({ queueName, exchangeName, handlerCount: handlers.length }, "All handlers failed for broadcast queue");
|
|
60
|
+
this.channel.nack(msg, false, false);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error({ error, queueName, exchangeName }, "Failed to parse message from broadcast queue");
|
|
64
|
+
this.channel.nack(msg, false, false);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, {
|
|
68
|
+
noAck: false
|
|
69
|
+
// Require acknowledgment after processing
|
|
70
|
+
});
|
|
71
|
+
this.activeConsumerTags.add(consumerTag);
|
|
72
|
+
logger.info({ queueName, exchangeName, exclusiveQueueName, consumerTag }, "Consumer registered for broadcast queue");
|
|
73
|
+
} else {
|
|
74
|
+
await this.channel.assertQueue(queueName, {
|
|
75
|
+
durable: true,
|
|
76
|
+
// Queue persists after restart
|
|
77
|
+
exclusive: false,
|
|
78
|
+
// Multiple consumers can connect
|
|
79
|
+
autoDelete: false
|
|
80
|
+
// Queue is not auto-deleted
|
|
81
|
+
});
|
|
82
|
+
const { consumerTag } = await this.channel.consume(queueName, async (msg) => {
|
|
83
|
+
if (msg) {
|
|
84
|
+
let successCount = 0;
|
|
85
|
+
const content = msg.content.toString();
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.parse(content);
|
|
88
|
+
for (const handler of handlers) {
|
|
89
|
+
try {
|
|
90
|
+
await handler(data);
|
|
91
|
+
successCount++;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error({ error, queueName }, "Handler failed for queue");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (successCount > 0) {
|
|
97
|
+
this.channel.ack(msg);
|
|
98
|
+
} else {
|
|
99
|
+
logger.error({ queueName, handlerCount: handlers.length }, "All handlers failed for queue, message will not be requeued");
|
|
100
|
+
this.channel.nack(msg, false, false);
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error({ error, queueName }, "Failed to parse message from queue");
|
|
104
|
+
this.channel.nack(msg, false, false);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}, {
|
|
108
|
+
noAck: false
|
|
109
|
+
// Require acknowledgment after processing
|
|
110
|
+
});
|
|
111
|
+
this.activeConsumerTags.add(consumerTag);
|
|
112
|
+
logger.info({ queueName, consumerTag }, "Consumer registered for queue");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async disconnect() {
|
|
117
|
+
logger.info("Disconnecting consumer");
|
|
118
|
+
if (this.channel && this.activeConsumerTags.size > 0) {
|
|
119
|
+
const cancelPromises = [];
|
|
120
|
+
for (const consumerTag of this.activeConsumerTags) {
|
|
121
|
+
const cancelPromise = (async () => {
|
|
122
|
+
try {
|
|
123
|
+
logger.debug({ consumerTag }, "Cancelling consumer");
|
|
124
|
+
await Promise.race([
|
|
125
|
+
this.channel.cancel(consumerTag),
|
|
126
|
+
new Promise(
|
|
127
|
+
(_, reject) => setTimeout(() => reject(new Error("Consumer cancellation timeout")), 5e3)
|
|
128
|
+
)
|
|
129
|
+
]);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const errorMessage = error?.message || "";
|
|
132
|
+
if (errorMessage.includes("Channel ended") || errorMessage.includes("Channel closed") || errorMessage.includes("no reply will be forthcoming")) {
|
|
133
|
+
logger.debug({ consumerTag }, "Consumer already cancelled (channel closed)");
|
|
134
|
+
} else {
|
|
135
|
+
logger.error({ error, consumerTag }, "Failed to cancel consumer");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
cancelPromises.push(cancelPromise);
|
|
140
|
+
}
|
|
141
|
+
await Promise.allSettled(cancelPromises);
|
|
142
|
+
}
|
|
143
|
+
this.activeConsumerTags.clear();
|
|
144
|
+
await super.disconnect();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export {
|
|
149
|
+
EventBusConsumer
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=chunk-5DMUVWFR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../lib/consumer.ts"],"sourcesContent":["import amqplib from 'amqplib'\nimport { BaseEventBusConnection, ConnectionState } from './base-connection.js'\nimport { logger } from '@a_jackie_z/logger'\n\n// Type definitions\nexport type EventHandler<T = any> = (data: T) => Promise<void>;\nexport type QueueHandlers = Map<string, EventHandler[]>;\nexport type ExchangeBindings = Map<string, string>; // Map<queueName, exchangeName>\n\n// Re-export ConnectionState for backward compatibility\nexport { ConnectionState } from './base-connection.js'\n\nexport interface EventBusConsumerOptions {\n rabbitMqUrl: string;\n queueHandlers: QueueHandlers;\n onStateChange?: (state: ConnectionState, reconnectCount?: number) => void;\n exchangeBindings?: ExchangeBindings;\n}\n\nexport class EventBusConsumer extends BaseEventBusConnection<amqplib.Channel> {\n private readonly queueHandlers: QueueHandlers;\n private readonly exchangeBindings?: ExchangeBindings;\n private activeConsumerTags: Set<string> = new Set();\n\n constructor(options: EventBusConsumerOptions) {\n super(options);\n this.queueHandlers = options.queueHandlers;\n if (options.exchangeBindings !== undefined) {\n this.exchangeBindings = options.exchangeBindings;\n }\n }\n\n protected async createChannelAndSetup(): Promise<void> {\n if (!this.channelModel) {\n throw new Error('Channel model is not initialized');\n }\n\n this.channel = await this.channelModel.createChannel();\n\n // Limit to 1 message processed at a time\n this.channel.prefetch(1);\n\n // Clear active consumer tags on reconnect\n this.activeConsumerTags.clear();\n\n // Assert queues and register handlers\n for (const [queueName, handlers] of this.queueHandlers.entries()) {\n // Check if this queue should be bound to an exchange (broadcast mode)\n const exchangeName = this.exchangeBindings?.get(queueName);\n\n if (exchangeName) {\n // Broadcast mode: assert exchange and create exclusive queue\n await this.channel.assertExchange(exchangeName, 'fanout', {\n durable: false, // Exchange doesn't need to persist (stateless routing)\n autoDelete: false // Exchange is not auto-deleted\n });\n\n // Create exclusive queue that gets deleted when consumer disconnects\n // This ensures each consumer instance receives all broadcast messages\n const { queue: exclusiveQueueName } = await this.channel.assertQueue('', {\n exclusive: true, // Queue is exclusive to this connection\n autoDelete: true // Queue is deleted when connection closes\n });\n\n // Bind the exclusive queue to the fanout exchange\n await this.channel.bindQueue(exclusiveQueueName, exchangeName, '');\n\n logger.info({ queueName, exchangeName, exclusiveQueueName }, 'Queue bound to exchange for broadcast');\n\n // Setup consumer for the exclusive queue\n const { consumerTag } = await this.channel.consume(exclusiveQueueName, async (msg) => {\n if (msg) {\n let successCount = 0;\n const content = msg.content.toString();\n\n try {\n const data = JSON.parse(content);\n\n // Execute handlers sequentially\n for (const handler of handlers) {\n try {\n await handler(data);\n successCount++;\n } catch (error) {\n logger.error({ error, queueName, exchangeName }, 'Handler failed for broadcast queue');\n }\n }\n\n // Acknowledge if at least one handler succeeded\n if (successCount > 0) {\n this.channel!.ack(msg);\n } else {\n // All handlers failed, nack without requeue\n logger.error({ queueName, exchangeName, handlerCount: handlers.length }, 'All handlers failed for broadcast queue');\n this.channel!.nack(msg, false, false);\n }\n } catch (error) {\n logger.error({ error, queueName, exchangeName }, 'Failed to parse message from broadcast queue');\n this.channel!.nack(msg, false, false);\n }\n }\n }, {\n noAck: false // Require acknowledgment after processing\n });\n\n this.activeConsumerTags.add(consumerTag);\n logger.info({ queueName, exchangeName, exclusiveQueueName, consumerTag }, 'Consumer registered for broadcast queue');\n } else {\n // Direct queue mode: traditional point-to-point messaging\n await this.channel.assertQueue(queueName, {\n durable: true, // Queue persists after restart\n exclusive: false, // Multiple consumers can connect\n autoDelete: false // Queue is not auto-deleted\n });\n\n // Setup consumer for this queue\n const { consumerTag } = await this.channel.consume(queueName, async (msg) => {\n if (msg) {\n let successCount = 0;\n const content = msg.content.toString();\n\n try {\n const data = JSON.parse(content);\n\n // Execute handlers sequentially\n for (const handler of handlers) {\n try {\n await handler(data);\n successCount++;\n } catch (error) {\n logger.error({ error, queueName }, 'Handler failed for queue');\n }\n }\n\n // Acknowledge if at least one handler succeeded\n if (successCount > 0) {\n this.channel!.ack(msg);\n } else {\n // All handlers failed, nack without requeue\n logger.error({ queueName, handlerCount: handlers.length }, 'All handlers failed for queue, message will not be requeued');\n this.channel!.nack(msg, false, false);\n }\n } catch (error) {\n logger.error({ error, queueName }, 'Failed to parse message from queue');\n this.channel!.nack(msg, false, false);\n }\n }\n }, {\n noAck: false // Require acknowledgment after processing\n });\n\n this.activeConsumerTags.add(consumerTag);\n logger.info({ queueName, consumerTag }, 'Consumer registered for queue');\n }\n }\n }\n\n override async disconnect() {\n logger.info('Disconnecting consumer');\n\n // Cancel all active consumers gracefully before base disconnect\n if (this.channel && this.activeConsumerTags.size > 0) {\n const cancelPromises: Promise<void>[] = [];\n\n for (const consumerTag of this.activeConsumerTags) {\n const cancelPromise = (async () => {\n try {\n logger.debug({ consumerTag }, 'Cancelling consumer');\n\n // Apply timeout only if channel operation can be initiated\n await Promise.race([\n this.channel!.cancel(consumerTag),\n new Promise<void>((_, reject) =>\n setTimeout(() => reject(new Error('Consumer cancellation timeout')), 5000)\n )\n ]);\n } catch (error: any) {\n // Gracefully handle channel-closed errors (expected during shutdown)\n const errorMessage = error?.message || '';\n if (errorMessage.includes('Channel ended') ||\n errorMessage.includes('Channel closed') ||\n errorMessage.includes('no reply will be forthcoming')) {\n logger.debug({ consumerTag }, 'Consumer already cancelled (channel closed)');\n } else {\n logger.error({ error, consumerTag }, 'Failed to cancel consumer');\n }\n }\n })();\n\n cancelPromises.push(cancelPromise);\n }\n\n // Wait for all cancellations to complete or fail\n await Promise.allSettled(cancelPromises);\n }\n\n this.activeConsumerTags.clear();\n\n // Call base class disconnect to close channel and connection\n await super.disconnect();\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// lib/base-connection.ts
|
|
2
|
+
import amqplib from "amqplib";
|
|
3
|
+
import { logger } from "@a_jackie_z/logger";
|
|
4
|
+
var ConnectionState = {
|
|
5
|
+
CONNECTED: "CONNECTED",
|
|
6
|
+
DISCONNECTED: "DISCONNECTED",
|
|
7
|
+
RECONNECTING: "RECONNECTING"
|
|
8
|
+
};
|
|
9
|
+
var BaseEventBusConnection = class {
|
|
10
|
+
channelModel = null;
|
|
11
|
+
channel = null;
|
|
12
|
+
rabbitMqUrl;
|
|
13
|
+
onStateChange;
|
|
14
|
+
reconnectTimer = null;
|
|
15
|
+
isReconnecting = false;
|
|
16
|
+
shouldReconnect = true;
|
|
17
|
+
reconnectCount = 0;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.rabbitMqUrl = options.rabbitMqUrl;
|
|
20
|
+
this.onStateChange = options.onStateChange;
|
|
21
|
+
}
|
|
22
|
+
async connect() {
|
|
23
|
+
try {
|
|
24
|
+
this.channelModel = await amqplib.connect(this.rabbitMqUrl);
|
|
25
|
+
this.channelModel.on("error", (error) => {
|
|
26
|
+
logger.error({ error }, "RabbitMQ connection error");
|
|
27
|
+
this.scheduleReconnect();
|
|
28
|
+
});
|
|
29
|
+
this.channelModel.on("close", () => {
|
|
30
|
+
logger.debug("RabbitMQ connection closed");
|
|
31
|
+
this.scheduleReconnect();
|
|
32
|
+
});
|
|
33
|
+
await this.createChannelAndSetup();
|
|
34
|
+
if (this.channel) {
|
|
35
|
+
this.channel.on("error", (error) => {
|
|
36
|
+
logger.error({ error }, "RabbitMQ channel error");
|
|
37
|
+
this.scheduleReconnect();
|
|
38
|
+
});
|
|
39
|
+
this.channel.on("close", () => {
|
|
40
|
+
logger.debug("RabbitMQ channel closed");
|
|
41
|
+
this.scheduleReconnect();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
this.reconnectCount = 0;
|
|
45
|
+
this.isReconnecting = false;
|
|
46
|
+
this.notifyStateChange(ConnectionState.CONNECTED);
|
|
47
|
+
logger.info({ state: ConnectionState.CONNECTED }, "RabbitMQ connection established successfully");
|
|
48
|
+
return this.channel;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error({ error }, "Failed to connect to RabbitMQ");
|
|
51
|
+
this.scheduleReconnect();
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
scheduleReconnect() {
|
|
56
|
+
if (!this.shouldReconnect || this.isReconnecting) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.isReconnecting = true;
|
|
60
|
+
if (this.reconnectTimer) {
|
|
61
|
+
clearTimeout(this.reconnectTimer);
|
|
62
|
+
}
|
|
63
|
+
this.reconnectCount++;
|
|
64
|
+
this.notifyStateChange(ConnectionState.RECONNECTING, this.reconnectCount);
|
|
65
|
+
logger.info({ reconnectCount: this.reconnectCount, state: ConnectionState.RECONNECTING }, "Reconnection attempt scheduled in 15 seconds");
|
|
66
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
67
|
+
await this.reconnect();
|
|
68
|
+
}, 15e3);
|
|
69
|
+
}
|
|
70
|
+
async reconnect() {
|
|
71
|
+
try {
|
|
72
|
+
logger.info({ reconnectCount: this.reconnectCount }, "Attempting to reconnect");
|
|
73
|
+
this.channel = null;
|
|
74
|
+
this.channelModel = null;
|
|
75
|
+
await this.connect();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logger.error({ error, reconnectCount: this.reconnectCount }, "Reconnection failed");
|
|
78
|
+
this.isReconnecting = false;
|
|
79
|
+
this.scheduleReconnect();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
notifyStateChange(state, reconnectCount) {
|
|
83
|
+
if (this.onStateChange) {
|
|
84
|
+
try {
|
|
85
|
+
this.onStateChange(state, reconnectCount);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error({ error }, "Error in state change callback");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async disconnect() {
|
|
92
|
+
logger.info({ state: ConnectionState.DISCONNECTED }, "Disconnecting from RabbitMQ");
|
|
93
|
+
this.shouldReconnect = false;
|
|
94
|
+
this.notifyStateChange(ConnectionState.DISCONNECTED);
|
|
95
|
+
if (this.reconnectTimer) {
|
|
96
|
+
clearTimeout(this.reconnectTimer);
|
|
97
|
+
this.reconnectTimer = null;
|
|
98
|
+
}
|
|
99
|
+
if (this.channel) {
|
|
100
|
+
try {
|
|
101
|
+
await this.channel.close();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const errorMessage = error?.message || "";
|
|
104
|
+
if (errorMessage.includes("Channel closed") || errorMessage.includes("IllegalOperationError")) {
|
|
105
|
+
logger.debug("Channel already closed");
|
|
106
|
+
} else {
|
|
107
|
+
logger.error({ error }, "Error closing channel");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (this.channelModel) {
|
|
112
|
+
try {
|
|
113
|
+
await this.channelModel.close();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const errorMessage = error?.message || "";
|
|
116
|
+
if (errorMessage.includes("Connection closed") || errorMessage.includes("Connection closing") || errorMessage.includes("IllegalOperationError")) {
|
|
117
|
+
logger.debug("Connection already closed or closing");
|
|
118
|
+
} else {
|
|
119
|
+
logger.error({ error }, "Error closing connection");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
logger.info("Disconnected from RabbitMQ");
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export {
|
|
128
|
+
ConnectionState,
|
|
129
|
+
BaseEventBusConnection
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=chunk-JBE5KWYZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../lib/base-connection.ts"],"sourcesContent":["import amqplib from 'amqplib'\nimport { logger } from '@a_jackie_z/logger'\n\nexport const ConnectionState = {\n CONNECTED: 'CONNECTED',\n DISCONNECTED: 'DISCONNECTED',\n RECONNECTING: 'RECONNECTING'\n} as const;\n\nexport type ConnectionState = typeof ConnectionState[keyof typeof ConnectionState];\n\nexport interface BaseEventBusConnectionOptions {\n rabbitMqUrl: string;\n onStateChange?: (state: ConnectionState, reconnectCount?: number) => void;\n}\n\nexport abstract class BaseEventBusConnection<T extends amqplib.Channel> {\n protected channelModel: amqplib.ChannelModel | null = null;\n protected channel: T | null = null;\n protected readonly rabbitMqUrl: string;\n protected readonly onStateChange: ((state: ConnectionState, reconnectCount?: number) => void) | undefined;\n protected reconnectTimer: NodeJS.Timeout | null = null;\n protected isReconnecting: boolean = false;\n protected shouldReconnect: boolean = true;\n protected reconnectCount: number = 0;\n\n constructor(options: BaseEventBusConnectionOptions) {\n this.rabbitMqUrl = options.rabbitMqUrl;\n this.onStateChange = options.onStateChange;\n }\n\n /**\n * Abstract method for creating and setting up the channel.\n * Subclasses must implement this to define their specific channel type and setup logic.\n */\n protected abstract createChannelAndSetup(): Promise<void>;\n\n async connect() {\n try {\n this.channelModel = await amqplib.connect(this.rabbitMqUrl);\n\n // Setup connection error handlers\n this.channelModel.on('error', (error) => {\n logger.error({ error }, 'RabbitMQ connection error');\n this.scheduleReconnect();\n });\n\n this.channelModel.on('close', () => {\n logger.debug('RabbitMQ connection closed');\n this.scheduleReconnect();\n });\n\n // Create and setup channel (subclass-specific)\n await this.createChannelAndSetup();\n\n // Setup channel error handlers\n if (this.channel) {\n this.channel.on('error', (error) => {\n logger.error({ error }, 'RabbitMQ channel error');\n this.scheduleReconnect();\n });\n\n this.channel.on('close', () => {\n logger.debug('RabbitMQ channel closed');\n this.scheduleReconnect();\n });\n }\n\n // Reset reconnect count on successful connection\n this.reconnectCount = 0;\n this.isReconnecting = false;\n\n // Notify state change\n this.notifyStateChange(ConnectionState.CONNECTED);\n\n logger.info({ state: ConnectionState.CONNECTED }, 'RabbitMQ connection established successfully');\n return this.channel;\n } catch (error) {\n logger.error({ error }, 'Failed to connect to RabbitMQ');\n this.scheduleReconnect();\n throw error;\n }\n }\n\n protected scheduleReconnect() {\n if (!this.shouldReconnect || this.isReconnecting) {\n return;\n }\n\n this.isReconnecting = true;\n\n // Clear existing timer\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n }\n\n this.reconnectCount++;\n\n // Notify state change\n this.notifyStateChange(ConnectionState.RECONNECTING, this.reconnectCount);\n\n logger.info({ reconnectCount: this.reconnectCount, state: ConnectionState.RECONNECTING }, 'Reconnection attempt scheduled in 15 seconds');\n\n this.reconnectTimer = setTimeout(async () => {\n await this.reconnect();\n }, 15000);\n }\n\n protected async reconnect() {\n try {\n logger.info({ reconnectCount: this.reconnectCount }, 'Attempting to reconnect');\n\n // Nullify existing connections\n this.channel = null;\n this.channelModel = null;\n\n // Attempt to reconnect\n await this.connect();\n } catch (error) {\n logger.error({ error, reconnectCount: this.reconnectCount }, 'Reconnection failed');\n this.isReconnecting = false;\n this.scheduleReconnect();\n }\n }\n\n protected notifyStateChange(state: ConnectionState, reconnectCount?: number) {\n if (this.onStateChange) {\n try {\n this.onStateChange(state, reconnectCount);\n } catch (error) {\n logger.error({ error }, 'Error in state change callback');\n }\n }\n }\n\n async disconnect() {\n logger.info({ state: ConnectionState.DISCONNECTED }, 'Disconnecting from RabbitMQ');\n\n this.shouldReconnect = false;\n\n // Notify state change\n this.notifyStateChange(ConnectionState.DISCONNECTED);\n\n // Clear reconnection timer\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n\n // Close channel and connection\n if (this.channel) {\n try {\n await this.channel.close();\n } catch (error: any) {\n // Ignore errors if channel is already closed\n const errorMessage = error?.message || '';\n if (errorMessage.includes('Channel closed') ||\n errorMessage.includes('IllegalOperationError')) {\n logger.debug('Channel already closed');\n } else {\n logger.error({ error }, 'Error closing channel');\n }\n }\n }\n\n if (this.channelModel) {\n try {\n await this.channelModel.close();\n } catch (error: any) {\n // Ignore errors if connection is already closed or closing\n const errorMessage = error?.message || '';\n if (errorMessage.includes('Connection closed') ||\n errorMessage.includes('Connection closing') ||\n errorMessage.includes('IllegalOperationError')) {\n logger.debug('Connection already closed or closing');\n } else {\n logger.error({ error }, 'Error closing connection');\n }\n }\n }\n\n logger.info('Disconnected from RabbitMQ');\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseEventBusConnection
|
|
3
|
+
} from "./chunk-JBE5KWYZ.js";
|
|
4
|
+
|
|
5
|
+
// lib/producer.ts
|
|
6
|
+
import { logger } from "@a_jackie_z/logger";
|
|
7
|
+
var EventBusProducer = class extends BaseEventBusConnection {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
super(options);
|
|
10
|
+
}
|
|
11
|
+
async createChannelAndSetup() {
|
|
12
|
+
if (!this.channelModel) {
|
|
13
|
+
throw new Error("Channel model is not initialized");
|
|
14
|
+
}
|
|
15
|
+
this.channel = await this.channelModel.createConfirmChannel();
|
|
16
|
+
logger.debug("RabbitMQ confirm channel created successfully");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Publish a message to a queue with persistence enabled.
|
|
20
|
+
* Throws immediately if channel is not available.
|
|
21
|
+
*
|
|
22
|
+
* @param queueName The name of the queue to publish to
|
|
23
|
+
* @param data The data to publish
|
|
24
|
+
* @throws Error if channel is not available or publish fails
|
|
25
|
+
*/
|
|
26
|
+
async publish(queueName, data) {
|
|
27
|
+
if (!this.channel) {
|
|
28
|
+
throw new Error("Channel is not available. Ensure connect() is called and connection is established.");
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await this.channel.assertQueue(queueName, {
|
|
32
|
+
durable: true,
|
|
33
|
+
// Queue persists after restart
|
|
34
|
+
exclusive: false,
|
|
35
|
+
// Multiple producers/consumers can connect
|
|
36
|
+
autoDelete: false
|
|
37
|
+
// Queue is not auto-deleted
|
|
38
|
+
});
|
|
39
|
+
const message = JSON.stringify(data);
|
|
40
|
+
this.channel.sendToQueue(queueName, Buffer.from(message), {
|
|
41
|
+
persistent: true
|
|
42
|
+
// Message survives broker restart
|
|
43
|
+
});
|
|
44
|
+
await this.channel.waitForConfirms();
|
|
45
|
+
logger.debug({ queueName, data }, "Message published to queue");
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error({ error, queueName }, "Failed to publish message to queue");
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Broadcast a message to all consumers listening on the exchange.
|
|
53
|
+
* Uses fanout exchange to deliver message to all bound queues.
|
|
54
|
+
* Throws immediately if channel is not available.
|
|
55
|
+
*
|
|
56
|
+
* @param exchangeName The name of the fanout exchange to broadcast to
|
|
57
|
+
* @param data The data to broadcast
|
|
58
|
+
* @throws Error if channel is not available or broadcast fails
|
|
59
|
+
*/
|
|
60
|
+
async broadcast(exchangeName, data) {
|
|
61
|
+
if (!this.channel) {
|
|
62
|
+
throw new Error("Channel is not available. Ensure connect() is called and connection is established.");
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await this.channel.assertExchange(exchangeName, "fanout", {
|
|
66
|
+
durable: false,
|
|
67
|
+
// Exchange doesn't need to persist (stateless routing)
|
|
68
|
+
autoDelete: false
|
|
69
|
+
// Exchange is not auto-deleted
|
|
70
|
+
});
|
|
71
|
+
const message = JSON.stringify(data);
|
|
72
|
+
this.channel.publish(exchangeName, "", Buffer.from(message), {
|
|
73
|
+
persistent: false
|
|
74
|
+
// Transient messages for broadcast (not persisted)
|
|
75
|
+
});
|
|
76
|
+
await this.channel.waitForConfirms();
|
|
77
|
+
logger.debug({ exchangeName, data }, "Message broadcast to exchange");
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.error({ error, exchangeName }, "Failed to broadcast message to exchange");
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
EventBusProducer
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=chunk-YP7YVSEN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../lib/producer.ts"],"sourcesContent":["import amqplib from 'amqplib'\nimport { BaseEventBusConnection, ConnectionState } from './base-connection.js'\nimport { logger } from '@a_jackie_z/logger'\n\n// Re-export ConnectionState for convenience\nexport { ConnectionState } from './base-connection.js'\n\nexport interface EventBusProducerOptions {\n rabbitMqUrl: string;\n onStateChange?: (state: ConnectionState, reconnectCount?: number) => void;\n}\n\nexport class EventBusProducer extends BaseEventBusConnection<amqplib.ConfirmChannel> {\n constructor(options: EventBusProducerOptions) {\n super(options);\n }\n\n protected async createChannelAndSetup(): Promise<void> {\n if (!this.channelModel) {\n throw new Error('Channel model is not initialized');\n }\n\n // Create confirm channel for reliable publishing\n this.channel = await this.channelModel.createConfirmChannel();\n logger.debug('RabbitMQ confirm channel created successfully');\n }\n\n /**\n * Publish a message to a queue with persistence enabled.\n * Throws immediately if channel is not available.\n *\n * @param queueName The name of the queue to publish to\n * @param data The data to publish\n * @throws Error if channel is not available or publish fails\n */\n async publish(queueName: string, data: any): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel is not available. Ensure connect() is called and connection is established.');\n }\n\n try {\n // Assert queue with durable option\n await this.channel.assertQueue(queueName, {\n durable: true, // Queue persists after restart\n exclusive: false, // Multiple producers/consumers can connect\n autoDelete: false // Queue is not auto-deleted\n });\n\n // Send message with persistent flag\n const message = JSON.stringify(data);\n this.channel.sendToQueue(queueName, Buffer.from(message), {\n persistent: true // Message survives broker restart\n });\n\n // Wait for broker confirmation\n await this.channel.waitForConfirms();\n\n logger.debug({ queueName, data }, 'Message published to queue');\n } catch (error) {\n logger.error({ error, queueName }, 'Failed to publish message to queue');\n throw error;\n }\n }\n\n /**\n * Broadcast a message to all consumers listening on the exchange.\n * Uses fanout exchange to deliver message to all bound queues.\n * Throws immediately if channel is not available.\n *\n * @param exchangeName The name of the fanout exchange to broadcast to\n * @param data The data to broadcast\n * @throws Error if channel is not available or broadcast fails\n */\n async broadcast(exchangeName: string, data: any): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel is not available. Ensure connect() is called and connection is established.');\n }\n\n try {\n // Assert fanout exchange\n await this.channel.assertExchange(exchangeName, 'fanout', {\n durable: false, // Exchange doesn't need to persist (stateless routing)\n autoDelete: false // Exchange is not auto-deleted\n });\n\n // Broadcast message to exchange (no routing key needed for fanout)\n const message = JSON.stringify(data);\n this.channel.publish(exchangeName, '', Buffer.from(message), {\n persistent: false // Transient messages for broadcast (not persisted)\n });\n\n // Wait for broker confirmation\n await this.channel.waitForConfirms();\n\n logger.debug({ exchangeName, data }, 'Message broadcast to exchange');\n } catch (error) {\n logger.error({ error, exchangeName }, 'Failed to broadcast message to exchange');\n throw error;\n }\n }\n}\n"],"mappings":";;;;;AAEA,SAAS,cAAc;AAUhB,IAAM,mBAAN,cAA+B,uBAA+C;AAAA,EACnF,YAAY,SAAkC;AAC5C,UAAM,OAAO;AAAA,EACf;AAAA,EAEA,MAAgB,wBAAuC;AACrD,QAAI,CAAC,KAAK,cAAc;AACtB,YAAM,IAAI,MAAM,kCAAkC;AAAA,IACpD;AAGA,SAAK,UAAU,MAAM,KAAK,aAAa,qBAAqB;AAC5D,WAAO,MAAM,+CAA+C;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,QAAQ,WAAmB,MAA0B;AACzD,QAAI,CAAC,KAAK,SAAS;AACjB,YAAM,IAAI,MAAM,qFAAqF;AAAA,IACvG;AAEA,QAAI;AAEF,YAAM,KAAK,QAAQ,YAAY,WAAW;AAAA,QACxC,SAAS;AAAA;AAAA,QACT,WAAW;AAAA;AAAA,QACX,YAAY;AAAA;AAAA,MACd,CAAC;AAGD,YAAM,UAAU,KAAK,UAAU,IAAI;AACnC,WAAK,QAAQ,YAAY,WAAW,OAAO,KAAK,OAAO,GAAG;AAAA,QACxD,YAAY;AAAA;AAAA,MACd,CAAC;AAGD,YAAM,KAAK,QAAQ,gBAAgB;AAEnC,aAAO,MAAM,EAAE,WAAW,KAAK,GAAG,4BAA4B;AAAA,IAChE,SAAS,OAAO;AACd,aAAO,MAAM,EAAE,OAAO,UAAU,GAAG,oCAAoC;AACvE,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,UAAU,cAAsB,MAA0B;AAC9D,QAAI,CAAC,KAAK,SAAS;AACjB,YAAM,IAAI,MAAM,qFAAqF;AAAA,IACvG;AAEA,QAAI;AAEF,YAAM,KAAK,QAAQ,eAAe,cAAc,UAAU;AAAA,QACxD,SAAS;AAAA;AAAA,QACT,YAAY;AAAA;AAAA,MACd,CAAC;AAGD,YAAM,UAAU,KAAK,UAAU,IAAI;AACnC,WAAK,QAAQ,QAAQ,cAAc,IAAI,OAAO,KAAK,OAAO,GAAG;AAAA,QAC3D,YAAY;AAAA;AAAA,MACd,CAAC;AAGD,YAAM,KAAK,QAAQ,gBAAgB;AAEnC,aAAO,MAAM,EAAE,cAAc,KAAK,GAAG,+BAA+B;AAAA,IACtE,SAAS,OAAO;AACd,aAAO,MAAM,EAAE,OAAO,aAAa,GAAG,yCAAyC;AAC/E,YAAM;AAAA,IACR;AAAA,EACF;AACF;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import amqplib from 'amqplib';
|
|
2
|
+
|
|
3
|
+
declare const ConnectionState: {
|
|
4
|
+
readonly CONNECTED: "CONNECTED";
|
|
5
|
+
readonly DISCONNECTED: "DISCONNECTED";
|
|
6
|
+
readonly RECONNECTING: "RECONNECTING";
|
|
7
|
+
};
|
|
8
|
+
type ConnectionState = typeof ConnectionState[keyof typeof ConnectionState];
|
|
9
|
+
interface BaseEventBusConnectionOptions {
|
|
10
|
+
rabbitMqUrl: string;
|
|
11
|
+
onStateChange?: (state: ConnectionState, reconnectCount?: number) => void;
|
|
12
|
+
}
|
|
13
|
+
declare abstract class BaseEventBusConnection<T extends amqplib.Channel> {
|
|
14
|
+
protected channelModel: amqplib.ChannelModel | null;
|
|
15
|
+
protected channel: T | null;
|
|
16
|
+
protected readonly rabbitMqUrl: string;
|
|
17
|
+
protected readonly onStateChange: ((state: ConnectionState, reconnectCount?: number) => void) | undefined;
|
|
18
|
+
protected reconnectTimer: NodeJS.Timeout | null;
|
|
19
|
+
protected isReconnecting: boolean;
|
|
20
|
+
protected shouldReconnect: boolean;
|
|
21
|
+
protected reconnectCount: number;
|
|
22
|
+
constructor(options: BaseEventBusConnectionOptions);
|
|
23
|
+
/**
|
|
24
|
+
* Abstract method for creating and setting up the channel.
|
|
25
|
+
* Subclasses must implement this to define their specific channel type and setup logic.
|
|
26
|
+
*/
|
|
27
|
+
protected abstract createChannelAndSetup(): Promise<void>;
|
|
28
|
+
connect(): Promise<T | null>;
|
|
29
|
+
protected scheduleReconnect(): void;
|
|
30
|
+
protected reconnect(): Promise<void>;
|
|
31
|
+
protected notifyStateChange(state: ConnectionState, reconnectCount?: number): void;
|
|
32
|
+
disconnect(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type EventHandler<T = any> = (data: T) => Promise<void>;
|
|
36
|
+
type QueueHandlers = Map<string, EventHandler[]>;
|
|
37
|
+
type ExchangeBindings = Map<string, string>;
|
|
38
|
+
|
|
39
|
+
interface EventBusConsumerOptions {
|
|
40
|
+
rabbitMqUrl: string;
|
|
41
|
+
queueHandlers: QueueHandlers;
|
|
42
|
+
onStateChange?: (state: ConnectionState, reconnectCount?: number) => void;
|
|
43
|
+
exchangeBindings?: ExchangeBindings;
|
|
44
|
+
}
|
|
45
|
+
declare class EventBusConsumer extends BaseEventBusConnection<amqplib.Channel> {
|
|
46
|
+
private readonly queueHandlers;
|
|
47
|
+
private readonly exchangeBindings?;
|
|
48
|
+
private activeConsumerTags;
|
|
49
|
+
constructor(options: EventBusConsumerOptions);
|
|
50
|
+
protected createChannelAndSetup(): Promise<void>;
|
|
51
|
+
disconnect(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface EventBusProducerOptions {
|
|
55
|
+
rabbitMqUrl: string;
|
|
56
|
+
onStateChange?: (state: ConnectionState, reconnectCount?: number) => void;
|
|
57
|
+
}
|
|
58
|
+
declare class EventBusProducer extends BaseEventBusConnection<amqplib.ConfirmChannel> {
|
|
59
|
+
constructor(options: EventBusProducerOptions);
|
|
60
|
+
protected createChannelAndSetup(): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Publish a message to a queue with persistence enabled.
|
|
63
|
+
* Throws immediately if channel is not available.
|
|
64
|
+
*
|
|
65
|
+
* @param queueName The name of the queue to publish to
|
|
66
|
+
* @param data The data to publish
|
|
67
|
+
* @throws Error if channel is not available or publish fails
|
|
68
|
+
*/
|
|
69
|
+
publish(queueName: string, data: any): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Broadcast a message to all consumers listening on the exchange.
|
|
72
|
+
* Uses fanout exchange to deliver message to all bound queues.
|
|
73
|
+
* Throws immediately if channel is not available.
|
|
74
|
+
*
|
|
75
|
+
* @param exchangeName The name of the fanout exchange to broadcast to
|
|
76
|
+
* @param data The data to broadcast
|
|
77
|
+
* @throws Error if channel is not available or broadcast fails
|
|
78
|
+
*/
|
|
79
|
+
broadcast(exchangeName: string, data: any): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { BaseEventBusConnection, type BaseEventBusConnectionOptions, ConnectionState, EventBusConsumer, type EventBusConsumerOptions, EventBusProducer, type EventBusProducerOptions, type EventHandler, type ExchangeBindings, type QueueHandlers };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EventBusConsumer
|
|
3
|
+
} from "./chunk-5DMUVWFR.js";
|
|
4
|
+
import {
|
|
5
|
+
EventBusProducer
|
|
6
|
+
} from "./chunk-YP7YVSEN.js";
|
|
7
|
+
import {
|
|
8
|
+
BaseEventBusConnection,
|
|
9
|
+
ConnectionState
|
|
10
|
+
} from "./chunk-JBE5KWYZ.js";
|
|
11
|
+
export {
|
|
12
|
+
BaseEventBusConnection,
|
|
13
|
+
ConnectionState,
|
|
14
|
+
EventBusConsumer,
|
|
15
|
+
EventBusProducer
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EventBusConsumer
|
|
3
|
+
} from "../../chunk-5DMUVWFR.js";
|
|
4
|
+
import "../../chunk-JBE5KWYZ.js";
|
|
5
|
+
|
|
6
|
+
// lib/tests/broadcast-consumer.ts
|
|
7
|
+
import { logger } from "@a_jackie_z/logger";
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
var isChildConsumer = process.env.IS_CHILD_CONSUMER === "true";
|
|
12
|
+
var consumerId = process.env.CONSUMER_ID || "1";
|
|
13
|
+
if (isChildConsumer) {
|
|
14
|
+
const notificationHandler = async (data) => {
|
|
15
|
+
logger.info({ data, consumerId }, `[Consumer ${consumerId}] Received broadcast notification`);
|
|
16
|
+
};
|
|
17
|
+
const alertHandler = async (data) => {
|
|
18
|
+
logger.info({ data, consumerId }, `[Consumer ${consumerId}] Received broadcast alert`);
|
|
19
|
+
};
|
|
20
|
+
const queueHandlers = /* @__PURE__ */ new Map([
|
|
21
|
+
["notifications", [notificationHandler]],
|
|
22
|
+
["alerts", [alertHandler]]
|
|
23
|
+
]);
|
|
24
|
+
const exchangeBindings = /* @__PURE__ */ new Map([
|
|
25
|
+
["notifications", "notification_broadcast"],
|
|
26
|
+
["alerts", "alert_broadcast"]
|
|
27
|
+
]);
|
|
28
|
+
const consumer = new EventBusConsumer({
|
|
29
|
+
rabbitMqUrl: "amqp://rabbitmq:12345678@192.168.2.151:5672",
|
|
30
|
+
queueHandlers,
|
|
31
|
+
onStateChange: (state, reconnectCount) => {
|
|
32
|
+
logger.info({ state, reconnectCount, consumerId }, `[Consumer ${consumerId}] State change`);
|
|
33
|
+
},
|
|
34
|
+
exchangeBindings
|
|
35
|
+
});
|
|
36
|
+
async function runConsumer() {
|
|
37
|
+
try {
|
|
38
|
+
logger.info({ consumerId }, `[Consumer ${consumerId}] Starting...`);
|
|
39
|
+
await consumer.connect();
|
|
40
|
+
logger.info({ consumerId }, `[Consumer ${consumerId}] Connected and listening for broadcasts`);
|
|
41
|
+
process.on("SIGINT", async () => {
|
|
42
|
+
logger.info({ consumerId }, `[Consumer ${consumerId}] Disconnecting...`);
|
|
43
|
+
await consumer.disconnect();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
});
|
|
46
|
+
process.on("SIGTERM", async () => {
|
|
47
|
+
logger.info({ consumerId }, `[Consumer ${consumerId}] Disconnecting...`);
|
|
48
|
+
await consumer.disconnect();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
logger.error({ error, consumerId }, `[Consumer ${consumerId}] Failed to start`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
runConsumer();
|
|
57
|
+
} else {
|
|
58
|
+
logger.info("=== Starting 2 Broadcast Consumers Demo ===");
|
|
59
|
+
logger.info("Press Ctrl+C to stop all consumers");
|
|
60
|
+
logger.info("");
|
|
61
|
+
const consumers = [];
|
|
62
|
+
const consumer1 = spawn(process.execPath, [__filename], {
|
|
63
|
+
env: {
|
|
64
|
+
...process.env,
|
|
65
|
+
IS_CHILD_CONSUMER: "true",
|
|
66
|
+
CONSUMER_ID: "1"
|
|
67
|
+
},
|
|
68
|
+
stdio: "inherit"
|
|
69
|
+
});
|
|
70
|
+
consumers.push(consumer1);
|
|
71
|
+
logger.info("\u2713 Consumer 1 launched");
|
|
72
|
+
const consumer2 = spawn(process.execPath, [__filename], {
|
|
73
|
+
env: {
|
|
74
|
+
...process.env,
|
|
75
|
+
IS_CHILD_CONSUMER: "true",
|
|
76
|
+
CONSUMER_ID: "2"
|
|
77
|
+
},
|
|
78
|
+
stdio: "inherit"
|
|
79
|
+
});
|
|
80
|
+
consumers.push(consumer2);
|
|
81
|
+
logger.info("\u2713 Consumer 2 launched");
|
|
82
|
+
logger.info("");
|
|
83
|
+
logger.info("Both consumers are now listening for broadcast messages...");
|
|
84
|
+
process.on("SIGINT", () => {
|
|
85
|
+
logger.info("");
|
|
86
|
+
logger.info("Stopping all consumers...");
|
|
87
|
+
consumers.forEach((c) => c.kill("SIGINT"));
|
|
88
|
+
setTimeout(() => process.exit(0), 2e3);
|
|
89
|
+
});
|
|
90
|
+
process.on("SIGTERM", () => {
|
|
91
|
+
logger.info("Stopping all consumers...");
|
|
92
|
+
consumers.forEach((c) => c.kill("SIGTERM"));
|
|
93
|
+
setTimeout(() => process.exit(0), 2e3);
|
|
94
|
+
});
|
|
95
|
+
consumer1.on("exit", (code) => {
|
|
96
|
+
logger.info({ code }, "Consumer 1 exited");
|
|
97
|
+
if (code !== 0) {
|
|
98
|
+
logger.error("Consumer 1 exited with error, stopping all...");
|
|
99
|
+
consumers.forEach((c) => c.kill());
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
consumer2.on("exit", (code) => {
|
|
104
|
+
logger.info({ code }, "Consumer 2 exited");
|
|
105
|
+
if (code !== 0) {
|
|
106
|
+
logger.error("Consumer 2 exited with error, stopping all...");
|
|
107
|
+
consumers.forEach((c) => c.kill());
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=broadcast-consumer.js.map
|