@micro.ts/core
Version:
Microservice framework with Typescript
513 lines (512 loc) • 19 kB
JavaScript
"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;