@bitrix24/b24rabbitmq
Version:
Library for integrating Bitrix24 applications with RabbitMQ
297 lines (290 loc) • 8.7 kB
JavaScript
/**
* @version @bitrix24/b24rabbitmq v0.0.4
* @copyright (c) 2025 Bitrix24
* @licence MIT
* @links https://github.com/bitrix24/b24rabbitmq - GitHub
* @links https://github.com/bitrix24/b24rabbitmq/blob/main/docs/en/1_page.md - Documentation
*/
import amqp from 'amqplib';
class RabbitMQBase {
connection;
channel;
config;
constructor(config) {
this.config = {
channel: { prefetchCount: 1 },
...config
};
}
async connect() {
throw new Error("Need override this function");
}
/**
* Initialize all exchanges from the config
* @protected
*/
async setupExchanges() {
for (const exchange of this.config.exchanges) {
await this.registerExchange(exchange);
}
}
async registerExchange(exchange) {
await this.channel.assertExchange(
exchange.name,
exchange.type,
exchange.options || {}
);
}
/**
* Initialize queues from config
* @protected
*/
async setupQueues() {
for (const queue of this.config.queues) {
await this.registerQueue(queue);
}
}
async registerQueue(queue) {
let assertsOptions = {
maxPriority: queue.maxPriority ?? 10
};
if (assertsOptions.maxPriority) {
assertsOptions = {
arguments: {
"x-max-priority": assertsOptions.maxPriority
},
...assertsOptions
};
}
if (queue.deadLetter) {
assertsOptions = {
arguments: {
"x-dead-letter-exchange": queue.deadLetter.exchange,
"x-dead-letter-routing-key": queue.deadLetter.routingKey || ""
},
...assertsOptions
};
}
const q = await this.channel.assertQueue(
queue.name || "",
{
...assertsOptions,
...queue.options
}
);
for (const binding of queue.bindings) {
if (binding.headers) {
await this.channel.bindQueue(
q.queue,
binding.exchange,
binding.routingKey || "",
binding.headers
);
} else {
await this.channel.bindQueue(
q.queue,
binding.exchange,
binding.routingKey || ""
);
}
}
return q;
}
async disconnect() {
await this.channel?.close();
await this.connection?.close();
console.log("[RabbitMQ::Base] disconnect");
}
}
class RabbitMQProducer extends RabbitMQBase {
exchanges = /* @__PURE__ */ new Map();
async initialize() {
await this.connect();
await this.setupExchanges();
}
async connect() {
try {
console.log("[RabbitMQ::Producer] connect ...");
this.connection = await amqp.connect(this.config.connection.url);
this.channel = await this.connection.createChannel();
await this.channel.prefetch(this.config.channel.prefetchCount);
console.log("[RabbitMQ::Producer] connected successfully");
} catch (error) {
const problem = error instanceof Error ? error : new Error(`[RabbitMQ::Producer] connected error`, { cause: error });
console.error(problem);
throw problem;
}
}
async registerExchange(exchange) {
await super.registerExchange(exchange);
this.exchanges.set(exchange.name, exchange);
}
async publish(exchangeName, routingKey, message, options = {}) {
return this.channel.publish(
exchangeName,
routingKey,
Buffer.from(JSON.stringify(message)),
{
priority: 5,
...options
}
);
}
}
class RabbitMQConsumer extends RabbitMQBase {
retries = 0;
handlers = /* @__PURE__ */ new Map();
async initialize() {
await this.connect();
await this.setupExchanges();
await this.setupQueues();
}
async connect() {
try {
console.log("[RabbitMQ::Consumer] connect ...");
this.connection = await amqp.connect(this.config.connection.url);
this.channel = await this.connection.createChannel();
await this.channel.prefetch(this.config.channel.prefetchCount);
this.connection.on(
"close",
() => this.handleReconnect()
);
console.log("[RabbitMQ::Consumer] connected successfully");
this.retries = 0;
} catch (error) {
const problem = error instanceof Error ? error : new Error(`[RabbitMQ::Consumer] connected error`, { cause: error });
console.error(problem);
if (problem.message.includes("ENOTFOUND") || problem.message.includes("ECONNREFUSED")) {
throw problem;
}
this.handleReconnect();
}
}
handleReconnect() {
if (this.retries >= (this.config.connection.maxRetries || 5)) {
throw new Error("[RabbitMQ::Consumer] Max connection retries exceeded");
}
setTimeout(() => {
this.retries++;
console.log(`[RabbitMQ::Consumer] reconnecting attempt ${this.retries}`);
this.connect();
}, this.config.connection.reconnectInterval || 5e3);
}
registerHandler(queueName, handler) {
this.handlers.set(queueName, handler);
}
unRegisterHandler(queueName) {
if (this.handlers.has(queueName)) {
this.handlers.delete(queueName);
}
}
async consume(queueName) {
return this.channel.consume(
queueName,
async (msg) => {
if (!msg) return;
const handler = this.handlers.get(queueName);
if (handler) {
try {
const content = JSON.parse(msg.content.toString());
await handler(
content,
() => this.channel.ack(msg),
() => this.channel.nack(msg, false, false)
);
} catch (error) {
console.error(`[RabbitMQ] Error processing message: ${error instanceof Error ? error.message : error}`);
this.channel.nack(msg, false, false);
}
}
}
);
}
}
const byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 256).toString(16).slice(1));
}
function sfc32(a, b, c, d) {
return () => {
a |= 0;
b |= 0;
c |= 0;
d |= 0;
const t = (a + b | 0) + d | 0;
d = d + 1 | 0;
a = b ^ b >>> 9;
b = c + (c << 3) | 0;
c = (c << 21 | c >>> 11) + t | 0;
return t >>> 0;
};
}
function uuidv7() {
const bytes = new Uint8Array(16);
const timestamp = BigInt(Date.now());
const perf = BigInt(Math.floor(performance.now() * 1e3) % 65535);
const combinedTime = timestamp << 16n | perf;
bytes[0] = Number(combinedTime >> 40n & 0xffn);
bytes[1] = Number(combinedTime >> 32n & 0xffn);
bytes[2] = Number(combinedTime >> 24n & 0xffn);
bytes[3] = Number(combinedTime >> 16n & 0xffn);
bytes[4] = Number(combinedTime >> 8n & 0xffn);
bytes[5] = Number(combinedTime & 0xffn);
const seed = (Math.random() * 4294967295 ^ Date.now() ^ performance.now()) >>> 0;
const rand = sfc32(2654435769, 608135816, 3084996962, seed);
const randView = new DataView(bytes.buffer);
randView.setUint32(6, rand());
randView.setUint32(10, rand());
randView.setUint16(14, rand());
bytes[6] = 112 | bytes[6] & 15;
bytes[8] = 128 | bytes[8] & 63;
return (byteToHex[bytes[0]] + byteToHex[bytes[1]] + byteToHex[bytes[2]] + byteToHex[bytes[3]] + "-" + byteToHex[bytes[4]] + byteToHex[bytes[5]] + "-" + byteToHex[bytes[6]] + byteToHex[bytes[7]] + "-" + byteToHex[bytes[8]] + byteToHex[bytes[9]] + "-" + byteToHex[bytes[10]] + byteToHex[bytes[11]] + byteToHex[bytes[12]] + byteToHex[bytes[13]] + byteToHex[bytes[14]] + byteToHex[bytes[15]]).toLowerCase();
}
class RabbitRPC {
constructor(producer, consumer) {
this.producer = producer;
this.consumer = consumer;
}
async call(exchange, routingKey, payload, timeout = 5e3) {
const correlationId = uuidv7();
const queueName = `rps-${correlationId}`;
const replyQueue = await this.consumer.registerQueue({
name: queueName,
bindings: [
{
exchange: "amq.direct",
routingKey: queueName
}
],
options: { exclusive: true }
});
return new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error("[RabbitRPC] timeout")),
timeout
);
const handler = async (msg, ack) => {
if (msg.correlationId === correlationId) {
if (timer) {
clearTimeout(timer);
}
this.consumer.unRegisterHandler(replyQueue.queue);
ack();
resolve(msg);
}
};
this.consumer.registerHandler(replyQueue.queue, handler);
this.producer.publish(
exchange,
routingKey,
payload,
{
correlationId,
replyTo: replyQueue.queue
}
);
});
}
}
export { RabbitMQBase, RabbitMQConsumer, RabbitMQProducer, RabbitRPC };
//# sourceMappingURL=index.mjs.map