rabbitmq-client
Version:
Robust, typed, RabbitMQ (0-9-1) client library
606 lines (605 loc) • 25.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Channel = void 0;
const exception_1 = require("./exception");
const util_1 = require("./util");
const node_events_1 = __importDefault(require("node:events"));
const codec_1 = require("./codec");
var CH_MODE;
(function (CH_MODE) {
CH_MODE[CH_MODE["NORMAL"] = 0] = "NORMAL";
CH_MODE[CH_MODE["TRANSACTION"] = 1] = "TRANSACTION";
CH_MODE[CH_MODE["CONFIRM"] = 2] = "CONFIRM";
})(CH_MODE || (CH_MODE = {}));
/**
* @see {@link Connection#acquire | Connection#acquire()}
* @see {@link Connection#createConsumer | Connection#createConsumer()}
* @see {@link Connection#createPublisher | Connection#createPublisher()}
* @see {@link Connection#createRPCClient | Connection#createRPCClient()}
*
* A raw Channel can be acquired from your Connection, but please consider
* using a higher level abstraction like a {@link Consumer} or
* {@link Publisher} for most cases.
*
* AMQP is a multi-channelled protocol. Channels provide a way to multiplex a
* heavyweight TCP/IP connection into several light weight connections. This
* makes the protocol more “firewall friendly” since port usage is predictable.
* It also means that traffic shaping and other network QoS features can be
* easily employed. Channels are independent of each other and can perform
* different functions simultaneously with other channels, the available
* bandwidth being shared between the concurrent activities.
*
* @example
* ```
* const rabbit = new Connection()
*
* // Will wait for the connection to establish and then create a Channel
* const ch = await rabbit.acquire()
*
* // Channels can emit some events too (see documentation)
* ch.on('close', () => {
* console.log('channel was closed')
* })
*
* // Create a queue for the duration of this connection
* await ch.queueDeclare({queue: 'my-queue'})
*
* // Enable publisher acknowledgements
* await ch.confirmSelect()
*
* const data = {title: 'just some object'}
*
* // Resolves when the data has been flushed through the socket or if
* // ch.confirmSelect() was called: will wait for an acknowledgement
* await ch.basicPublish({routingKey: 'my-queue'}, data)
*
* const msg = ch.basicGet('my-queue')
* console.log(msg)
*
* await ch.queueDelete('my-queue')
*
* // It's your responsibility to close any acquired channels
* await ch.close()
* ```
*/
class Channel extends node_events_1.default {
/** @internal */
_conn;
id;
/** False if the channel is closed */
active;
/** @internal */
_state;
/** @internal */
constructor(id, conn, emitErrors = false) {
super();
this._conn = conn;
this.id = id;
this.active = true;
this._state = {
emitErrors: emitErrors,
maxFrameSize: conn._opt.frameMax,
deliveryCount: 1,
mode: CH_MODE.NORMAL,
unconfirmed: new Map(),
rpcBuffer: [],
cleared: false,
consumers: new Map(),
stream: new util_1.EncoderStream(conn._socket)
};
this._state.stream.on('error', () => {
// don't need to propagate error here:
// - if connection ended: already handled by the Connection class
// - if encoding error: error recieved by write callback
this.close();
});
}
/** Close the channel */
async close() {
if (!this.active) {
return;
}
this.active = false;
try {
// wait for encoder stream to end
if (this._state.stream.writable) {
if (!this._state.rpc)
this._state.stream.end();
await new Promise(resolve => this._state.stream.on('close', resolve));
}
else {
// if an rpc failed to encode then wait for it to clear
await new Promise(setImmediate);
}
// wait for final rpc, if it was already sent
if (this._state.rpc) {
const [dfd] = this._state.rpc;
this._state.rpc = undefined;
await dfd.promise;
}
// send channel.close
const dfd = (0, util_1.createDeferred)();
this._state.rpc = [dfd, codec_1.Cmd.ChannelClose, codec_1.Cmd.ChannelCloseOK];
this._conn._writeMethod({
type: codec_1.FrameType.METHOD,
channelId: this.id,
methodId: codec_1.Cmd.ChannelClose,
params: { replyCode: 200, replyText: '', methodId: 0 }
});
await dfd.promise;
}
catch (err) {
// ignored; if write fails because the connection closed then this is
// technically a success. Can't have a channel without a connection!
}
finally {
this._clear();
}
}
/** @internal */
_handleRPC(methodId, data) {
if (methodId === codec_1.Cmd.ChannelClose) {
const params = data;
this.active = false;
this._conn._writeMethod({
type: codec_1.FrameType.METHOD,
channelId: this.id,
methodId: codec_1.Cmd.ChannelCloseOK,
params: undefined
});
const strcode = codec_1.ReplyCode[params.replyCode] || String(params.replyCode);
const msg = codec_1.Cmd[params.methodId] + ': ' + params.replyText;
const err = new exception_1.AMQPChannelError(strcode, msg);
//const badName = SPEC.getFullName(params.classId, params.methodId)
if (params.methodId === codec_1.Cmd.BasicPublish && this._state.unconfirmed.size > 0) {
// reject first unconfirmed message
const [tag, dfd] = this._state.unconfirmed.entries().next().value;
this._state.unconfirmed.delete(tag);
dfd.reject(err);
}
else if (this._state.rpc && params.methodId === this._state.rpc[1]) {
// or reject the rpc
const [dfd] = this._state.rpc;
this._state.rpc = undefined;
dfd.reject(err);
}
else {
// last resort
if (this._state.emitErrors) {
this.emit('error', err);
}
else {
this._conn.emit('error', err);
}
}
this._clear();
return;
}
if (!this._state.rpc) {
throw new exception_1.AMQPConnectionError('UNEXPECTED_FRAME', `client received unexpected method ch${this.id}:${codec_1.Cmd[methodId]} ${JSON.stringify(data)}`);
}
const [dfd, , expectedId] = this._state.rpc;
this._state.rpc = undefined;
if (expectedId !== methodId) {
throw new exception_1.AMQPConnectionError('UNEXPECTED_FRAME', `client received unexpected method ch${this.id}:${codec_1.Cmd[methodId]} ${JSON.stringify(data)}`);
}
dfd.resolve(data);
if (this._state.stream.writable) {
if (!this.active)
this._state.stream.end();
else if (this._state.rpcBuffer.length > 0)
this._rpcNext(this._state.rpcBuffer.shift());
}
}
/**
* Invoke all pending response handlers with an error
* @internal
*/
_clear(err) {
if (this._state.cleared)
return;
this._state.cleared = true;
if (err == null)
err = new exception_1.AMQPChannelError('CH_CLOSE', 'channel is closed');
this.active = false;
if (this._state.rpc) {
const [dfd] = this._state.rpc;
this._state.rpc = undefined;
dfd.reject(err);
}
for (const [dfd] of this._state.rpcBuffer) {
dfd.reject(err);
}
this._state.rpcBuffer = [];
for (const dfd of this._state.unconfirmed.values()) {
dfd.reject(err);
}
this._state.unconfirmed.clear();
this._state.consumers.clear();
this._state.stream.destroy(err);
this.emit('close');
}
/** @internal */
_onMethod(methodFrame) {
if (this._state.incoming != null) {
throw new exception_1.AMQPConnectionError('UNEXPECTED_FRAME', 'unexpected method frame, already awaiting header/body; this is a bug');
}
if ([codec_1.Cmd.BasicDeliver, codec_1.Cmd.BasicReturn, codec_1.Cmd.BasicGetOK].includes(methodFrame.methodId)) {
this._state.incoming = { methodFrame, headerFrame: undefined, chunks: undefined, received: 0 };
}
else if (methodFrame.methodId === codec_1.Cmd.BasicGetEmpty) {
this._handleRPC(codec_1.Cmd.BasicGetOK, undefined);
}
else if (this._state.mode === CH_MODE.CONFIRM && methodFrame.methodId === codec_1.Cmd.BasicAck) {
const params = methodFrame.params;
if (params.multiple) {
for (const [tag, dfd] of this._state.unconfirmed.entries()) {
if (tag > params.deliveryTag)
break;
dfd.resolve();
this._state.unconfirmed.delete(tag);
}
}
else {
const dfd = this._state.unconfirmed.get(params.deliveryTag);
if (dfd) {
dfd.resolve();
this._state.unconfirmed.delete(params.deliveryTag);
}
else {
//TODO channel error; PRECONDITION_FAILED, unexpected ack
}
}
}
else if (this._state.mode === CH_MODE.CONFIRM && methodFrame.methodId === codec_1.Cmd.BasicNack) {
const params = methodFrame.params;
if (params.multiple) {
for (const [tag, dfd] of this._state.unconfirmed.entries()) {
if (tag > params.deliveryTag)
break;
dfd.reject(new exception_1.AMQPError('NACK', 'message rejected by server'));
this._state.unconfirmed.delete(tag);
}
}
else {
const dfd = this._state.unconfirmed.get(params.deliveryTag);
if (dfd) {
dfd.reject(new exception_1.AMQPError('NACK', 'message rejected by server'));
this._state.unconfirmed.delete(params.deliveryTag);
}
else {
//TODO channel error; PRECONDITION_FAILED, unexpected nack
}
}
}
else if (methodFrame.methodId === codec_1.Cmd.BasicCancel) {
const params = methodFrame.params;
this._state.consumers.delete(params.consumerTag);
setImmediate(() => {
this.emit('basic.cancel', params.consumerTag, new exception_1.AMQPError('CANCEL_FORCED', 'cancelled by server'));
});
//} else if (methodFrame.fullName === 'channel.flow') unsupported; https://blog.rabbitmq.com/posts/2014/04/breaking-things-with-rabbitmq-3-3
}
else {
this._handleRPC(methodFrame.methodId, methodFrame.params);
}
}
/** @internal */
_onHeader(headerFrame) {
if (!this._state.incoming || this._state.incoming.headerFrame || this._state.incoming.received > 0)
throw new exception_1.AMQPConnectionError('UNEXPECTED_FRAME', 'unexpected header frame; this is a bug');
const expectedContentFrameCount = Math.ceil(headerFrame.bodySize / (this._state.maxFrameSize - 8));
this._state.incoming.headerFrame = headerFrame;
this._state.incoming.chunks = new Array(expectedContentFrameCount);
if (expectedContentFrameCount === 0)
this._onBody();
}
/** @internal */
_onBody(bodyFrame) {
if (this._state.incoming?.chunks == null || this._state.incoming.headerFrame == null || this._state.incoming.methodFrame == null)
throw new exception_1.AMQPConnectionError('UNEXPECTED_FRAME', 'unexpected AMQP body frame; this is a bug');
if (bodyFrame)
this._state.incoming.chunks[this._state.incoming.received++] = bodyFrame.payload;
if (this._state.incoming.received === this._state.incoming.chunks.length) {
const { methodFrame, headerFrame, chunks } = this._state.incoming;
this._state.incoming = undefined;
let body = Buffer.concat(chunks);
if (headerFrame.fields.contentType === 'text/plain' && !headerFrame.fields.contentEncoding) {
body = body.toString();
}
else if (headerFrame.fields.contentType === 'application/json' && !headerFrame.fields.contentEncoding) {
try {
body = JSON.parse(body.toString());
}
catch (_) {
// do nothing; this is a user problem
}
}
const uncastMessage = {
...methodFrame.params,
...headerFrame.fields,
durable: headerFrame.fields.deliveryMode === 2,
body
};
if (methodFrame.methodId === codec_1.Cmd.BasicDeliver) {
const message = uncastMessage;
// setImmediate allows basicConsume to resolve first if
// basic.consume-ok & basic.deliver are received in the same chunk.
// Also this resets the stack trace for handler()
setImmediate(() => {
const handler = this._state.consumers.get(message.consumerTag);
if (!handler) {
// this is a bug; missing handler for consumerTag
// TODO should never happen but maybe close the channel here
}
else {
// no try-catch; users must handle their own errors
handler(message);
}
});
}
else if (methodFrame.methodId === codec_1.Cmd.BasicReturn) {
setImmediate(() => {
this.emit('basic.return', uncastMessage); // ReturnedMessage
});
}
else if (methodFrame.methodId === codec_1.Cmd.BasicGetOK) {
this._handleRPC(codec_1.Cmd.BasicGetOK, uncastMessage); // SyncMessage
}
}
}
/** @internal
* AMQP does not support RPC pipelining!
* C = client
* S = server
*
* C:basic.consume
* C:queue.declare
* ...
* S:queue.declare <- response may arrive out of order
* S:basic.consume
*
* So we can only have one RPC in-flight at a time:
* C:basic.consume
* S:basic.consume
* C:queue.declare
* S:queue.declare
**/
_invoke(req, res, params) {
if (!this.active)
return Promise.reject(new exception_1.AMQPChannelError('CH_CLOSE', 'channel is closed'));
const dfd = (0, util_1.createDeferred)();
const it = (0, codec_1.genFrame)({
type: codec_1.FrameType.METHOD,
channelId: this.id,
methodId: req,
params: params
}, this._state.maxFrameSize);
const rpc = [dfd, req, res, it];
if (this._state.rpc)
this._state.rpcBuffer.push(rpc);
else
this._rpcNext(rpc);
return dfd.promise.catch(util_1.recaptureAndThrow);
}
/** @internal
* Start the next RPC */
_rpcNext([dfd, req, res, it]) {
this._state.rpc = [dfd, req, res];
this._state.stream.write(it, (err) => {
if (err) {
this._state.rpc = undefined;
dfd.reject(err);
}
});
}
/** @internal */
_invokeNowait(methodId, params) {
if (!this.active)
throw new exception_1.AMQPChannelError('CH_CLOSE', 'channel is closed');
const frame = {
type: codec_1.FrameType.METHOD,
channelId: this.id,
methodId: methodId,
params: params
};
this._state.stream.write((0, codec_1.genFrame)(frame, this._state.maxFrameSize), (err) => {
if (err) {
err.message += '; ' + codec_1.Cmd[methodId];
if (this._state.emitErrors) {
this.emit('error', err);
}
else {
this._conn.emit('error', err);
}
}
});
}
basicPublish(params, body) {
if (!this.active)
return Promise.reject(new exception_1.AMQPChannelError('CH_CLOSE', 'channel is closed'));
if (typeof params == 'string') {
params = { routingKey: params };
}
params = Object.assign({ timestamp: Math.floor(Date.now() / 1000) }, params);
params.deliveryMode = (params.durable || params.deliveryMode === 2) ? 2 : 1;
params.rsvp1 = 0;
if (typeof body == 'string') {
body = Buffer.from(body, 'utf8');
params.contentType = 'text/plain';
params.contentEncoding = undefined;
}
else if (!Buffer.isBuffer(body)) {
body = Buffer.from(JSON.stringify(body), 'utf8');
params.contentType = 'application/json';
params.contentEncoding = undefined;
}
const publish = this._state.stream.writeAsync((0, codec_1.genContentFrames)(this.id, params, body, this._state.maxFrameSize));
if (this._state.mode === CH_MODE.CONFIRM) {
return publish.then(() => {
// wait for basic.ack or basic.nack
// note: Unroutable mandatory messages are acknowledged right
// after the basic.return method. May be ack'd out-of-order.
const dfd = (0, util_1.createDeferred)();
this._state.unconfirmed.set(this._state.deliveryCount++, dfd);
return dfd.promise;
});
}
return publish;
}
/**
* This is a low-level method; consider using {@link Connection#createConsumer | Connection#createConsumer()} instead.
*
* Begin consuming messages from a queue. Consumers last as long as the
* channel they were declared on, or until the client cancels them. The
* callback `cb(msg)` is called for each incoming message. You must call
* {@link Channel#basicAck} to complete the delivery, usually after you've
* finished some task. */
async basicConsume(params, cb) {
const data = await this._invoke(codec_1.Cmd.BasicConsume, codec_1.Cmd.BasicConsumeOK, { ...params, rsvp1: 0, nowait: false });
const consumerTag = data.consumerTag;
this._state.consumers.set(consumerTag, cb);
return { consumerTag };
}
async basicCancel(params) {
if (typeof params == 'string') {
params = { consumerTag: params };
}
if (params.consumerTag == null)
throw new TypeError('consumerTag is undefined; expected a string');
// note: server may send a few messages before basic.cancel-ok is returned
const res = await this._invoke(codec_1.Cmd.BasicCancel, codec_1.Cmd.BasicCancelOK, { ...params, nowait: false });
this._state.consumers.delete(params.consumerTag);
return res;
}
/**
* This method sets the channel to use publisher acknowledgements.
* https://www.rabbitmq.com/confirms.html#publisher-confirms
*/
async confirmSelect() {
await this._invoke(codec_1.Cmd.ConfirmSelect, codec_1.Cmd.ConfirmSelectOK, { nowait: false });
this._state.mode = CH_MODE.CONFIRM;
}
/**
* Don't use this unless you know what you're doing. This method is provided
* for the sake of completeness, but you should use `confirmSelect()` instead.
*
* Sets the channel to use standard transactions. The client must use this
* method at least once on a channel before using the Commit or Rollback
* methods. Mutually exclusive with confirm mode.
*/
async txSelect() {
await this._invoke(codec_1.Cmd.TxSelect, codec_1.Cmd.TxSelectOK, undefined);
this._state.mode = CH_MODE.TRANSACTION;
}
queueDeclare(params = '') {
if (typeof params == 'string') {
params = { queue: params };
}
return this._invoke(codec_1.Cmd.QueueDeclare, codec_1.Cmd.QueueDeclareOK, { ...params, rsvp1: 0, nowait: false });
}
/** Acknowledge one or more messages. */
basicAck(params) {
return this._invokeNowait(codec_1.Cmd.BasicAck, params);
}
basicGet(params = '') {
if (typeof params == 'string') {
params = { queue: params };
}
return this._invoke(codec_1.Cmd.BasicGet, codec_1.Cmd.BasicGetOK, { ...params, rsvp1: 0 });
}
/** Reject one or more incoming messages. */
basicNack(params) {
this._invokeNowait(codec_1.Cmd.BasicNack, { ...params, requeue: typeof params.requeue == 'undefined' ? true : params.requeue });
}
/** Specify quality of service. */
async basicQos(params) {
await this._invoke(codec_1.Cmd.BasicQos, codec_1.Cmd.BasicQosOK, params);
}
/**
* This method asks the server to redeliver all unacknowledged messages on a
* specified channel. Zero or more messages may be redelivered.
*/
async basicRecover(params) {
await this._invoke(codec_1.Cmd.BasicRecover, codec_1.Cmd.BasicRecoverOK, params);
}
/** Bind exchange to an exchange. */
async exchangeBind(params) {
if (params.destination == null)
throw new TypeError('destination is undefined; expected a string');
if (params.source == null)
throw new TypeError('source is undefined; expected a string');
await this._invoke(codec_1.Cmd.ExchangeBind, codec_1.Cmd.ExchangeBindOK, { ...params, rsvp1: 0, nowait: false });
}
/** Verify exchange exists, create if needed. */
async exchangeDeclare(params) {
if (params.exchange == null)
throw new TypeError('exchange is undefined; expected a string');
await this._invoke(codec_1.Cmd.ExchangeDeclare, codec_1.Cmd.ExchangeDeclareOK, { ...params, type: params.type || 'direct', rsvp1: 0, nowait: false });
}
/** Delete an exchange. */
async exchangeDelete(params) {
if (params.exchange == null)
throw new TypeError('exchange is undefined; expected a string');
await this._invoke(codec_1.Cmd.ExchangeDelete, codec_1.Cmd.ExchangeDeleteOK, { ...params, rsvp1: 0, nowait: false });
}
/** Unbind an exchange from an exchange. */
async exchangeUnbind(params) {
if (params.destination == null)
throw new TypeError('destination is undefined; expected a string');
if (params.source == null)
throw new TypeError('source is undefined; expected a string');
await this._invoke(codec_1.Cmd.ExchangeUnbind, codec_1.Cmd.ExchangeUnbindOK, { ...params, rsvp1: 0, nowait: false });
}
/**
* This method binds a queue to an exchange. Until a queue is bound it will
* not receive any messages. In a classic messaging model, store-and-forward
* queues are bound to a direct exchange and subscription queues are bound to
* a topic exchange.
*/
async queueBind(params) {
if (params.exchange == null)
throw new TypeError('exchange is undefined; expected a string');
await this._invoke(codec_1.Cmd.QueueBind, codec_1.Cmd.QueueBindOK, { ...params, nowait: false });
}
queueDelete(params = '') {
if (typeof params == 'string') {
params = { queue: params };
}
return this._invoke(codec_1.Cmd.QueueDelete, codec_1.Cmd.QueueDeleteOK, { ...params, rsvp1: 0, nowait: false });
}
queuePurge(params = '') {
if (typeof params == 'string') {
params = { queue: params };
}
return this._invoke(codec_1.Cmd.QueuePurge, codec_1.Cmd.QueuePurgeOK, { queue: params.queue, rsvp1: 0, nowait: false });
}
/** Unbind a queue from an exchange. */
async queueUnbind(params) {
if (params.exchange == null)
throw new TypeError('exchange is undefined; expected a string');
await this._invoke(codec_1.Cmd.QueueUnbind, codec_1.Cmd.QueueUnbindOK, { ...params, rsvp1: 0 });
}
/**
* This method commits all message publications and acknowledgments performed
* in the current transaction. A new transaction starts immediately after a
* commit.
*/
async txCommit() {
await this._invoke(codec_1.Cmd.TxCommit, codec_1.Cmd.TxCommitOK, undefined);
}
/**
* This method abandons all message publications and acknowledgments
* performed in the current transaction. A new transaction starts immediately
* after a rollback. Note that unacked messages will not be automatically
* redelivered by rollback; if that is required an explicit recover call
* should be issued.
*/
async txRollback() {
await this._invoke(codec_1.Cmd.TxRollback, codec_1.Cmd.TxRollbackOK, undefined);
}
}
exports.Channel = Channel;