@cloudamqp/amqp-client
Version:
AMQP 0-9-1 client, both for browsers (WebSocket) and node (TCP Socket)
1,007 lines (949 loc) • 33.6 kB
text/typescript
import { AMQPError } from "./amqp-error.js"
import * as AMQPFrame from "./amqp-frame.js"
import { AMQPView } from "./amqp-view.js"
import { AMQPConsumer, AMQPGeneratorConsumer } from "./amqp-consumer.js"
import type { AMQPMessage } from "./amqp-message.js"
import type { AMQPBaseClient } from "./amqp-base-client.js"
import type { AMQPProperties } from "./amqp-properties.js"
/**
* Represents an AMQP Channel. Almost all actions in AMQP are performed on a Channel.
*/
export class AMQPChannel {
/** The connection this channel belongs to. */
readonly connection: AMQPBaseClient
/** Channel number. */
readonly id: number
/** Active consumers on this channel, keyed by consumer tag. */
readonly consumers = new Map<string, AMQPConsumer | AMQPGeneratorConsumer>()
private rpcQueue: Promise<unknown> = Promise.resolve(true)
private readonly rpcCallbacks: [(value?: unknown) => void, (err?: Error) => void][] = []
private readonly unconfirmedPublishes: [number, (confirmId: number) => void, (err?: Error) => void][] = []
/** Whether the channel has been closed. */
closed = false
/** @internal Monotonic confirm sequence number. */
confirmId = 0
/** @internal In-progress delivery being assembled from frames. */
delivery?: AMQPMessage
/** @internal In-progress basicGet response being assembled. */
getMessage?: AMQPMessage
/** @internal In-progress returned message being assembled. */
returned?: AMQPMessage
/** Callback for channel-level errors. */
onerror: (reason: string) => void
/**
* @param connection - The connection this channel belongs to
* @param id - ID of the channel
*/
constructor(connection: AMQPBaseClient, id: number) {
this.connection = connection
this.id = id
this.onerror = (reason: string) => {
this.logger?.error(`channel ${this.id} closed: ${reason}`)
// Propagate channel errors to the connection's onerror handler
this.connection.onerror(new AMQPError(`Channel ${this.id} closed: ${reason}`, this.connection))
}
}
private get logger() {
return this.connection.logger
}
open(): Promise<AMQPChannel> {
const channelOpen = new AMQPFrame.Writer({
bufferSize: 13,
type: AMQPFrame.Type.METHOD,
channel: this.id,
frameSize: 5,
classId: AMQPFrame.ClassId.CHANNEL,
method: AMQPFrame.ChannelMethod.OPEN,
})
channelOpen.writeUint8(0) // reserved1
channelOpen.finalize()
return this.sendRpc(channelOpen)
}
/**
* Alias for basicQos
* @param prefetchCount - max inflight messages
*/
prefetch(prefetchCount: number): Promise<void> {
return this.basicQos(prefetchCount)
}
/**
* Default handler for Returned messages
* @param message returned from server
*/
onReturn(message: AMQPMessage) {
this.logger?.error("Message returned from server", message)
}
/**
* Close the channel gracefully
* @param [reason] might be logged by the server
*/
close(reason = "", code = 200): Promise<void> {
if (this.closed) return this.rejectClosed()
this.closed = true
const frame = new AMQPFrame.Writer({
bufferSize: 512,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.CHANNEL,
method: AMQPFrame.ChannelMethod.CLOSE,
})
frame.writeUint16(code) // reply code
frame.writeShortString(reason) // reply reason
frame.writeUint16(0) // failing-class-id
frame.writeUint16(0) // failing-method-id
frame.finalize()
return this.sendRpc(frame)
}
/**
* Synchronously receive a message from a queue
* @param queue - name of the queue to poll
* @param param
* @param [param.noAck=true] - if message is removed from the server upon delivery, or have to be acknowledged
* @return - returns null if the queue is empty otherwise a single message
*/
basicGet(queue: string, { noAck = true } = {}): Promise<AMQPMessage | null> {
if (this.closed) return this.rejectClosed()
const frame = new AMQPFrame.Writer({
bufferSize: 512,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.BASIC,
method: AMQPFrame.BasicMethod.GET,
})
frame.writeUint16(0) // reserved1
frame.writeShortString(queue)
frame.writeUint8(noAck ? 1 : 0)
frame.finalize()
return this.sendRpc(frame)
}
/**
* Consume from a queue. Messages are delivered asynchronously to the callback.
* @param queue - name of the queue
* @param params - consumer options
* @param callback - called for each delivered message
*/
basicConsume(
queue: string,
params: ConsumeParams,
callback: (msg: AMQPMessage) => void | Promise<void>,
): Promise<AMQPConsumer>
/**
* Consume from a queue via an async generator at `consumer.messages`.
* @param queue - name of the queue
* @param params - consumer options
*/
basicConsume(queue: string, params: ConsumeParams): Promise<AMQPGeneratorConsumer>
basicConsume(
queue: string,
{ tag = "", noAck = true, exclusive = false, args = {} }: ConsumeParams = {},
callback?: (msg: AMQPMessage) => void | Promise<void>,
): Promise<AMQPConsumer | AMQPGeneratorConsumer> {
if (this.closed) return this.rejectClosed()
const noWait = false
const noLocal = false
const frame = new AMQPFrame.Writer({
bufferSize: 4096,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.BASIC,
method: AMQPFrame.BasicMethod.CONSUME,
})
frame.writeUint16(0) // reserved1
frame.writeShortString(queue)
frame.writeShortString(tag)
let bits = 0
if (noLocal) bits = bits | (1 << 0)
if (noAck) bits = bits | (1 << 1)
if (exclusive) bits = bits | (1 << 2)
if (noWait) bits = bits | (1 << 3)
frame.writeUint8(bits)
frame.writeTable(args)
frame.finalize()
return new Promise((resolve, reject) => {
this.sendRpc(frame)
.then((consumerTag) => {
let consumer: AMQPConsumer | AMQPGeneratorConsumer
if (callback) {
consumer = new AMQPConsumer(this, consumerTag, callback)
} else {
consumer = new AMQPGeneratorConsumer(this, consumerTag)
}
this.consumers.set(consumerTag, consumer)
resolve(consumer)
})
.catch(reject)
})
}
/**
* Cancel/stop a consumer
* @param tag - consumer tag
*/
basicCancel(tag: string): Promise<AMQPChannel> {
if (this.closed) return this.rejectClosed()
const noWait = false
const frame = new AMQPFrame.Writer({
bufferSize: 512,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.BASIC,
method: AMQPFrame.BasicMethod.CANCEL,
})
frame.writeShortString(tag)
frame.writeUint8(noWait ? 1 : 0)
frame.finalize()
return new Promise((resolve, reject) => {
this.sendRpc(frame)
.then((consumerTag) => {
const consumer = this.consumers.get(consumerTag)
if (consumer) {
consumer.setClosed()
this.consumers.delete(consumerTag)
}
resolve(this)
})
.catch(reject)
})
}
/**
* Acknowledge a delivered message
* @param deliveryTag - tag of the message
* @param [multiple=false] - batch confirm all messages up to this delivery tag
*/
basicAck(deliveryTag: number, multiple = false): Promise<void> {
if (this.closed) return this.rejectClosed()
const frame = new AMQPFrame.Writer({
bufferSize: 21,
type: AMQPFrame.Type.METHOD,
channel: this.id,
frameSize: 13,
classId: AMQPFrame.ClassId.BASIC,
method: AMQPFrame.BasicMethod.ACK,
})
frame.writeUint64(deliveryTag)
frame.writeUint8(multiple ? 1 : 0)
frame.finalize()
return this.connection.send(frame.toUint8Array())
}
/**
* Acknowledge a delivered message
* @param deliveryTag - tag of the message
* @param [requeue=false] - if the message should be requeued or removed
* @param [multiple=false] - batch confirm all messages up to this delivery tag
*/
basicNack(deliveryTag: number, requeue = false, multiple = false): Promise<void> {
if (this.closed) return this.rejectClosed()
const frame = new AMQPFrame.Writer({
bufferSize: 21,
type: AMQPFrame.Type.METHOD,
channel: this.id,
frameSize: 13,
classId: AMQPFrame.ClassId.BASIC,
method: AMQPFrame.BasicMethod.NACK,
})
frame.writeUint64(deliveryTag)
let bits = 0
if (multiple) bits = bits | (1 << 0)
if (requeue) bits = bits | (1 << 1)
frame.writeUint8(bits)
frame.finalize()
return this.connection.send(frame.toUint8Array())
}
/**
* Acknowledge a delivered message
* @param deliveryTag - tag of the message
* @param [requeue=false] - if the message should be requeued or removed
*/
basicReject(deliveryTag: number, requeue = false): Promise<void> {
if (this.closed) return this.rejectClosed()
const frame = new AMQPFrame.Writer({
bufferSize: 21,
type: AMQPFrame.Type.METHOD,
channel: this.id,
frameSize: 13,
classId: AMQPFrame.ClassId.BASIC,
method: AMQPFrame.BasicMethod.REJECT,
})
frame.writeUint64(deliveryTag)
frame.writeUint8(requeue ? 1 : 0)
frame.finalize()
return this.connection.send(frame.toUint8Array())
}
/**
* Tell the server to redeliver all unacknowledged messages again, or reject and requeue them.
* @param [requeue=false] - if the message should be requeued or redeliviered to this channel
*/
basicRecover(requeue = false): Promise<void> {
if (this.closed) return this.rejectClosed()
const frame = new AMQPFrame.Writer({
bufferSize: 13,
type: AMQPFrame.Type.METHOD,
channel: this.id,
frameSize: 5,
classId: AMQPFrame.ClassId.BASIC,
method: AMQPFrame.BasicMethod.RECOVER,
})
frame.writeUint8(requeue ? 1 : 0)
frame.finalize()
return this.sendRpc(frame)
}
/**
* Publish a message
* @param exchange - the exchange to publish to, the exchange must exists
* @param routingKey - routing key
* @param data - the data to be published, can be a string or an uint8array
* @param properties - properties to be published
* @param [mandatory] - if the message should be returned if there's no queue to be delivered to
* @param [immediate] - if the message should be returned if it can't be delivered to a consumer immediately (not supported in RabbitMQ)
* @return - fulfilled when the message is enqueue on the socket, or if publish confirm is enabled when the message is confirmed by the server
*/
async basicPublish(
exchange: string,
routingKey: string,
data: string | Uint8Array | ArrayBuffer | Buffer | null,
properties: AMQPProperties = {},
mandatory = false,
immediate = false,
): Promise<number> {
if (this.closed) return this.rejectClosed()
if (this.connection.blocked)
return Promise.reject(new AMQPError(`Connection blocked by server: ${this.connection.blocked}`, this.connection))
let body: Uint8Array
if (typeof Buffer !== "undefined" && data instanceof Buffer) {
body = data
} else if (data instanceof Uint8Array) {
body = data
} else if (data instanceof ArrayBuffer) {
body = new Uint8Array(data)
} else if (data === null) {
body = new Uint8Array(0)
} else if (typeof data === "string") {
body = this.connection.textEncoder.encode(data)
} else {
throw new TypeError(`Invalid type ${typeof data} for parameter data`)
}
let j = 0
// get a buffer from the pool or create a new, it will later be returned to the pool for reuse
let buffer = this.connection.bufferPool.pop() || new AMQPView(new ArrayBuffer(this.connection.frameMax))
buffer.setUint8(j, AMQPFrame.Type.METHOD)
j += 1
buffer.setUint16(j, this.id)
j += 2 // channel
j += 4 // frame size, update later
buffer.setUint16(j, AMQPFrame.ClassId.BASIC)
j += 2
buffer.setUint16(j, AMQPFrame.BasicMethod.PUBLISH)
j += 2
buffer.setUint16(j, 0)
j += 2 // reserved1
j += buffer.setShortString(j, exchange)
j += buffer.setShortString(j, routingKey)
let bits = 0
if (mandatory) bits = bits | (1 << 0)
if (immediate) bits = bits | (1 << 1)
buffer.setUint8(j, bits)
j += 1
buffer.setUint8(j, AMQPFrame.End.CODE)
j += 1
buffer.setUint32(3, j - 8) // update frameSize
const headerStart = j
buffer.setUint8(j, AMQPFrame.Type.HEADER)
j += 1
buffer.setUint16(j, this.id)
j += 2 // channel
j += 4 // frame size, update later
buffer.setUint16(j, AMQPFrame.ClassId.BASIC)
j += 2
buffer.setUint16(j, 0)
j += 2 // weight
buffer.setUint32(j, 0)
j += 4 // bodysize (upper 32 of 64 bits)
buffer.setUint32(j, body.byteLength)
j += 4 // bodysize
j += buffer.setProperties(j, properties)
buffer.setUint8(j, AMQPFrame.End.CODE)
j += 1
buffer.setUint32(headerStart + 3, j - headerStart - 8) // update frameSize
let bufferView = new Uint8Array(buffer.buffer)
const bodyFrameCount = Math.ceil(body.byteLength / (this.connection.frameMax - 8))
const bufferSize = j + body.byteLength + 8 * bodyFrameCount
if (buffer.byteLength < bufferSize) {
// the body doesn't fit in the buffer, expand it
const newBuffer = new ArrayBuffer(bufferSize)
const newBufferView = new Uint8Array(newBuffer)
newBufferView.set(bufferView.subarray(0, j))
buffer = new AMQPView(newBuffer)
bufferView = newBufferView
}
// split body into multiple frames if body > frameMax
for (let bodyPos = 0; bodyPos < body.byteLength; ) {
const frameSize = Math.min(body.byteLength - bodyPos, this.connection.frameMax - 8) // frame overhead is 8 bytes
const dataSlice = body.subarray(bodyPos, bodyPos + frameSize)
buffer.setUint8(j, AMQPFrame.Type.BODY)
j += 1
buffer.setUint16(j, this.id)
j += 2 // channel
buffer.setUint32(j, frameSize)
j += 4
bufferView.set(dataSlice, j)
j += frameSize // body content
buffer.setUint8(j, AMQPFrame.End.CODE)
j += 1
bodyPos += frameSize
}
const sendFrames = this.connection.send(bufferView.subarray(0, j))
// return buffer to buffer pool for later reuse
// if publish confirm is enabled, put a promise on a queue if the sends were ok
// the promise on the queue will be fullfilled by the read loop when an ack/nack
// comes from the server
if (this.confirmId) {
const wait4Confirm = new Promise<number>((resolve, reject) =>
this.unconfirmedPublishes.push([this.confirmId++, resolve, reject]),
)
return sendFrames.then(() => wait4Confirm).finally(() => this.connection.bufferPool.push(buffer))
} else {
return sendFrames.then(() => 0).finally(() => this.connection.bufferPool.push(buffer))
}
}
/**
* Set prefetch limit.
* Recommended to set as each unacknowledged message will be stored in memory of the client.
* The server won't deliver more messages than the limit until messages are acknowledged.
* @param prefetchCount - number of messages to limit to
* @param prefetchSize - number of bytes to limit to (not supported by RabbitMQ)
* @param global - if the prefetch is limited to the channel, or if false to each consumer
*/
basicQos(prefetchCount: number, prefetchSize = 0, global = false): Promise<void> {
if (this.closed) return this.rejectClosed()
const frame = new AMQPFrame.Writer({
bufferSize: 19,
type: AMQPFrame.Type.METHOD,
channel: this.id,
frameSize: 11,
classId: AMQPFrame.ClassId.BASIC,
method: AMQPFrame.BasicMethod.QOS,
})
frame.writeUint32(prefetchSize)
frame.writeUint16(prefetchCount)
frame.writeUint8(global ? 1 : 0)
frame.finalize()
return this.sendRpc(frame)
}
/**
* Enable or disable flow. Disabling flow will stop the server from delivering messages to consumers.
* Not supported in RabbitMQ
* @param active - false to stop the flow, true to accept messages
*/
basicFlow(active = true): Promise<boolean> {
if (this.closed) return this.rejectClosed()
const frame = new AMQPFrame.Writer({
bufferSize: 13,
type: AMQPFrame.Type.METHOD,
channel: this.id,
frameSize: 5,
classId: AMQPFrame.ClassId.CHANNEL,
method: AMQPFrame.BasicMethod.CONSUME,
})
frame.writeUint8(active ? 1 : 0) // active flow
frame.finalize()
return this.sendRpc(frame)
}
/**
* Enable publish confirm. The server will then confirm each publish with an Ack or Nack when the message is enqueued.
*/
confirmSelect(): Promise<void> {
if (this.closed) return this.rejectClosed()
const frame = new AMQPFrame.Writer({
bufferSize: 13,
type: AMQPFrame.Type.METHOD,
channel: this.id,
frameSize: 5,
classId: AMQPFrame.ClassId.CONFIRM,
method: AMQPFrame.ConfirmMethod.SELECT,
})
frame.writeUint8(0) // noWait
frame.finalize()
return this.sendRpc(frame) // parseFrames in base will set channel.confirmId = 0
}
/**
* Declare a queue
* @param name - name of the queue, if empty the server will generate a name
* @param params
* @param [params.passive=false] - if the queue name doesn't exists the channel will be closed with an error, fulfilled if the queue name does exists
* @param [params.durable=true] - if the queue should survive server restarts
* @param [params.autoDelete=false] - if the queue should be deleted when the last consumer of the queue disconnects
* @param [params.exclusive=false] - if the queue should be deleted when the channel is closed
* @param args - optional custom queue arguments
* @return fulfilled when confirmed by the server
*/
queueDeclare(
name = "",
{ passive = false, durable = name !== "", autoDelete = name === "", exclusive = name === "" }: QueueParams = {},
args = {},
): Promise<QueueOk> {
if (this.closed) return this.rejectClosed()
const noWait = false
const declare = new AMQPFrame.Writer({
bufferSize: 4096,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.QUEUE,
method: AMQPFrame.QueueMethod.DECLARE,
})
declare.writeUint16(0) // reserved1
declare.writeShortString(name)
let bits = 0
if (passive) bits = bits | (1 << 0)
if (durable) bits = bits | (1 << 1)
if (exclusive) bits = bits | (1 << 2)
if (autoDelete) bits = bits | (1 << 3)
if (noWait) bits = bits | (1 << 4)
declare.writeUint8(bits)
declare.writeTable(args)
declare.finalize()
return this.sendRpc(declare)
}
/**
* Delete a queue
* @param name - name of the queue, if empty it will delete the last declared queue
* @param params
* @param [params.ifUnused=false] - only delete if the queue doesn't have any consumers
* @param [params.ifEmpty=false] - only delete if the queue is empty
*/
queueDelete(name = "", { ifUnused = false, ifEmpty = false } = {}): Promise<MessageCount> {
if (this.closed) return this.rejectClosed()
const noWait = false
const frame = new AMQPFrame.Writer({
bufferSize: 512,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.QUEUE,
method: AMQPFrame.QueueMethod.DELETE,
})
frame.writeUint16(0) // reserved1
frame.writeShortString(name)
let bits = 0
if (ifUnused) bits = bits | (1 << 0)
if (ifEmpty) bits = bits | (1 << 1)
if (noWait) bits = bits | (1 << 2)
frame.writeUint8(bits)
frame.finalize()
return this.sendRpc(frame)
}
/**
* Bind a queue to an exchange
* @param queue - name of the queue
* @param exchange - name of the exchange
* @param routingKey - key to bind with
* @param args - optional arguments, e.g. for header exchanges
* @return fulfilled when confirmed by the server
*/
queueBind(queue: string, exchange: string, routingKey: string, args = {}): Promise<void> {
if (this.closed) return this.rejectClosed()
const noWait = false
const bind = new AMQPFrame.Writer({
bufferSize: 4096,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.QUEUE,
method: AMQPFrame.BasicMethod.CONSUME,
})
bind.writeUint16(0) // reserved1
bind.writeShortString(queue)
bind.writeShortString(exchange)
bind.writeShortString(routingKey)
bind.writeUint8(noWait ? 1 : 0)
bind.writeTable(args)
bind.finalize()
return this.sendRpc(bind)
}
/**
* Unbind a queue from an exchange
* @param queue - name of the queue
* @param exchange - name of the exchange
* @param routingKey - key that was bound
* @param args - arguments, e.g. for header exchanges
* @return fulfilled when confirmed by the server
*/
queueUnbind(queue: string, exchange: string, routingKey: string, args = {}): Promise<void> {
if (this.closed) return this.rejectClosed()
const unbind = new AMQPFrame.Writer({
bufferSize: 4096,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.QUEUE,
method: AMQPFrame.QueueMethod.UNBIND,
})
unbind.writeUint16(0) // reserved1
unbind.writeShortString(queue)
unbind.writeShortString(exchange)
unbind.writeShortString(routingKey)
unbind.writeTable(args)
unbind.finalize()
return this.sendRpc(unbind)
}
/**
* Purge a queue
* @param queue - name of the queue
* @return fulfilled when confirmed by the server
*/
queuePurge(queue: string): Promise<MessageCount> {
if (this.closed) return this.rejectClosed()
const noWait = false
const purge = new AMQPFrame.Writer({
bufferSize: 512,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.QUEUE,
method: AMQPFrame.BasicMethod.CANCEL,
})
purge.writeUint16(0) // reserved1
purge.writeShortString(queue)
purge.writeUint8(noWait ? 1 : 0)
purge.finalize()
return this.sendRpc(purge)
}
/**
* Declare an exchange
* @param name - name of the exchange
* @param type - type of exchange (direct, fanout, topic, header, or a custom type)
* @param param
* @param [param.passive=false] - if the exchange name doesn't exists the channel will be closed with an error, fulfilled if the exchange name does exists
* @param [param.durable=true] - if the exchange should survive server restarts
* @param [param.autoDelete=false] - if the exchange should be deleted when the last binding from it is deleted
* @param [param.internal=false] - if exchange is internal to the server. Client's can't publish to internal exchanges.
* @param args - optional arguments
* @return Fulfilled when the exchange is created or if it already exists
*/
exchangeDeclare(
name: string,
type: ExchangeType,
{ passive = false, durable = true, autoDelete = false, internal = false }: ExchangeParams = {},
args = {},
): Promise<void> {
if (this.closed) return this.rejectClosed()
const noWait = false
const frame = new AMQPFrame.Writer({
bufferSize: 4096,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.EXCHANGE,
method: AMQPFrame.ExchangeMethod.DECLARE,
})
frame.writeUint16(0) // reserved1
frame.writeShortString(name)
frame.writeShortString(type)
let bits = 0
if (passive) bits = bits | (1 << 0)
if (durable) bits = bits | (1 << 1)
if (autoDelete) bits = bits | (1 << 2)
if (internal) bits = bits | (1 << 3)
if (noWait) bits = bits | (1 << 4)
frame.writeUint8(bits)
frame.writeTable(args)
frame.finalize()
return this.sendRpc(frame)
}
/**
* Delete an exchange
* @param name - name of the exchange
* @param param
* @param [param.ifUnused=false] - only delete if the exchange doesn't have any bindings
* @return Fulfilled when the exchange is deleted or if it's already deleted
*/
exchangeDelete(name: string, { ifUnused = false } = {}): Promise<void> {
if (this.closed) return this.rejectClosed()
const noWait = false
const frame = new AMQPFrame.Writer({
bufferSize: 512,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.EXCHANGE,
method: AMQPFrame.BasicMethod.CONSUME,
})
frame.writeUint16(0) // reserved1
frame.writeShortString(name)
let bits = 0
if (ifUnused) bits = bits | (1 << 0)
if (noWait) bits = bits | (1 << 1)
frame.writeUint8(bits)
frame.finalize()
return this.sendRpc(frame)
}
/**
* Exchange to exchange binding.
* @param destination - name of the destination exchange
* @param source - name of the source exchange
* @param routingKey - key to bind with
* @param args - optional arguments, e.g. for header exchanges
* @return fulfilled when confirmed by the server
*/
exchangeBind(destination: string, source: string, routingKey = "", args = {}): Promise<void> {
if (this.closed) return this.rejectClosed()
const bind = new AMQPFrame.Writer({
bufferSize: 4096,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.EXCHANGE,
method: AMQPFrame.BasicMethod.CANCEL,
})
bind.writeUint16(0) // reserved1
bind.writeShortString(destination)
bind.writeShortString(source)
bind.writeShortString(routingKey)
bind.writeUint8(0)
bind.writeTable(args)
bind.finalize()
return this.sendRpc(bind)
}
/**
* Delete an exchange-to-exchange binding
* @param destination - name of destination exchange
* @param source - name of the source exchange
* @param routingKey - key that was bound
* @param args - arguments, e.g. for header exchanges
* @return fulfilled when confirmed by the server
*/
exchangeUnbind(destination: string, source: string, routingKey = "", args = {}): Promise<void> {
if (this.closed) return this.rejectClosed()
const unbind = new AMQPFrame.Writer({
bufferSize: 4096,
type: AMQPFrame.Type.METHOD,
channel: this.id,
classId: AMQPFrame.ClassId.EXCHANGE,
method: AMQPFrame.ExchangeMethod.UNBIND,
})
unbind.writeUint16(0) // reserved1
unbind.writeShortString(destination)
unbind.writeShortString(source)
unbind.writeShortString(routingKey)
unbind.writeUint8(0)
unbind.writeTable(args)
unbind.finalize()
return this.sendRpc(unbind)
}
/**
* Set this channel in Transaction mode.
* Rember to commit the transaction, overwise the server will eventually run out of memory.
*/
txSelect() {
return this.txMethod(10)
}
/**
* Commit a transaction
*/
txCommit() {
return this.txMethod(20)
}
/**
* Rollback a transaction
*/
txRollback() {
return this.txMethod(30)
}
private txMethod(methodId: number): Promise<void> {
if (this.closed) return this.rejectClosed()
const frame = new AMQPFrame.Writer({
bufferSize: 12,
type: AMQPFrame.Type.METHOD,
channel: this.id,
frameSize: 4,
classId: AMQPFrame.ClassId.TX,
method: methodId,
})
frame.finalize()
return this.sendRpc(frame)
}
/**
* Send a RPC request, will resolve a RPC promise when RPC response arrives
* @param frame - AMQP.AMQPFrame to send
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private sendRpc(frame: AMQPFrame.Writer): Promise<any> {
const bytes = frame.toUint8Array()
return new Promise((resolve, reject) => {
this.rpcQueue = this.rpcQueue
.then(() => {
// Add the callbacks to the queue before sending
this.rpcCallbacks.push([resolve, reject])
return this.connection.send(bytes).catch((err) => {
// Remove the callbacks from the queue if send fails
const callbacks = this.rpcCallbacks.pop()
if (callbacks) {
callbacks[1](err) // call reject
} else {
reject(err)
}
})
})
.catch(reject)
})
}
/**
* Marks the channel as closed
* All outstanding RPC requests will be rejected
* All outstanding publish confirms will be rejected
* All consumers will be marked as closed
* @ignore
* @param [err] - why the channel was closed
*/
setClosed(err?: Error): void {
const closedByServer = err !== undefined
err ||= new Error("Connection closed by client")
if (!this.closed) {
this.closed = true
// On client-initiated close, consumers resolve cleanly (no error).
// Only propagate the error when the server closed the channel/connection.
this.consumers.forEach((consumer) => consumer.setClosed(closedByServer ? err : undefined))
this.consumers.clear()
// Reject all pending RPC callbacks
this.rpcCallbacks.forEach(([, reject]) => reject(err))
this.rpcCallbacks.length = 0
// Reject and clear all unconfirmed publishes
this.unconfirmedPublishes.forEach(([, , reject]) => reject(err))
this.unconfirmedPublishes.length = 0
if (closedByServer) this.onerror(err.message)
}
}
/**
* @return Rejected promise with an error
*/
private rejectClosed() {
return Promise.reject(new AMQPError("Channel is closed", this.connection))
}
/**
* Called from AMQPBaseClient when a publish is confirmed by the server.
* Will fulfill one or more (if multiple) Unconfirmed Publishes.
* @ignore
* @param deliveryTag
* @param multiple - true if all unconfirmed publishes up to this deliveryTag should be resolved or just this one
* @param nack - true if negative confirm, hence reject the unconfirmed publish(es)
*/
publishConfirmed(deliveryTag: number, multiple: boolean, nack: boolean): void {
// is queueMicrotask() needed here?
const idx = this.unconfirmedPublishes.findIndex(([tag]) => tag === deliveryTag)
if (idx !== -1) {
const confirmed = multiple
? this.unconfirmedPublishes.splice(0, idx + 1)
: this.unconfirmedPublishes.splice(idx, 1)
confirmed.forEach(([tag, resolve, reject]) => {
if (nack) reject(new Error("Message rejected"))
else resolve(tag)
})
} else {
this.logger?.warn("Cant find unconfirmed deliveryTag", deliveryTag, "multiple:", multiple, "nack:", nack)
}
}
/**
* Called from AMQPBaseClient when a message is ready
* @ignore
* @param message
*/
onMessageReady(message: AMQPMessage): void {
message.body = message._rawBytes
if (this.delivery) {
delete this.delivery
this.deliver(message)
} else if (this.getMessage) {
delete this.getMessage
this.resolveRPC(message)
} else {
delete this.returned
this.onReturn(message)
}
}
/**
* Resolvs next RPC command
* @ignore
*/
resolveRPC(value?: unknown): unknown | void {
const callbacks = this.rpcCallbacks.shift()
if (callbacks) {
callbacks[0](value) // call resolve
}
return value
}
/**
* Reject next RPC command
* @ignore
*/
rejectRPC(err?: Error): Error | void {
const callbacks = this.rpcCallbacks.shift()
if (callbacks) {
callbacks[1](err) // call reject
}
return err
}
/**
* Deliver a message to a consumer
* @ignore
*/
deliver(message: AMQPMessage): void {
queueMicrotask(() => {
const consumer = this.consumers.get(message.consumerTag)
if (consumer) {
consumer.onMessage(message)
} else {
this.logger?.warn("Consumer", message.consumerTag, "not available on channel", this.id)
}
})
}
}
export type QueueOk = {
name: string
messageCount: number
consumerCount: number
}
export type MessageCount = {
messageCount: number
}
export type ExchangeType = "direct" | "fanout" | "topic" | "headers" | string
export type ExchangeParams = {
/**
* if the exchange name doesn't exist the channel will be closed with an error, fulfilled if the exchange name does exists
*/
passive?: boolean
/**
* if the exchange should survive server restarts
*/
durable?: boolean
/**
* if the exchange should be deleted when the last binding from it is deleted
*/
autoDelete?: boolean
/**
* if exchange is internal to the server. Client's can't publish to internal exchanges.
*/
internal?: boolean
}
export type QueueParams = {
/**
* if the queue name doesn't exist the channel will be closed with an error, fulfilled if the queue name does exists
*/
passive?: boolean
/**
* if the queue should survive server restarts
*/
durable?: boolean
/**
* if the queue should be deleted when the last consumer of the queue disconnects
*/
autoDelete?: boolean
/**
* if the queue should be deleted when the channel is closed
*/
exclusive?: boolean
}
export type ConsumeParams = {
/** Consumer tag. Server-generated if empty.
* @defaultValue `""` */
tag?: string
/** If true, messages are auto-acknowledged on delivery.
* @defaultValue `true` */
noAck?: boolean
/** Fail if other consumers already exist on the queue.
* @defaultValue `false` */
exclusive?: boolean
/** Custom arguments.
* @defaultValue `{}` */
args?: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
}