@team-supercharge/nest-amqp
Version:
AMQP 1.0 module for Nest framework
342 lines (341 loc) • 17.6 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 __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueueService = void 0;
const common_1 = require("@nestjs/common");
const class_validator_1 = require("class-validator");
const util_1 = require("../../util");
const domain_1 = require("../../domain");
const enum_1 = require("../../enum");
const constant_1 = require("../../constant");
const amqp_service_1 = require("../amqp/amqp.service");
const object_validator_service_1 = require("../object-validator/object-validator.service");
const PARALLEL_MESSAGE_COUNT = 1;
const toString = Object.prototype.toString;
/**
* Handles queue receivers and senders for the created connection.
*/
let QueueService = class QueueService {
constructor(amqpService, objectValidatorService) {
this.amqpService = amqpService;
this.objectValidatorService = objectValidatorService;
// this means only one sender and receiver / app / queue
this.receivers = new Map();
this.senders = new Map();
}
/**
* Creates a receiver which will listen to message on the given queue. The
* callback function will invoked with the body and the message control
* objects when a new message arrives on the queue. If a receiver is already
* created for the given queue then a new receiver won't be created.
*
* @param {string} source Name or Source object of the queue.
* @param {function(body: T, control: MessageControl, metadata: Omit<Message, 'body'>) => Promise<void>} callback Function what will invoked when message arrives.
* @param {ListenOptions<T>} options Options for message processing.
* @param {string} connection Name of the connection
*
* @public
*/
async listen(source, callback, options, connection = constant_1.AMQP_DEFAULT_CONNECTION_TOKEN) {
var _a, _b, _c;
const sourceToken = typeof source === 'string' ? source : source.address;
// get receiver
const initialCredit = (_a = options === null || options === void 0 ? void 0 : options.parallelMessageProcessing) !== null && _a !== void 0 ? _a : PARALLEL_MESSAGE_COUNT;
const transformerOptions = (_b = options === null || options === void 0 ? void 0 : options.transformerOptions) !== null && _b !== void 0 ? _b : {};
const validatorOptions = (_c = options === null || options === void 0 ? void 0 : options.validatorOptions) !== null && _c !== void 0 ? _c : null;
const messageValidator = async (context, control) => {
var _a;
logger.verbose(`incoming message on queue '${sourceToken}'`);
const messageBody = context.message.body;
const metadata = util_1.extendObject(context.message, { body: undefined });
let body;
// if not expecting parsed data
if (!options || !class_validator_1.isDefined(options.type)) {
body = null;
}
else {
// if expecting parsed data
let parsed;
// parse body received as string from queue
try {
parsed = this.decodeMessage(messageBody);
}
catch (error) {
logger.error('cant decode message', messageBody);
// can't decode, need to reject message
control.reject(error.message);
return;
}
try {
// HACK - change for better solution, when available
// Explanation: Class-transformer supports differentiating on type and using different classes, but currently the discriminator can only be
// inside the nested object. This extra property will be deleted during transformation
// istanbul ignore next
if (class_validator_1.isDefined(parsed === null || parsed === void 0 ? void 0 : parsed.type) && class_validator_1.isDefined(parsed === null || parsed === void 0 ? void 0 : parsed.payload)) {
parsed.payload.type = parsed.type;
}
body =
options && (options.noValidate || options.skipValidation)
? parsed
: await this.objectValidatorService.validate(options.type, parsed, { transformerOptions, validatorOptions });
}
catch (error) {
if (error instanceof util_1.ValidationNullObjectException) {
logger.error(`null received as body on ${context.receiver.address}`);
const acceptValidationNullObjectException = (_a = options.acceptValidationNullObjectException) !== null && _a !== void 0 ? _a : false;
if (acceptValidationNullObjectException === true) {
control.accept();
}
else {
control.reject(error.message);
}
return;
}
// istanbul ignore else
if (error instanceof util_1.ValidationException) {
logger.error(`validation error ${sourceToken} (payload: ${JSON.stringify(parsed)}): ${error.message}`, error.stack);
}
else {
const parsedError = util_1.tryParseJSON(error.message) || error.message;
logger.error(`unexpected error happened during validation process on '${sourceToken}' (payload: ${JSON.stringify(parsed)}): ${parsedError.toString()}`, error.stack);
}
// can't validate, need to reject message
control.reject(error.message);
return;
}
}
try {
// run callback function
const startTime = new Date();
await callback(body, control, metadata);
const durationInMs = new Date().getTime() - startTime.getTime();
logger.log(`handling '${sourceToken}' finished in ${durationInMs} (ms)`);
// handle auto-accept when message is otherwise not handled
// istanbul ignore next
if (!control.isHandled()) {
control.accept();
}
}
catch (error) {
logger.error(`error in callback on queue '${sourceToken}': ${error.message}`, error.stack);
// can't process callback, need to reject message
control.reject(error.message);
}
logger.verbose(`handled message on queue '${sourceToken}'`);
};
const messageHandler = async (context) => {
const control = new domain_1.MessageControl(context);
messageValidator(context, control).catch(error => {
logger.error(`unexpected error happened during message validation on '${context.receiver.address}': ${error.message}`, error);
control.reject(error.message);
});
};
await this.getReceiver(source, initialCredit, messageHandler, connection);
}
async send(target, message, sendOptions, connectionName) {
const connection = connectionName !== null && connectionName !== void 0 ? connectionName : (typeof sendOptions === 'string' ? sendOptions : constant_1.AMQP_DEFAULT_CONNECTION_TOKEN);
const options = toString.call(sendOptions) === '[object Object]' ? sendOptions : {};
// get sender
const sender = await this.getSender(target, connection);
const { schedule } = options, baseOptions = __rest(options, ["schedule"]);
// TODO: refactor messageToSend creation using state object or state switch
let messageToSend;
// scheduling
if (class_validator_1.isDefined(schedule === null || schedule === void 0 ? void 0 : schedule.cron)) {
// when using CRON syntax, simply add it to the message
// NOD: not possible to use seconds
messageToSend = {
body: this.encodeMessage(message),
message_annotations: {
'x-opt-delivery-cron': schedule.cron,
},
};
}
else if (schedule === null || schedule === void 0 ? void 0 : schedule.divideMinute) {
const period = Math.floor(60000 / schedule.divideMinute);
const repeat = schedule.divideMinute - 1;
// compose schedule
messageToSend = {
body: this.encodeMessage(message),
message_annotations: {
'x-opt-delivery-cron': '* * * * *',
'x-opt-delivery-delay': 0,
'x-opt-delivery-period': period,
'x-opt-delivery-repeat': repeat,
},
};
}
else if (class_validator_1.isDefined(schedule === null || schedule === void 0 ? void 0 : schedule.afterSeconds)) {
const milliseconds = schedule.afterSeconds * 1000;
const now = new Date();
logger.debug(`scheduling queue message for delivery after ${milliseconds} ms at around`, new Date(now.getTime() + milliseconds).toISOString());
// compose schedule
messageToSend = {
body: this.encodeMessage(message),
message_annotations: {
'x-opt-delivery-delay': milliseconds,
},
};
}
else {
messageToSend = {
body: this.encodeMessage(message),
};
}
// add other options to the message
util_1.extendObject(messageToSend, baseOptions);
// TTL handling
// istanbul ignore if
if (class_validator_1.isDefined(options === null || options === void 0 ? void 0 : options.ttl)) {
logger.debug(`setting ttl on message with ${options.ttl} ms`);
messageToSend.ttl = options.ttl;
}
logger.verbose(`outgoing message to queue '${target}', payload: ${JSON.stringify(messageToSend)}`);
// send message
const delivery = await sender.send(messageToSend);
// istanbul ignore next: SendState is dependent on broker, very hard to mock out
return delivery.sent || delivery.settled ? enum_1.SendState.Success : enum_1.SendState.Failed;
}
/**
* Closes the connection to the message broker. Waits for all running
* processes to complete.
*/
async shutdown() {
logger.log('shutting down queue processing');
// stop receiving
const receivers = Array.from(this.receivers.values());
for (const receiver of receivers) {
await receiver.close();
while (receiver.connection.isOpen() && receiver.credit === 0) {
logger.log(`waiting to finish queue processing`);
await util_1.sleep(1000);
}
}
// disconnect queue connections
await this.amqpService.disconnect();
logger.log('queue processing stopped');
}
/**
* Clears the existing senders and receivers.
*/
clearSenderAndReceiverLinks() {
logger.warn('clearing senders and receivers');
this.senders.clear();
this.receivers.clear();
}
/**
* Removes listener from active listeners
*
* @param {string} source Name or Source object of the queue.
* @param {string} connection Name of the connection
*
* @returns {Promise<boolean>} Returns true if listener was removed, otherwise false. If listener was not found, returns false.
*
* @public
*/
async removeListener(source, connection = constant_1.AMQP_DEFAULT_CONNECTION_TOKEN) {
const sourceToken = typeof source === 'string' ? source : JSON.stringify(source);
const receiverToken = this.getLinkToken(sourceToken, connection);
if (this.receivers.has(receiverToken)) {
const receiver = this.receivers.get(receiverToken);
await receiver.close();
return this.receivers.delete(receiverToken);
}
return false;
}
async getReceiver(source, credit, messageHandler, connection) {
var _a, _b, _c, _d, _e, _f;
const sourceToken = typeof source === 'string' ? source : JSON.stringify(source);
const receiverToken = this.getLinkToken(sourceToken, connection);
if (this.receivers.has(receiverToken)) {
return this.receivers.get(receiverToken);
}
const connectionOptions = this.amqpService.getConnectionOptions(connection);
const retryDelay = (_c = (_b = (_a = connectionOptions.retryConnection) === null || _a === void 0 ? void 0 : _a.receiver) === null || _b === void 0 ? void 0 : _b.retryDelay) !== null && _c !== void 0 ? _c : 0;
const maxRetryAttempts = (_f = (_e = (_d = connectionOptions.retryConnection) === null || _d === void 0 ? void 0 : _d.receiver) === null || _e === void 0 ? void 0 : _e.maxRetryAttempts) !== null && _f !== void 0 ? _f : 1;
let attempt = 0;
do {
try {
const receiver = await this.amqpService.createReceiver(source, credit, messageHandler.bind(this), connection);
this.receivers.set(receiverToken, receiver);
return receiver;
}
catch (error) {
logger.error(`Error creating receiver (attempt ${attempt + 1}): ${error.message}`, error.stack);
attempt = attempt + 1;
if (attempt >= maxRetryAttempts) {
throw new Error(`Max retry attempts reached for creating receiver: ${error.message}`);
}
if (retryDelay > 0) {
await util_1.sleep(retryDelay);
}
}
} while (attempt < maxRetryAttempts);
}
async getSender(target, connection) {
var _a, _b, _c, _d, _e, _f;
const senderToken = this.getLinkToken(target, connection);
if (this.senders.has(senderToken)) {
return this.senders.get(senderToken);
}
const connectionOptions = this.amqpService.getConnectionOptions(connection);
const retryDelay = (_c = (_b = (_a = connectionOptions.retryConnection) === null || _a === void 0 ? void 0 : _a.sender) === null || _b === void 0 ? void 0 : _b.retryDelay) !== null && _c !== void 0 ? _c : 0;
const maxRetryAttempts = (_f = (_e = (_d = connectionOptions.retryConnection) === null || _d === void 0 ? void 0 : _d.sender) === null || _e === void 0 ? void 0 : _e.maxRetryAttempts) !== null && _f !== void 0 ? _f : 1;
let attempt = 0;
do {
try {
const sender = await this.amqpService.createSender(target, connection);
this.senders.set(senderToken, sender);
return sender;
}
catch (error) {
logger.error(`Error creating sender (attempt ${attempt + 1}): ${error.message}`, error.stack);
attempt++;
if (attempt >= maxRetryAttempts) {
throw new Error(`Max retry attempts reached for creating sender: ${error.message}`);
}
if (retryDelay > 0) {
await util_1.sleep(retryDelay);
}
}
} while (attempt < maxRetryAttempts);
}
encodeMessage(message) {
return JSON.stringify(message);
}
decodeMessage(message) {
if (toString.call(message) === '[object Object]') {
return message;
}
const objectLike = message instanceof Buffer ? message.toString() : message;
return JSON.parse(objectLike);
}
getLinkToken(sourceToken, connection) {
return `${connection}:${sourceToken}`;
}
};
QueueService = __decorate([
common_1.Injectable(),
__metadata("design:paramtypes", [amqp_service_1.AMQPService, object_validator_service_1.ObjectValidatorService])
], QueueService);
exports.QueueService = QueueService;
const logger = new util_1.Logger(util_1.getLoggerContext(QueueService.name));