UNPKG

@21jumpclick/service-messenger

Version:

Amqp lib to send and receive messages from different applications

256 lines (241 loc) 6.99 kB
import {Channel, connect, Connection, Replies} from 'amqplib' import {randomBytes} from 'crypto' import {BrokerConfig, Callback, MessageOptions} from '../types' const appId = '@21jumpclick/service-messenger' const allowedAppIds = [ appId, '@vicgrk/messenger' ] export class Broker { private rabbitMQ!: Connection private channel!: Channel private broadcastQueue!: Replies.AssertQueue private closing = false constructor(private opts: BrokerConfig, private exchange: string) { } async connect() { this.rabbitMQ = await connect( typeof this.opts === 'string' ? this.opts : `amqp://${this.opts.user || 'guest'}:${this.opts.password || 'guest'}@${this.opts.host || 'localhost' }:${this.opts.port || 5672}` ) this.channel = await this.rabbitMQ.createChannel() await this.channel.prefetch(10) await this.assertExchange(this.exchange) await this.assertBroadcast() this.closing = false } async publish(key: string, data: unknown, options?: MessageOptions) { if (this.closing) { return } if (!this.channel) { await this.connect() } await this.send(key, data, options) } async listen<T extends {pattern: string}>(cb: Callback<{ key: string; args: any, opts: { origin: string } }>) { if (this.closing) { return } if (!this.channel) { await this.connect() } this.channel.consume(this.broadcastQueue.queue, async (msg) => { if (!msg) { return } this.channel.ack(msg) let opts = <{ origin: string }>msg.properties.headers opts ||= {origin: 'UNKNOWN'} const args: T = JSON.parse(msg.content.toString()) const key = args.pattern try { cb({key, args, opts}) } catch (error) { console.error(error) } }) this.channel.consume(this.exchange, async (msg) => { if (!msg) { return } this.channel.ack(msg) let tmp: T | { data: T } = JSON.parse(msg.content.toString()) const key = (tmp as T).pattern if (allowedAppIds.includes(msg.properties.appId) && typeof (<{ data: T }>tmp).data !== 'undefined') { tmp = (<{ data: T }>tmp).data } const args = <T>tmp let opts = <{ origin: string }>msg.properties.headers opts ||= {origin: 'UNKNOWN'} if (!msg.properties.correlationId) { try { cb({key, args, opts}) } catch (error) { console.error(error) } } else { try { const data = await cb({key, args, opts}) this.channel.sendToQueue( msg.properties.replyTo, Buffer.from(JSON.stringify({data: {error: null, data}})), { correlationId: msg.properties.correlationId, headers: { origin: this.exchange, } } ) } catch (error) { this.channel.sendToQueue( msg.properties.replyTo, Buffer.from(JSON.stringify({data: {error}})), { correlationId: msg.properties.correlationId, headers: { origin: this.exchange, } } ) } } }) } async broadcast(key: string, data: unknown) { if (this.closing) { return } if (!this.channel) { await this.connect() } const [exchange] = key.split('.') await this.channel.assertExchange(`${exchange}-broadcast`, 'fanout', {durable: true}) const buffer = Buffer.from(JSON.stringify({data})) this.channel.publish(`${exchange}-broadcast`, key, buffer, { appId, headers: {origin: this.exchange} }) } async invoke<T>(key: string, data: unknown, options?: MessageOptions) { if (this.closing) { return } if (!this.channel) { await this.connect() } const correlationId = this.generateCorrelationId() const {queue} = await this.channel.assertQueue('', { exclusive: true, autoDelete: true, durable: false, }) let opts: any = { ...options, appId, correlationId, replyTo: queue, headers: { ...options?.headers, origin: this.exchange } } return new Promise<T>((resolve, reject) => { this.channel.consume(queue, (msg) => { if (!msg) { return } this.channel.ack(msg) this.channel.deleteQueue(queue) if (msg.properties.correlationId == correlationId) { const response = JSON.parse(msg.content.toString()).data as { error: Error | null data: T } if (response.error) { return reject(response.error) } resolve(response.data) } }) this.send(key, data, opts) }) } private async send(key: string, data: unknown, options?: MessageOptions) { const [exchange] = key.split('.') await this.assertExchange(exchange) let opts: any = { ...options, headers: { ...options?.headers, // 'x-deduplication-header': options?.deduplicationFieldPath, origin: this.exchange }, appId, } // if (options?.deduplicationFieldPath) { // opts['x-deduplication-header'] = valueFromPath(data, options.deduplicationFieldPath) // } const pattern = key.replace(`${exchange}.`, '') const buffer = Buffer.from(JSON.stringify({data, pattern})) this.channel.publish( exchange, key, buffer, opts ) } async close() { try { this.closing = false await this.channel.close() return await this.rabbitMQ.close() } catch (error) { return } } private async assertExchange(exchange: string) { await this.channel.assertExchange(exchange, (<any>this.opts)?.exhangeType || 'fanout', <any>{ durable: true, arguments: { 'x-cache-size': 128, 'x-cache-ttl': 1e3, 'x-cache-persistence': 'memory' } } ) await this.channel.assertQueue(exchange, { durable: true, }) await this.channel.bindQueue( exchange, exchange, `${exchange}.*` ) return } private async assertBroadcast() { await this.channel.assertExchange(`${this.exchange}-broadcast`, 'fanout', {durable: true}) this.broadcastQueue = await this.channel.assertQueue('', { durable: false, exclusive: true, }) await this.channel.bindQueue( this.broadcastQueue.queue, `${this.exchange}-broadcast`, '*' ) } generateCorrelationId() { return randomBytes(4).toString('hex') } }