@team-supercharge/nest-amqp
Version:
AMQP 1.0 module for Nest framework
244 lines (243 loc) • 11.5 kB
JavaScript
;
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));