UNPKG

@team-supercharge/nest-amqp

Version:
342 lines (341 loc) 17.6 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 __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));