UNPKG

mqrpc

Version:

💫 Easy RPC over RabbitMQ

107 lines (106 loc) • 5.15 kB
"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;