UNPKG

@bitrix24/b24rabbitmq

Version:

Library for integrating Bitrix24 applications with RabbitMQ

297 lines (290 loc) 8.7 kB
/** * @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