rabbitmq-client
Version:
Robust, typed, RabbitMQ (0-9-1) client library
198 lines (197 loc) • 7.07 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RPCClient = void 0;
const util_1 = require("./util");
const exception_1 = require("./exception");
const DEFAULT_TIMEOUT = 30_000;
/**
* @see {@link Connection#createRPCClient | Connection#createRPCClient()}
* @see {@link RPCProps}
* @see {@link https://www.rabbitmq.com/direct-reply-to.html}
*
* This will create a single "client" `Channel` on which you may publish
* messages and listen for direct responses. This can allow, for example, two
* micro-services to communicate with each other using RabbitMQ as the
* middleman instead of directly via HTTP.
*
* If you're using the createConsumer() helper, then you can reply to RPC
* requests simply by using the `reply()` argument of
* the {@link ConsumerHandler}.
*
* Also, since this wraps a Channel, this must be closed before closing the
* Connection: `RPCClient.close()`
*
* @example
* ```
* // rpc-client.js
* const rabbit = new Connection()
*
* const rpcClient = rabbit.createRPCClient({confirm: true})
*
* const res = await rpcClient.send('my-rpc-queue', 'ping')
* console.log('response:', res.body) // pong
*
* await rpcClient.close()
* await rabbit.close()
* ```
*
* ```
* // rpc-server.js
* const rabbit = new Connection()
*
* const rpcServer = rabbit.createConsumer({
* queue: 'my-rpc-queue'
* }, async (req, reply) => {
* console.log('request:', req.body)
* await reply('pong')
* })
*
* process.on('SIGINT', async () => {
* await rpcServer.close()
* await rabbit.close()
* })
* ```
*
* If you're communicating with a different rabbitmq client implementation
* (maybe in a different language) then the consumer should send responses
* like this:
* ```
* ch.basicPublish({
* routingKey: req.replyTo,
* correlationId: req.correlationId,
* exchange: ""
* }, responseBody)
* ```
*/
class RPCClient {
/** @internal */
_conn;
/** @internal */
_ch;
/** @internal */
_props;
/** @internal */
_requests = new Map();
/** @internal */
_pendingSetup;
/** @internal CorrelationId counter */
_id = 0;
/** True while the client has not been explicitly closed */
active = true;
/** @internal */
constructor(conn, props) {
this._conn = conn;
this._props = props;
}
/** @internal */
async _setup() {
let { _ch: ch, _props: props } = this;
if (!ch || !ch.active) {
ch = this._ch = await this._conn.acquire({ emitErrorsFromChannel: true });
let caught;
ch.once('error', (err) => {
caught = err;
});
ch.once('close', () => {
// request-response MUST be on the same channel, so if the channel dies
// so does all pending requests
const err = new exception_1.AMQPChannelError('RPC_CLOSED', 'RPC channel closed unexpectedly', caught);
for (const dfd of this._requests.values())
dfd.reject(err);
this._requests.clear();
});
}
if (props.queues)
for (const params of props.queues) {
await ch.queueDeclare(params);
}
if (props.exchanges)
for (const params of props.exchanges) {
await ch.exchangeDeclare(params);
}
if (props.queueBindings)
for (const params of props.queueBindings) {
await ch.queueBind(params);
}
if (props.exchangeBindings)
for (const params of props.exchangeBindings) {
await ch.exchangeBind(params);
}
if (props.confirm) {
await ch.confirmSelect();
}
// n.b. This is not a real queue & this consumer will not appear in the management UI
await ch.basicConsume({
noAck: true,
queue: 'amq.rabbitmq.reply-to'
}, (res) => {
if (res.correlationId) {
// resolve an exact request
const dfd = this._requests.get(res.correlationId);
if (dfd != null) {
this._requests.delete(res.correlationId);
dfd.resolve(res);
}
}
// otherwise the response is discarded
});
// ch.once('basic.cancel') shouldn't happen
}
async send(envelope, body) {
const maxAttempts = this._props.maxAttempts || 1;
let attempts = 0;
while (true)
try {
if (!this.active)
throw new exception_1.AMQPChannelError('RPC_CLOSED', 'RPC client is closed');
if (!this._ch?.active) {
if (!this._pendingSetup)
this._pendingSetup = this._setup().finally(() => { this._pendingSetup = undefined; });
await this._pendingSetup;
}
const id = String(++this._id);
const timeout = this._props.timeout == null ? DEFAULT_TIMEOUT : this._props.timeout;
await this._ch.basicPublish({
...(typeof envelope === 'string' ? { routingKey: envelope } : envelope),
replyTo: 'amq.rabbitmq.reply-to',
correlationId: id,
expiration: String(timeout)
}, body);
const dfd = (0, util_1.createDeferred)();
const timer = setTimeout(() => {
dfd.reject(new exception_1.AMQPError('RPC_TIMEOUT', 'RPC response timed out'));
this._requests.delete(id);
}, timeout);
this._requests.set(id, dfd);
// remember to stop the timer if we get a response or if there is some other failure
return await dfd.promise.finally(() => { clearTimeout(timer); });
}
catch (err) {
if (++attempts >= maxAttempts) {
Error.captureStackTrace(err); // original async trace is likely not useful to users
throw err;
}
// else loop; notify with event?
}
}
/** @deprecated Alias for {@link RPCClient#send} */
publish(envelope, body) {
return this.send(envelope, body);
}
/** Stop consuming messages. Close the channel once all pending message
* handlers have settled. If called while the Connection is reconnecting,
* then this may be delayed by {@link ConnectionOptions.acquireTimeout} */
async close() {
this.active = false;
try {
await this._pendingSetup;
await Promise.allSettled(Array.from(this._requests.values()).map(dfd => dfd.promise));
}
catch (err) {
// do nothing; task failed successfully
}
// Explicitly not cancelling the consumer; it's not necessary.
await this._ch?.close();
}
}
exports.RPCClient = RPCClient;