UNPKG

mqrpc

Version:

💫 Easy RPC over RabbitMQ

169 lines (104 loc) • 7.12 kB
# MQRPC > 💫 Easy RPC over RabbitMQ Easily implement RPC over your RabbitMQ broker with a few lines of code: ```javascript import { RpcClient, RpcServer } from 'mqrpc' const server = new RpcServer({ amqpClient: { amqpUrl: 'amqp://localhost '} }) const client = new RpcClient({ amqpClient: { amqpUrl: 'amqp://localhost '} }) server.register('math.add', (a, b) => a + b) await server.init() await client.init() await client.call('math.add', 2, 2) // 4 ``` MQRPC leverages [RabbitMQs Direct reply-to](https://www.rabbitmq.com/direct-reply-to.html) functionality to implement an RPC system with reasonable defaults that work out-of-the box. All you need is a RabbitMQ broker and its URL. ## Features * Any number of servers & clients * Argument & Return serialization * Error serialization * Timeout management ## API Both Server & Client are designed to be simple to use and thus have a low-surface API. The following type definitions follow TypeScript syntax loosely: #### RpcServer ##### `new RpcServer({ amqpClient: AmqpOpts, rpcServer?: ServerOpts })` Instances a new server with the given config. `amqpClient` is required: ```typescript type AmqpOpts = { connection?: amqplib.Connection // Pass a live amqplib connection here to re-use it. amqpUrl?: string // The RabbitMQ URL. Ignored if `connection` is provided. socketOptions?: object // Customize connection to RabbitMQ. prefetchCount: number // Customize consumer prefetch count. } type ServerOpts = { rpcExchangeName?: string // Exchange name for server, defaults to 'mqrpc'. logger?: object // For custom logger injection. } ``` Although all configs are optional, one of `amqpClient.connection` or `amqpClient.amqpUrl` must be passed. ##### `async server.init()` Declares all the exchanges, queues and bindings for the server. Starts listening for calls from clients, _so you should call this after registering all available procedures_. ##### `server.register(procedure: string, handler: Function)` Registers a new procedure and its handler in the server. The handler can be synchronous or return a Promise. ```javascript server.register('util.echo', arg => arg) server.register('time.now', () => Date.now()) server.register('math.average', (...args) => args.reduce((acc, val) => acc + val) / args.length) server.register('meaning.of.life', async () => 42) ``` `register` should be called before `init` to ensure the server won't receive any unknown calls by clients that are already live. ##### `server.registerDebugProcedures()` Adds a default procedure for debugging, with name `mqrpc.echo`, that returns any given argument. ##### `async server.term()` Neatly shut down the server. Closes the AMQP channel and, if one wasn't provided, the connection as well. #### RpcClient ##### `new RpcClient({ amqpClient: AmqpOpts, rpcClient?: ClientOpts })` Instances a new client with the given config. `amqpClient` is required: ```typescript type AmqpOpts = { connection?: amqplib.Connection // Pass a live amqplib connection here to re-use it. amqpUrl?: string // The RabbitMQ URL. Ignored if `connection` is provided. socketOptions?: object // Customize connection to RabbitMQ. prefetchCount: number // Customize consumer prefetch count. } type ClientOpts = { rpcExchangeName?: string // Exchange name for server, defaults to 'mqrpc'. logger?: object // For custom logger injection. ackTimeout?: number // How long should the client wait for a Server to start working on a call. Default 0 (no timeout). idleTimeout?: number // How long can the server be idle until it is considered "dead". Default 0 (no timeout). callTimeout?: number // Maximum time from making a call to receiving a reply. Default 900 000 (15 minutes). } ``` Although all configs are optional, one of `amqpClient.connection` or `amqpClient.amqpUrl` must be passed. Every timeout is in milliseconds and will throw `TimeoutExpired` when breached. See [timeouts](#timeouts) below for more info. ##### `async client.init()` Connects to RabbitMQ and starts listening for replies from servers. ##### `async client.call(procedure: string, ...args: any[])` Calls the named `procedure` on an RpcServer with the given `args`. Resolves to whatever the procedure returns. Rejects if the procedure throws, or there is a connection error or an error in the server. ###### Errors The following error types may be thrown: * `ProcedureFailed`: The most common (hopefully), is thrown when the procedure itself throws. The remote error stack may be included in `error.causeStack`. * `ServerError`: When an error occurs in the server while processing the call. Eg: the requested procedure is not registered. * `UnparseableContent`: Whatever reply we got from a server could not be parsed. * `UnknownReply`: Reply was parseable, but the format isn't understood. ##### `async client.term({ waitForCalls: number })` Neatly shut down the client. Terminates any active calls, closes the AMQP channel and, if one wasn't provided, the connection as well. Will wait up to `waitForCalls` milliseconds for pending calls to resolve, or indefinitely if given 0ms. ## Timeouts Since it may not be sensible to wait forever for a call to resolve, the client exposes three configurable timeouts that will interrupt a call when expired. These are: ### `ackTimeout` When a server receives a procedure call it will send an `ack` message back to the client, immediately before beginning execution. This signals the client that a server is handling their call. This timeout signals how long to wait until the `ack` is received. This timeout is disabled by default, since it's sensible to expect a server will eventually pick up a client's call. However, it may be used to control for times of high message congestion, for example. ### `idleTimeout` While the server is executing a procedure, it'll periodically send `wait` messages back to the client (behind the scenes). This works as a heartbeat of sorts and indicates to the client that the server hasn't crashed, or in some way disconnected. This timeout indicates how long a server may be silent before aborting the call. This timeout is disabled by default, since RabbitMQ has its own hearbeat functionality that, in conjunction with its own `ack` mode, guarantees at-least-once execution. You may enable this if operating in `noAck` mode. ### `callTimeout` The overall maximum time a call may take to resolve a request. This timeout starts on a procedure call and terminates when a reply is received. This timeout is 15 minutes by default. ## Future Features * Publisher drain management * Server-side timeout management ## Testing You'll need a local RabbitMQ broker to run the tests. Optionally set env var `RABBITMQ_VHOST` to specify a vhost, uses `/` by default. Then: `$ yarn test` ## Contributing Feel free to submit PRs. Please include unit tests for any new features. ## Why TypeScript Because I wanted to try it out ¯\\_(ツ)_/¯