UNPKG

@team-supercharge/nest-amqp

Version:
244 lines (243 loc) 11.5 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var AMQPService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.AMQPService = void 0; const common_1 = require("@nestjs/common"); const events_1 = require("events"); const rhea_promise_1 = require("rhea-promise"); const url_1 = require("url"); const util_1 = require("../../util"); const constant_1 = require("../../constant"); const exception_1 = require("../../exception"); /** * Can create a single connection and manage the senders and receivers for it. * * @public */ let AMQPService = AMQPService_1 = class AMQPService { /** * Parses the connection URI and connect to the message broker by the given * information. * * NOTE: If the connection closes and there was no error then the service will * attempt to reconnect to the message broker but only once. * * ```ts * const connection = await AMQPService.createConnection({ connectionUri: 'amqp://user:password@localhost:5672' }); * ``` * * @param {QueueModuleOptions} options Options for the module. * @param {string} [connectionToken] Name of the connection that is created * * @return {Connection} The created `rhea-promise` Connection. * @static */ static async createConnection(options, connectionToken = constant_1.AMQP_DEFAULT_CONNECTION_TOKEN) { if (Object.prototype.toString.call(options) !== '[object Object]') { throw new Error('AMQPModule connection options must an object'); } logger.log('creating AMQP client'); logger.log(`connection options: ${JSON.stringify(options)}, connection name: ${connectionToken}`); const { throwExceptionOnConnectionError, connectionUri, connectionOptions: rheaConnectionOptions } = options; const parsedConnectionUri = new url_1.URL(connectionUri); const { protocol, hostname, port } = parsedConnectionUri; const username = decodeURIComponent(parsedConnectionUri.username); const password = decodeURIComponent(parsedConnectionUri.password); logger.log(`initializing client connection to ${JSON.stringify({ protocol, username, password: '*****', hostname, port, })}`); const transport = this.getTransport(protocol); const connection = new rhea_promise_1.Connection(Object.assign({ password, username, transport, host: hostname, port: Number.parseInt(port, 10) }, rheaConnectionOptions)); connection.on(rhea_promise_1.ConnectionEvents.connectionOpen, (_) => { logger.log('connection opened'); }); connection.on(rhea_promise_1.ConnectionEvents.connectionClose, (context) => { if (!context.error) { logger.log('connection closed'); return; } // If there was an error, try to reconnect in 1 second logger.error(`connection closed with error: ${context.error.name} - ${context.error.message}`, context.error.stack); const timeoutHandler = setTimeout(async () => { context.connection._connection.dispatch(rhea_promise_1.ConnectionEvents.disconnected, void 0); await context.connection .open() .then(() => { logger.log('connection successfully reopened'); const emitted = AMQPService_1.eventEmitter.emit(constant_1.AMQP_CONNECTION_RECONNECT); // istanbul ignore next: mocking out the event emitter is unnecessary if (!emitted) { logger.warn('reconnect event not emitted'); } }) .catch(error => { logger.error(`reopening connection failed with error: ${error.message}`, error); }); clearTimeout(timeoutHandler); }, 1000); }); connection.on(rhea_promise_1.ConnectionEvents.connectionError, (context) => { logger.error(`connection errored: ${context.error.message}`, context.error.stack); }); connection.on(rhea_promise_1.ConnectionEvents.disconnected, (context) => { var _a, _b, _c, _d; const error = (_c = (_a = context === null || context === void 0 ? void 0 : context.error) !== null && _a !== void 0 ? _a : (_b = context === null || context === void 0 ? void 0 : context._context) === null || _b === void 0 ? void 0 : _b.error) !== null && _c !== void 0 ? _c : null; // istanbul ignore next logger.warn(`connection closed by peer: ${(_d = error === null || error === void 0 ? void 0 : error.message) !== null && _d !== void 0 ? _d : ''}`, error === null || error === void 0 ? void 0 : error.stack); }); try { await connection.open(); } catch (error) { logger.error(`connection error: ${error.message}`, error.stack); if (throwExceptionOnConnectionError === true) { throw error; } } logger.log('created AMQP connection'); util_1.AMQConnectionStorage.add(connectionToken, connection); return connection; } /** * Returns the connection object with which the AMQP connection was created. * * @return {AMQPConnectionOptions} Connection options. */ getConnectionOptions(connection = constant_1.AMQP_DEFAULT_CONNECTION_TOKEN) { return Object.assign({}, util_1.AMQConnectionOptionsStorage.get(connection)); } /** * Closes all the created connections. */ async disconnect() { const connections = util_1.AMQConnectionStorage.getConnectionNames(); for (const connectionName of connections) { // disconnect queue const connection = util_1.AMQConnectionStorage.get(connectionName); await connection.close(); } logger.log('queue disconnected'); } /** * Creates a sender object which will send the message to the given queue. * * @param {string} queue Name of the queue. * @param {string} [connectionName] Name of the connection the sender will use * * @return {AwaitableSender} Sender. */ async createSender(queue, connectionName = constant_1.AMQP_DEFAULT_CONNECTION_TOKEN) { const connection = util_1.AMQConnectionStorage.get(connectionName); if (!connection) { throw new Error(`No connection found for name ${connectionName}`); } const sender = await connection.createAwaitableSender({ target: queue }); sender.on(rhea_promise_1.SenderEvents.senderOpen, (context) => { logger.log(`sender for ${context.sender.address} opened`); }); sender.on(rhea_promise_1.SenderEvents.senderClose, (context) => { logger.log(`sender for ${context.sender.address} closed`); }); sender.on(rhea_promise_1.SenderEvents.senderError, (context) => { logger.error(`sender errored: ${JSON.stringify({ name: context.sender.address, error: context.sender.error, })}`); }); sender.on(rhea_promise_1.SenderEvents.senderDraining, (context) => { logger.log(`sender for ${context.sender.address} requested to drain its credits by remote peer`); }); return sender; } /** * Creates a receiver object which will send the message to the given queue. * * @param {string} source Name of the queue. * @param {number} credits How many message can be processed parallel. * @param {function(context: EventContext): Promise<void>} onMessage Function what will be invoked when a message arrives. * @param {string} [connectionToken] Name of the connection the receiver is on * * @return {Receiver} Receiver. */ async createReceiver(source, credits, onMessage, connectionToken = constant_1.AMQP_DEFAULT_CONNECTION_TOKEN) { const onError = (context) => { logger.error(`receiver for ${context.receiver.address} errored: ${JSON.stringify({ error: context.receiver.error, })}`); }; const connection = util_1.AMQConnectionStorage.get(connectionToken); if (!connection) { throw new Error(`No connection found for name ${connectionToken}`); } const receiver = await connection.createReceiver({ onError, onMessage, source, autoaccept: false, credit_window: 0, }); receiver.addCredit(credits); receiver.on(rhea_promise_1.ReceiverEvents.receiverOpen, (context) => { logger.log(`receiver of ${context.receiver.address} opened`); const currentCredits = context.receiver.credit; // istanbul ignore next if (currentCredits < credits) { logger.debug('receiver does not have credits, adding credits'); context.receiver.addCredit(credits - currentCredits); } }); receiver.on(rhea_promise_1.ReceiverEvents.receiverClose, (context) => { logger.log(`receiver of ${context.receiver.address} closed`); }); receiver.on(rhea_promise_1.ReceiverEvents.receiverDrained, (context) => { logger.debug(`remote peer for receiver of ${context.receiver.address} drained`); }); receiver.on(rhea_promise_1.ReceiverEvents.receiverFlow, (context) => { logger.debug(`flow event received for receiver of ${context.receiver.address}`); }); receiver.on(rhea_promise_1.ReceiverEvents.settled, (context) => { logger.debug(`message has been settled by remote for receiver of ${context.receiver.address}`); }); logger.log(`receiver created: ${JSON.stringify({ credits: receiver.credit, source: receiver.source, })}`); return receiver; } // istanbul ignore next static getTransport(protocol) { switch (protocol) { case 'amqp:': return 'tcp'; case 'amqps:': return 'ssl'; case 'amqp+ssl:': return 'ssl'; case 'amqp+tls:': return 'tls'; default: throw new exception_1.NestAmqpInvalidConnectionProtocolException(`Not supported connection protocol: ${protocol}`); } } }; /** * Event emitter for AMQP to show what is happening with the created connection. */ AMQPService.eventEmitter = new events_1.EventEmitter(); AMQPService = AMQPService_1 = __decorate([ common_1.Injectable() ], AMQPService); exports.AMQPService = AMQPService; const logger = new util_1.Logger(util_1.getLoggerContext(AMQPService.name));