mqrpc
Version:
💫 Easy RPC over RabbitMQ
107 lines (106 loc) • 5.15 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const AmqpClient_1 = require("./AmqpClient");
const logger_1 = require("./logger");
const errors_1 = require("./RpcServer/errors");
const comms = require("./RpcServer/comms");
class RpcServer {
/**
* Instances a new RPC Server with the given config
*
* @param {RpcClientOptions} opts Config for this client, required.
* @param {AmqpClientOptions} opts.amqpClient Config for the underlying AMQP connection, required.
* @param {string} [opts.amqpClient.amqpUrl] URL for the AMQP broker.
* @param {object} [opts.amqpClient.socketOptions] Config for the AMQP connection.
* @param {object} [opts.amqpClient.connection] An open AMQP connection, for re-use.
* @param {object} [opts.amqpClient.channel] An open AMQP channel, for re-use.
* @param {number} [opts.amqpClient.prefetchCount] Global prefetch count when consuming messages. Default
* is 100.
* @param {RpcOptions} [opts.rpcServer] Config for the client itself.
* @param {string} [opts.rpcServer.rpcExchangeName] Exchange where calls are published. Default 'mqrpc'.
* Must match client.
* @param {StandardLogger} [opts.rpcServer.logger] Custom logger for client use.
*/
constructor(opts) {
this.rpcExchangeName = 'mqrpc';
this.log = logger_1.default;
this.procedures = new Map();
this.amqpClient = new AmqpClient_1.default(opts.amqpClient);
if (opts.rpcServer) {
if (opts.rpcServer.rpcExchangeName)
this.rpcExchangeName = opts.rpcServer.rpcExchangeName;
if (opts.rpcServer.logger)
this.log = opts.rpcServer.logger;
}
}
async init() {
if (this.procedures.size === 0) {
this.log.warn('[RpcServer] Initializing server with no registed procedures. ' +
'Any received calls will error out!');
}
await this.amqpClient.init();
await Promise.all([
this.amqpClient.channel.assertExchange(this.rpcExchangeName, 'direct', { durable: false }),
this.amqpClient.channel.assertQueue(`${this.rpcExchangeName}.call`, { durable: false })
]);
await this.amqpClient.channel.bindQueue(`${this.rpcExchangeName}.call`, this.rpcExchangeName, 'call');
const { consumerTag } = await this.amqpClient.channel.consume(`${this.rpcExchangeName}.call`, async (message) => {
if (!message)
return;
let content;
try {
content = comms.extractCallContent(message);
}
catch (err) {
this.log.error('[RpcServer] Got an invalid call', err, { message });
return this.amqpClient.channel.nack(message);
}
const heartbeatWrapper = comms.whileSendingHeartbeats(this.amqpClient.channel, message, content.timeouts);
try {
// TODO: if callTimeout is set, we should wait a max of that for the proc,
// since the client won't be there for the reply after that anyway
// FIXME: do not reply if the server has been `term`ed
const response = await heartbeatWrapper(() => this.call(content.procedure, content.args || []));
await comms.reply(this.amqpClient.channel, message, response);
}
catch (err) {
if (err instanceof errors_1.RpcServerError) {
return await comms.reply(this.amqpClient.channel, message, err);
}
// Not an error on the procedure per se, but some unexpected error
// while processing the call. A 500, if you will.
this.log.error('[RpcServer] Error running call', err);
this.amqpClient.channel.nack(message);
}
});
this.consumerTag = consumerTag;
}
async term() {
if (!this.consumerTag)
return;
await this.amqpClient.channel.cancel(this.consumerTag);
await this.amqpClient.term();
delete this.consumerTag;
}
register(procedure, handler) {
this.procedures.set(procedure, handler);
}
registerDebugProcedures() {
this.register('mqrpc.echo', (arg) => arg);
}
async call(procedure, args) {
if (!procedure)
throw new errors_1.InvalidCall('No `procedure` was provided');
const proc = this.procedures.get(procedure);
if (!proc)
throw new errors_1.NoSuchProcedure(procedure);
this.log.info(`[RpcServer] Running procedure ${procedure}`);
try {
return await proc(...args);
}
catch (err) {
throw new errors_1.ProcedureFailed(err);
}
}
}
exports.default = RpcServer;