@jonaskello-forks/amqp-client
Version:
AMQP 0-9-1 client, both for browsers (WebSocket) and node (TCP Socket)
585 lines (563 loc) • 24.5 kB
text/typescript
import { AMQPChannel } from './amqp-channel.js'
import { AMQPError } from './amqp-error.js'
import { AMQPMessage } from './amqp-message.js'
import { AMQPView } from './amqp-view.js'
const VERSION = '1.3.2'
/**
* Base class for AMQPClients.
* Implements everything except how to connect, send data and close the socket
*/
export abstract class AMQPBaseClient {
vhost: string
username: string
password: string
name?: string
platform?: string
channels: AMQPChannel[]
protected connectPromise?: [(conn: AMQPBaseClient) => void, (err: Error) => void]
protected closePromise?: [(value?: void) => void, (err: Error) => void]
closed = false
blocked?: string
channelMax = 0
frameMax: number
heartbeat: number
/**
* @param name - name of the connection, set in client properties
* @param platform - used in client properties
*/
constructor(vhost: string, username: string, password: string, name?: string, platform?: string, frameMax = 4096, heartbeat = 0) {
this.vhost = vhost
this.username = username
this.password = ""
Object.defineProperty(this, 'password', {
value: password,
enumerable: false // hide it from console.log etc.
})
if (name) this.name = name // connection name
if (platform) this.platform = platform
this.channels = [new AMQPChannel(this, 0)]
this.closed = false
if (frameMax < 4096) throw new Error("frameMax must be 4096 or larger")
this.frameMax = frameMax
if (heartbeat < 0) throw new Error("heartbeat must be positive")
this.heartbeat = heartbeat
}
/**
* Open a channel
* @param [id] - An existing or non existing specific channel
*/
channel(id?: number): Promise<AMQPChannel> {
if (this.closed) return this.rejectClosed()
if (id && id > 0) {
const channel = this.channels[id]
if (channel) return Promise.resolve(channel)
}
// Store channels in an array, set position to null when channel is closed
// Look for first null value or add one the end
if (!id)
id = this.channels.findIndex((ch) => ch === undefined)
if (id === -1) id = this.channels.length
// FIXME: check max channels (or let the server deal with that?)
const channel = new AMQPChannel(this, id)
this.channels[id] = channel
let j = 0
const channelOpen = new AMQPView(new ArrayBuffer(13))
channelOpen.setUint8(j, 1); j += 1 // type: method
channelOpen.setUint16(j, id); j += 2 // channel id
channelOpen.setUint32(j, 5); j += 4 // frameSize
channelOpen.setUint16(j, 20); j += 2 // class: channel
channelOpen.setUint16(j, 10); j += 2 // method: open
channelOpen.setUint8(j, 0); j += 1 // reserved1
channelOpen.setUint8(j, 206); j += 1 // frame end byte
return new Promise((resolve, reject) => {
this.send(new Uint8Array(channelOpen.buffer, 0, 13))
.then(() => channel.promises.push([resolve, reject]))
.catch(reject)
})
}
/**
* Gracefully close the AMQP connection
* @param [reason] might be logged by the server
*/
close(reason = "", code = 200) {
if (this.closed) return this.rejectClosed()
this.closed = true
let j = 0
const frame = new AMQPView(new ArrayBuffer(512))
frame.setUint8(j, 1); j += 1 // type: method
frame.setUint16(j, 0); j += 2 // channel: 0
frame.setUint32(j, 0); j += 4 // frameSize
frame.setUint16(j, 10); j += 2 // class: connection
frame.setUint16(j, 50); j += 2 // method: close
frame.setUint16(j, code); j += 2 // reply code
j += frame.setShortString(j, reason) // reply reason
frame.setUint16(j, 0); j += 2 // failing-class-id
frame.setUint16(j, 0); j += 2 // failing-method-id
frame.setUint8(j, 206); j += 1 // frame end byte
frame.setUint32(3, j - 8) // update frameSize
return new Promise((resolve, reject) => {
this.send(new Uint8Array(frame.buffer, 0, j))
.then(() => this.closePromise = [resolve, reject])
.catch(reject)
})
}
/**
* Try establish a connection
*/
abstract connect(): Promise<AMQPBaseClient>
/**
* @ignore
* @param bytes to send
* @return fulfilled when the data is enqueued
*/
abstract send(bytes: Uint8Array): Promise<void>
protected abstract closeSocket(): void
private rejectClosed() {
return Promise.reject(new AMQPError("Connection closed", this))
}
private rejectConnect(err: Error) {
if (this.connectPromise) {
const [, reject] = this.connectPromise
delete this.connectPromise
reject(err)
}
this.closed = true
this.closeSocket()
}
/**
* Parse and act on frames in an AMQPView
* @ignore
*/
protected parseFrames(view: AMQPView) {
// Can possibly be multiple AMQP frames in a single WS frame
for (let i = 0; i < view.byteLength;) {
let j = 0 // position in outgoing frame
const type = view.getUint8(i); i += 1
const channelId = view.getUint16(i); i += 2
const frameSize = view.getUint32(i); i += 4
try {
const frameEnd = view.getUint8(i + frameSize)
if (frameEnd !== 206)
throw(new AMQPError(`Invalid frame end ${frameEnd}, expected 206`, this))
} catch (e) {
throw(new AMQPError(`Frame end out of range, frameSize=${frameSize}, pos=${i}, byteLength=${view.byteLength}`, this))
}
const channel = this.channels[channelId]
if (!channel) {
console.warn("AMQP channel", channelId, "not open")
i += frameSize + 1
continue
}
switch (type) {
case 1: { // method
const classId = view.getUint16(i); i += 2
const methodId = view.getUint16(i); i += 2
switch (classId) {
case 10: { // connection
switch (methodId) {
case 10: { // start
// ignore start frame, just reply startok
i += frameSize - 4
const startOk = new AMQPView(new ArrayBuffer(4096))
startOk.setUint8(j, 1); j += 1 // type: method
startOk.setUint16(j, 0); j += 2 // channel: 0
startOk.setUint32(j, 0); j += 4 // frameSize: to be updated
startOk.setUint16(j, 10); j += 2 // class: connection
startOk.setUint16(j, 11); j += 2 // method: startok
const clientProps = {
connection_name: this.name || undefined,
product: "amqp-client.js",
information: "https://github.com/cloudamqp/amqp-client.js",
version: VERSION,
platform: this.platform,
capabilities: {
"authentication_failure_close": true,
"basic.nack": true,
"connection.blocked": true,
"consumer_cancel_notify": true,
"exchange_exchange_bindings": true,
"per_consumer_qos": true,
"publisher_confirms": true,
}
}
j += startOk.setTable(j, clientProps) // client properties
j += startOk.setShortString(j, "PLAIN") // mechanism
const response = `\u0000${this.username}\u0000${this.password}`
j += startOk.setLongString(j, response) // response
j += startOk.setShortString(j, "") // locale
startOk.setUint8(j, 206); j += 1 // frame end byte
startOk.setUint32(3, j - 8) // update frameSize
this.send(new Uint8Array(startOk.buffer, 0, j)).catch(this.rejectConnect)
break
}
case 30: { // tune
const channelMax = view.getUint16(i); i += 2
const frameMax = view.getUint32(i); i += 4
const heartbeat = view.getUint16(i); i += 2
this.channelMax = channelMax
this.frameMax = this.frameMax === 0 ? frameMax : Math.min(this.frameMax, frameMax)
this.heartbeat = this.heartbeat === 0 ? 0 : Math.min(this.heartbeat, heartbeat)
const tuneOk = new AMQPView(new ArrayBuffer(20))
tuneOk.setUint8(j, 1); j += 1 // type: method
tuneOk.setUint16(j, 0); j += 2 // channel: 0
tuneOk.setUint32(j, 12); j += 4 // frameSize: 12
tuneOk.setUint16(j, 10); j += 2 // class: connection
tuneOk.setUint16(j, 31); j += 2 // method: tuneok
tuneOk.setUint16(j, this.channelMax); j += 2 // channel max
tuneOk.setUint32(j, this.frameMax); j += 4 // frame max
tuneOk.setUint16(j, this.heartbeat); j += 2 // heartbeat
tuneOk.setUint8(j, 206); j += 1 // frame end byte
this.send(new Uint8Array(tuneOk.buffer, 0, j)).catch(this.rejectConnect)
j = 0
const open = new AMQPView(new ArrayBuffer(512))
open.setUint8(j, 1); j += 1 // type: method
open.setUint16(j, 0); j += 2 // channel: 0
open.setUint32(j, 0); j += 4 // frameSize: to be updated
open.setUint16(j, 10); j += 2 // class: connection
open.setUint16(j, 40); j += 2 // method: open
j += open.setShortString(j, this.vhost) // vhost
open.setUint8(j, 0); j += 1 // reserved1
open.setUint8(j, 0); j += 1 // reserved2
open.setUint8(j, 206); j += 1 // frame end byte
open.setUint32(3, j - 8) // update frameSize
this.send(new Uint8Array(open.buffer, 0, j)).catch(this.rejectConnect)
break
}
case 41: { // openok
i += 1 // reserved1
const promise = this.connectPromise
if (promise) {
const [resolve, ] = promise
delete this.connectPromise
resolve(this)
}
break
}
case 50: { // close
const code = view.getUint16(i); i += 2
const [text, strLen] = view.getShortString(i); i += strLen
const classId = view.getUint16(i); i += 2
const methodId = view.getUint16(i); i += 2
console.debug("connection closed by server", code, text, classId, methodId)
const msg = `connection closed: ${text} (${code})`
const err = new AMQPError(msg, this)
this.channels.forEach((ch) => ch.setClosed(err))
this.channels = [new AMQPChannel(this, 0)]
const closeOk = new AMQPView(new ArrayBuffer(12))
closeOk.setUint8(j, 1); j += 1 // type: method
closeOk.setUint16(j, 0); j += 2 // channel: 0
closeOk.setUint32(j, 4); j += 4 // frameSize
closeOk.setUint16(j, 10); j += 2 // class: connection
closeOk.setUint16(j, 51); j += 2 // method: closeok
closeOk.setUint8(j, 206); j += 1 // frame end byte
this.send(new Uint8Array(closeOk.buffer, 0, j))
.catch(err => console.warn("Error while sending Connection#CloseOk", err))
this.rejectConnect(err)
break
}
case 51: { // closeOk
this.channels.forEach((ch) => ch.setClosed())
this.channels = [new AMQPChannel(this, 0)]
const promise = this.closePromise
if (promise) {
const [resolve, ] = promise
delete this.closePromise
resolve()
this.closeSocket()
}
break
}
case 60: { // blocked
const [reason, len] = view.getShortString(i); i += len
console.warn("AMQP connection blocked:", reason)
this.blocked = reason
break
}
case 61: { // unblocked
console.info("AMQP connection unblocked")
delete this.blocked
break
}
default:
i += frameSize - 4
console.error("unsupported class/method id", classId, methodId)
}
break
}
case 20: { // channel
switch (methodId) {
case 11: { // openok
i += 4 // reserved1 (long string)
channel.resolvePromise(channel)
break
}
case 21: { // flowOk
const active = view.getUint8(i) !== 0; i += 1
channel.resolvePromise(active)
break
}
case 40: { // close
const code = view.getUint16(i); i += 2
const [text, strLen] = view.getShortString(i); i += strLen
const classId = view.getUint16(i); i += 2
const methodId = view.getUint16(i); i += 2
console.debug("channel", channelId, "closed", code, text, classId, methodId)
const msg = `channel ${channelId} closed: ${text} (${code})`
const err = new AMQPError(msg, this)
channel.setClosed(err)
delete this.channels[channelId]
const closeOk = new AMQPView(new ArrayBuffer(12))
closeOk.setUint8(j, 1); j += 1 // type: method
closeOk.setUint16(j, channelId); j += 2 // channel
closeOk.setUint32(j, 4); j += 4 // frameSize
closeOk.setUint16(j, 20); j += 2 // class: channel
closeOk.setUint16(j, 41); j += 2 // method: closeok
closeOk.setUint8(j, 206); j += 1 // frame end byte
this.send(new Uint8Array(closeOk.buffer, 0, j))
.catch(err => console.error("Error while sending Channel#closeOk", err))
break
}
case 41: { // closeOk
channel.setClosed()
delete this.channels[channelId]
channel.resolvePromise()
break
}
default:
i += frameSize - 4 // skip rest of frame
console.error("unsupported class/method id", classId, methodId)
}
break
}
case 40: { // exchange
switch (methodId) {
case 11: // declareOk
case 21: // deleteOk
case 31: // bindOk
case 51: { // unbindOk
channel.resolvePromise()
break
}
default:
i += frameSize - 4 // skip rest of frame
console.error("unsupported class/method id", classId, methodId)
}
break
}
case 50: { // queue
switch (methodId) {
case 11: { // declareOk
const [name, strLen] = view.getShortString(i); i += strLen
const messageCount = view.getUint32(i); i += 4
const consumerCount = view.getUint32(i); i += 4
channel.resolvePromise({ name, messageCount, consumerCount })
break
}
case 21: { // bindOk
channel.resolvePromise()
break
}
case 31: { // purgeOk
const messageCount = view.getUint32(i); i += 4
channel.resolvePromise({ messageCount })
break
}
case 41: { // deleteOk
const messageCount = view.getUint32(i); i += 4
channel.resolvePromise({ messageCount })
break
}
case 51: { // unbindOk
channel.resolvePromise()
break
}
default:
i += frameSize - 4
console.error("unsupported class/method id", classId, methodId)
}
break
}
case 60: { // basic
switch (methodId) {
case 11: { // qosOk
channel.resolvePromise()
break
}
case 21: { // consumeOk
const [consumerTag, len] = view.getShortString(i); i += len
channel.resolvePromise(consumerTag)
break
}
case 30: { // cancel
const [consumerTag, len] = view.getShortString(i); i += len
const noWait = view.getUint8(i) === 1; i += 1
const consumer = channel.consumers.get(consumerTag)
if (consumer) {
consumer.setClosed(new AMQPError("Consumer cancelled by the server", this))
channel.consumers.delete(consumerTag)
}
if (!noWait) {
const frame = new AMQPView(new ArrayBuffer(512))
frame.setUint8(j, 1); j += 1 // type: method
frame.setUint16(j, channel.id); j += 2 // channel
frame.setUint32(j, 0); j += 4 // frameSize
frame.setUint16(j, 60); j += 2 // class: basic
frame.setUint16(j, 31); j += 2 // method: cancelOk
j += frame.setShortString(j, consumerTag) // tag
frame.setUint8(j, 206); j += 1 // frame end byte
frame.setUint32(3, j - 8) // update frameSize
this.send(new Uint8Array(frame.buffer, 0, j))
}
break
}
case 31: { // cancelOk
const [consumerTag, len] = view.getShortString(i); i += len
channel.resolvePromise(consumerTag)
break
}
case 50: { // return
const code = view.getUint16(i); i += 2
const [text, len] = view.getShortString(i); i += len
const [exchange, exchangeLen] = view.getShortString(i); i += exchangeLen
const [routingKey, routingKeyLen] = view.getShortString(i); i += routingKeyLen
const message = new AMQPMessage(channel)
message.exchange = exchange
message.routingKey = routingKey
message.replyCode = code
message.replyText = text
channel.returned = message
break
}
case 60: { // deliver
const [consumerTag, consumerTagLen] = view.getShortString(i); i += consumerTagLen
const deliveryTag = view.getUint64(i); i += 8
const redelivered = view.getUint8(i) === 1; i += 1
const [exchange, exchangeLen] = view.getShortString(i); i += exchangeLen
const [routingKey, routingKeyLen] = view.getShortString(i); i += routingKeyLen
const message = new AMQPMessage(channel)
message.consumerTag = consumerTag
message.deliveryTag = deliveryTag
message.exchange = exchange
message.routingKey = routingKey
message.redelivered = redelivered
channel.delivery = message
break
}
case 71: { // getOk
const deliveryTag = view.getUint64(i); i += 8
const redelivered = view.getUint8(i) === 1; i += 1
const [exchange, exchangeLen]= view.getShortString(i); i += exchangeLen
const [routingKey, routingKeyLen]= view.getShortString(i); i += routingKeyLen
const messageCount = view.getUint32(i); i += 4
const message = new AMQPMessage(channel)
message.deliveryTag = deliveryTag
message.redelivered = redelivered
message.exchange = exchange
message.routingKey = routingKey
message.messageCount = messageCount
channel.getMessage = message
break
}
case 72: { // getEmpty
const [ , len]= view.getShortString(i); i += len // reserved1
channel.resolvePromise(null)
break
}
case 80: { // confirm ack
const deliveryTag = view.getUint64(i); i += 8
const multiple = view.getUint8(i) === 1; i += 1
channel.publishConfirmed(deliveryTag, multiple, false)
break
}
case 111: { // recoverOk
channel.resolvePromise()
break
}
case 120: { // confirm nack
const deliveryTag = view.getUint64(i); i += 8
const multiple = view.getUint8(i) === 1; i += 1
channel.publishConfirmed(deliveryTag, multiple, true)
break
}
default:
i += frameSize - 4
console.error("unsupported class/method id", classId, methodId)
}
break
}
case 85: { // confirm
switch (methodId) {
case 11: { // selectOk
channel.confirmId = 1
channel.resolvePromise()
break
}
default:
i += frameSize - 4
console.error("unsupported class/method id", classId, methodId)
}
break
}
case 90: { // tx / transaction
switch (methodId) {
case 11: // selectOk
case 21: // commitOk
case 31: { // rollbackOk
channel.resolvePromise()
break
}
default:
i += frameSize - 4
console.error("unsupported class/method id", classId, methodId)
}
break
}
default:
i += frameSize - 2
console.error("unsupported class id", classId)
}
break
}
case 2: { // header
i += 4 // ignoring class id and weight
const bodySize = view.getUint64(i); i += 8
const [properties, propLen] = view.getProperties(i); i += propLen
const message = channel.delivery || channel.getMessage || channel.returned
if (message) {
message.bodySize = bodySize
message.properties = properties
message.body = new Uint8Array(bodySize)
if (bodySize === 0)
channel.onMessageReady(message)
} else {
console.warn("Header frame but no message")
}
break
}
case 3: { // body
const message = channel.delivery || channel.getMessage || channel.returned
if (message && message.body) {
const bodyPart = new Uint8Array(view.buffer, view.byteOffset + i, frameSize)
message.body.set(bodyPart, message.bodyPos)
message.bodyPos += frameSize
i += frameSize
if (message.bodyPos === message.bodySize)
channel.onMessageReady(message)
} else {
console.warn("Body frame but no message")
}
break
}
case 8: { // heartbeat
const heartbeat = new Uint8Array([1, 0, 0, 0, 0, 0, 0, 206])
this.send(heartbeat).catch(err => console.warn("Error while sending heartbeat", err))
break
}
default:
console.error("invalid frame type:", type)
i += frameSize
}
i += 1 // frame end
}
}
}