UNPKG

@micro.ts/core

Version:

Microservice framework with Typescript

513 lines (512 loc) 19 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AmqpBroker = void 0; const amqplib_1 = require("amqplib"); const di_1 = require("../../di"); const BaseHelpers_1 = require("../../helpers/BaseHelpers"); const Logger_1 = require("../../server/Logger"); const AbstractBroker_1 = require("../AbstractBroker"); const AmqpClient_1 = require("./AmqpClient"); class AmqpBroker extends AbstractBroker_1.AbstractBroker { constructor() { super(...arguments); this.name = 'AmqpBroker'; this.beforeAssertHooks = []; /** * Default exchange for all the queues */ this._defaultExchange = { type: 'direct', name: '', }; /** * Bindings map created when adding queues */ this.bindings = new Map(); /** * Map an amqp message on a given queue to Action object * @param r * @param queue * @param routingKey * @param json */ this.requestMapper = (r, queue, routingKey, json, options) => __awaiter(this, void 0, void 0, function* () { let payload; try { payload = yield this.getPayload(r, json); } catch (err) { const shouldNotAck = !!options && !!options.noAck; if (!shouldNotAck) { this.channel.ack(r); } throw err; } const routingKeySplit = routingKey.split('.'); const queueSplit = this.extractQueueParamNames(queue); const params = {}; if (routingKeySplit.length === queueSplit.length) { queueSplit.forEach((item, index) => { if (item.param) { params[item.name] = routingKeySplit[index]; } }); } const act = { request: { params, path: queue, headers: r.properties.headers, // method: def.method, body: payload, qs: {}, raw: r, }, connection: this.connection, }; return act; }); /** * Default route mapping */ this.routeMapper = (def) => { let result = `${def.base}/${def.controller}/${def.handler}`; result = result.replace(/\/{2,}/g, '/').replace(/\//g, '.'); if (result.endsWith('.')) { result = result.slice(0, result.length - 1); } return result; }; } addBeforeAssertHook(hook) { this.beforeAssertHooks.push(hook); } execBeforeAsssertHooks() { return __awaiter(this, void 0, void 0, function* () { for (const hook of this.beforeAssertHooks) { yield hook(); } }); } /** * Getter for the default exchange */ get defaultExchange() { return this._defaultExchange; } /** * Setter for the default exchange * @param value */ set defaultExchange(value) { this._defaultExchange = value; } getPayload(r, json) { return __awaiter(this, void 0, void 0, function* () { const headers = r.properties.headers || {}; const isJson = !!headers['json'] && json; const isGzip = headers['Content-Encoding'] === 'gzip'; const messageBytes = r.content; let messageString = r.content.toString(); if (isGzip) { const unzippedBytes = yield (0, BaseHelpers_1.unzipAsync)(messageBytes); messageString = unzippedBytes.toString(); } if (isJson) { return JSON.parse(messageString); } let msg = messageString; try { msg = JSON.parse(messageString); } catch (jsonParseError) { // ignore } return msg; }); } convertPayload(payload, requestHeaders) { return __awaiter(this, void 0, void 0, function* () { const isJson = requestHeaders['json'] || (!!payload && payload instanceof Object); if (isJson && !requestHeaders['json']) { requestHeaders['json'] = true; } const isGzip = requestHeaders['Content-Encoding'] === 'gzip'; let payloadString = ''; if (isJson) { payloadString = JSON.stringify(payload); } else { if (!!payload && payload instanceof Object) { payloadString = JSON.stringify(payload); } else { if (payload !== null && payload !== undefined) { payloadString = payload.toString(); } else { payloadString = ''; } } } if (isGzip) { const gzipBytes = yield (0, BaseHelpers_1.zipAsync)(payloadString); return gzipBytes; } return Buffer.from(payloadString); }); } /** * Creates an AMQP client which provides sendToQueue, publish, and rpc methods, to work with the AMQP server * @param opts */ createClient(opts) { return __awaiter(this, void 0, void 0, function* () { const defaultOptions = { rpcQueue: 'rpc', unique: true, newChannel: true, }; /** * Append configured options to the default options */ const options = Object.assign(Object.assign({}, defaultOptions), (opts || {})); /** * Create client an channels */ const client = new AmqpClient_1.AmqpClient(this, options); yield client.init(); return client; }); } /** * Return AMQP connection, available after the server is started */ getConnection() { return this.connection; } /** * Returns the broker channel used to consume the app queues */ getChannel() { return this.channel; } /** * Consume message from the asserted queue, find its corresponding handler, execute the handler, * and if can reply, respond to the replyTo queue * @param route * @param message * @param value * @param json */ consumeMessage(route, message, value, json, options) { return __awaiter(this, void 0, void 0, function* () { if (message) { const exchange = message.fields.exchange || ''; const routingKey = message.fields.routingKey; /** * Convert message to action */ const mapped = yield this.requestMapper(message, route, routingKey, json, options); /** * Find the corresponding handler for the action object */ const handler = this.actionToRouteMapper(route, mapped, value); /** * Execute handler */ const result = yield handler(mapped); /** * If possible publish the value to the replyTo queue */ if (result && message.properties.replyTo && message.properties.correlationId) { yield this.rpcReply(result, message.properties.replyTo, message.properties.correlationId, message.properties.headers); } const shouldNotAck = !!options && !!options.noAck; if (!shouldNotAck) { this.channel.ack(message); } } }); } /** * Registered a single route definition and consumes messages from it * Called when the server is started * @param value * @param route */ registerSingleRoute(value, route) { return __awaiter(this, void 0, void 0, function* () { let json = false; let totalConsumers = 0; let queueOptions = {}; const consumeOptions = {}; /* * Finds the number of consumers and the assertQueue options */ value.forEach((v) => { if (Object.keys(queueOptions).length === 0 && v.def.queueOptions) { /** * Get the last queue options, in case there is collisions */ queueOptions = Object.assign({}, v.def.queueOptions); Object.assign(consumeOptions, v.def.queueOptions.consumeOptions || {}); /** * Remove the consumers key from the queue options, to use the resulting object as Options on queue assertion */ delete queueOptions.consumers; delete queueOptions.consumeOptions; } const consumers = v.def.queueOptions ? v.def.queueOptions.consumers || 1 : 1; if (v.def.json) { json = true; } totalConsumers += consumers; }); if (totalConsumers > 0) { yield this.channel.assertQueue(route, queueOptions); const exchanges = []; /** * Find all associated bindings */ this.bindings.forEach((values, key) => { const item = values.find((x) => x.queue === route); if (item) { exchanges.push({ exchange: key.name, pattern: item.pattern, }); } }); /** * Bind the queue to all associated exchanges */ if (exchanges.length) { /** * Bind queue to the configured exchanges */ for (let i = 0; i < exchanges.length; i++) { yield this.channel.bindQueue(route, exchanges[i].exchange, exchanges[i].pattern); } } /** * Create the specified number of consumers */ for (let i = 0; i < totalConsumers; i++) { yield this.channel.consume(route, (message) => __awaiter(this, void 0, void 0, function* () { yield this.consumeMessage(route, message, value, json, consumeOptions); }), consumeOptions); } return true; } else { return false; } }); } /** * Add single route, get its exchanges config and save it in the binding map * @param def * @param handler */ addRoute(def, handler) { /** * Check if a default exchange configuration exists */ const hasDefaultExchange = !!this.defaultExchange && this.defaultExchange.name.length > 0; /** * Check if a custom exchange configuration exists */ const hasCustomExchange = !!def.queueOptions && !!def.queueOptions.exchange && def.queueOptions.exchange.name.length > 0; /** * Build a list of exchange configurations */ const exchanges = []; if (hasDefaultExchange) { exchanges.push(this.defaultExchange); } if (hasCustomExchange) { exchanges.push(def.queueOptions.exchange); } /** * Call super method and get the resulting queue */ const queue = super.addRoute(def, handler); /** * For each exchange configuration insert on the exchanges map, the current bindings */ exchanges.forEach((exchange) => { let pattern = ''; if (!!def.queueOptions && !!def.queueOptions.bindingPattern) { /** * If binding pattern configured on the handler use that value as a binding pattern */ pattern = def.queueOptions.bindingPattern; } else { /** * Process the binding pattern using the queue name and the type of the exchange * if the exchange is a 'topic' extract the param names from the queue names and replace them with '#' * on the binding pattern */ pattern = this.getQueuePattern(queue, exchange.type); } /** * Find saved exchanges by name only, if an exchange configured more than 1 time with difference options, * keep the exchange with the first occurring options */ const keys = Array.from(this.bindings.keys()); /** * Check if the exchange already exists on the map */ const found = keys.find((x) => x.name === exchange.name); let bindings = []; /** * If the exchange name found in the current configuration, get the key and the value and delete it form the map, * Update the key with the new options, and reset it in the map with the new options and bindings */ if (found) { bindings = this.bindings.get(found) || []; this.bindings.delete(found); found.options = found.options || exchange.options; exchange = found; } bindings.push({ queue, pattern: pattern || '' }); this.bindings.set(exchange, bindings); }); return queue; } /** * Assert every exchange */ assertExchanges() { return __awaiter(this, void 0, void 0, function* () { const exchanges = Array.from(this.bindings.keys()); for (let i = 0; i < exchanges.length; i++) { yield this.channel.assertExchange(exchanges[i].name, exchanges[i].type, exchanges[i].options); } }); } registerRoutes() { return __awaiter(this, void 0, void 0, function* () { yield this.assertExchanges(); const routes = []; /** * Convert key value to array */ this.registeredRoutes.forEach((value, route) => { routes.push({ value, route }); }); /** * Await each item on registering the routes */ for (let i = 0; i < routes.length; i++) { yield this.registerSingleRoute(routes[i].value, routes[i].route); } }); } /** * Reply if the message has replyTo queue and correlationId * @param data * @param replyToQueue * @param correlationId */ rpcReply(data, replyToQueue, correlationId, requestHeaders = {}) { return __awaiter(this, void 0, void 0, function* () { const response = data.response || {}; const body = response.body || response.error; const headers = response.headers || {}; if (response.is_error) { headers.error = true; } headers.statusCode = response.statusCode; headers.json = !!requestHeaders.json; if (requestHeaders['Content-Encoding']) { headers['Content-Encoding'] = requestHeaders['Content-Encoding']; } const reply = yield this.convertPayload(body, requestHeaders); /** * Reply if the message has rpcReply and correlationId */ this.channel.sendToQueue(replyToQueue, reply, { correlationId, headers, }); }); } get connectionConfig() { return this.config; } /** * Create connection and channel, * assert exchanges and queues, * create bindings, * register consumers */ start() { return __awaiter(this, void 0, void 0, function* () { this.connection = yield (0, amqplib_1.connect)(this.connectionConfig); this.connection.on('close', (e) => { this.handleConnectionError(e); }); this.connection.on('error', (e) => { this.handleConnectionError(e); }); this.channel = yield this.connection.createChannel(); yield this.execBeforeAsssertHooks(); yield this.registerRoutes(); di_1.Container.get(Logger_1.LoggerKey).info(`AMQP Connected on ${this.connectionConfig}`); }); } /** * Extract params for a given queue string * @param queue */ extractQueueParamNames(queue) { return this.extractParamNames(queue, '.'); } /** * Get a queue pattern string from a given queue name, * If exchange type is not topic return the queue name as a pattern, * else replace all ':param' parts of the string with '#' * @param queue * @param type */ getQueuePattern(queue, type) { if (type === 'topic') { return queue .split('.') .map((x) => { if (x.length > 0 && x[0] === ':') { return '#'; } return x; }) .join('.'); } return queue; } /** * Called immediately after broker configuration is set, either with a constructor configuration, * or a config resolver from an IConfiguration instance */ construct() { // Do nothing } } exports.AmqpBroker = AmqpBroker;