UNPKG

rabbitmq-client

Version:
606 lines (605 loc) 25.6 kB
"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;