amqp-client-type-fix
Version:
AMQP 0-9-1 client, both for browsers (WebSocket) and node (TCP Socket)
1,362 lines (1,358 loc) • 104 kB
JavaScript
/**
* An error, can be both AMQP level errors or socket errors
* @property {string} message
* @property {AMQPBaseClient} connection - The connection the error was raised on
*/
class AMQPError extends Error {
/**
* @param message - Error description
* @param connection - The connection the error was raised on
*/
constructor(message, connection) {
super(message);
this.name = "AMQPError";
this.connection = connection;
}
}
/**
* An extended DataView, with AMQP protocol specific methods.
* Set methods returns bytes written.
* Get methods returns the value read and how many bytes it used.
* @ignore
*/
/** @ts-ignore */
class AMQPView extends DataView {
getUint64(byteOffset, littleEndian) {
// split 64-bit number into two 32-bit (4-byte) parts
const left = this.getUint32(byteOffset, littleEndian);
const right = this.getUint32(byteOffset + 4, littleEndian);
// combine the two 32-bit values
const combined = littleEndian ? left + 2 ** 32 * right : 2 ** 32 * left + right;
if (!Number.isSafeInteger(combined)) {
// eslint-disable-next-line no-console
console.warn(combined, 'exceeds MAX_SAFE_INTEGER. Precision may be lost');
}
return combined;
}
setUint64(byteOffset, value, littleEndian) {
this.setBigUint64(byteOffset, BigInt(value), littleEndian);
}
getInt64(byteOffset, littleEndian) {
return Number(this.getBigInt64(byteOffset, littleEndian));
}
setInt64(byteOffset, value, littleEndian) {
this.setBigInt64(byteOffset, BigInt(value), littleEndian);
}
getShortString(byteOffset) {
const len = this.getUint8(byteOffset);
byteOffset += 1;
if (typeof Buffer !== "undefined") {
const text = Buffer.from(this.buffer, this.byteOffset + byteOffset, len).toString();
return [text, len + 1];
}
else {
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset, len);
const text = new TextDecoder().decode(view);
return [text, len + 1];
}
}
setShortString(byteOffset, string) {
if (typeof Buffer !== "undefined") {
const len = Buffer.byteLength(string);
if (len > 255)
throw new Error(`Short string too long, ${len} bytes: ${string.substring(0, 255)}...`);
this.setUint8(byteOffset, len);
byteOffset += 1;
Buffer.from(this.buffer, this.byteOffset + byteOffset, len).write(string);
return len + 1;
}
else {
const utf8 = new TextEncoder().encode(string);
const len = utf8.byteLength;
if (len > 255)
throw new Error(`Short string too long, ${len} bytes: ${string.substring(0, 255)}...`);
this.setUint8(byteOffset, len);
byteOffset += 1;
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset);
view.set(utf8);
return len + 1;
}
}
getLongString(byteOffset, littleEndian) {
const len = this.getUint32(byteOffset, littleEndian);
byteOffset += 4;
if (typeof Buffer !== "undefined") {
const text = Buffer.from(this.buffer, this.byteOffset + byteOffset, len).toString();
return [text, len + 4];
}
else {
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset, len);
const text = new TextDecoder().decode(view);
return [text, len + 4];
}
}
setLongString(byteOffset, string, littleEndian) {
if (typeof Buffer !== "undefined") {
const len = Buffer.byteLength(string);
this.setUint32(byteOffset, len, littleEndian);
byteOffset += 4;
Buffer.from(this.buffer, this.byteOffset + byteOffset, len).write(string);
return len + 4;
}
else {
const utf8 = new TextEncoder().encode(string);
const len = utf8.byteLength;
this.setUint32(byteOffset, len, littleEndian);
byteOffset += 4;
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset);
view.set(utf8);
return len + 4;
}
}
getProperties(byteOffset, littleEndian) {
let j = byteOffset;
const flags = this.getUint16(j, littleEndian);
j += 2;
const props = {};
if ((flags & 0x8000) > 0) {
const [contentType, len] = this.getShortString(j);
j += len;
props.contentType = contentType;
}
if ((flags & 0x4000) > 0) {
const [contentEncoding, len] = this.getShortString(j);
j += len;
props.contentEncoding = contentEncoding;
}
if ((flags & 0x2000) > 0) {
const [headers, len] = this.getTable(j, littleEndian);
j += len;
props.headers = headers;
}
if ((flags & 0x1000) > 0) {
props.deliveryMode = this.getUint8(j);
j += 1;
}
if ((flags & 0x0800) > 0) {
props.priority = this.getUint8(j);
j += 1;
}
if ((flags & 0x0400) > 0) {
const [correlationId, len] = this.getShortString(j);
j += len;
props.correlationId = correlationId;
}
if ((flags & 0x0200) > 0) {
const [replyTo, len] = this.getShortString(j);
j += len;
props.replyTo = replyTo;
}
if ((flags & 0x0100) > 0) {
const [expiration, len] = this.getShortString(j);
j += len;
props.expiration = expiration;
}
if ((flags & 0x0080) > 0) {
const [messageId, len] = this.getShortString(j);
j += len;
props.messageId = messageId;
}
if ((flags & 0x0040) > 0) {
props.timestamp = new Date(this.getInt64(j, littleEndian) * 1000);
j += 8;
}
if ((flags & 0x0020) > 0) {
const [type, len] = this.getShortString(j);
j += len;
props.type = type;
}
if ((flags & 0x0010) > 0) {
const [userId, len] = this.getShortString(j);
j += len;
props.userId = userId;
}
if ((flags & 0x0008) > 0) {
const [appId, len] = this.getShortString(j);
j += len;
props.appId = appId;
}
const len = j - byteOffset;
return [props, len];
}
setProperties(byteOffset, properties, littleEndian) {
let j = byteOffset;
let flags = 0;
if (properties.contentType)
flags = flags | 0x8000;
if (properties.contentEncoding)
flags = flags | 0x4000;
if (properties.headers)
flags = flags | 0x2000;
if (properties.deliveryMode)
flags = flags | 0x1000;
if (properties.priority)
flags = flags | 0x0800;
if (properties.correlationId)
flags = flags | 0x0400;
if (properties.replyTo)
flags = flags | 0x0200;
if (properties.expiration)
flags = flags | 0x0100;
if (properties.messageId)
flags = flags | 0x0080;
if (properties.timestamp)
flags = flags | 0x0040;
if (properties.type)
flags = flags | 0x0020;
if (properties.userId)
flags = flags | 0x0010;
if (properties.appId)
flags = flags | 0x0008;
this.setUint16(j, flags, littleEndian);
j += 2;
if (properties.contentType) {
j += this.setShortString(j, properties.contentType);
}
if (properties.contentEncoding) {
j += this.setShortString(j, properties.contentEncoding);
}
if (properties.headers) {
j += this.setTable(j, properties.headers);
}
if (properties.deliveryMode) {
this.setUint8(j, properties.deliveryMode);
j += 1;
}
if (properties.priority) {
this.setUint8(j, properties.priority);
j += 1;
}
if (properties.correlationId) {
j += this.setShortString(j, properties.correlationId);
}
if (properties.replyTo) {
j += this.setShortString(j, properties.replyTo);
}
if (properties.expiration) {
j += this.setShortString(j, properties.expiration);
}
if (properties.messageId) {
j += this.setShortString(j, properties.messageId);
}
if (properties.timestamp) { // Date
const unixEpoch = Math.floor(Number(properties.timestamp) / 1000);
this.setInt64(j, unixEpoch, littleEndian);
j += 8;
}
if (properties.type) {
j += this.setShortString(j, properties.type);
}
if (properties.userId) {
j += this.setShortString(j, properties.userId);
}
if (properties.appId) {
j += this.setShortString(j, properties.appId);
}
const len = j - byteOffset;
return len;
}
getTable(byteOffset, littleEndian) {
const table = {};
let i = byteOffset;
const len = this.getUint32(byteOffset, littleEndian);
i += 4;
for (; i < byteOffset + 4 + len;) {
const [k, strLen] = this.getShortString(i);
i += strLen;
const [v, vLen] = this.getField(i, littleEndian);
i += vLen;
table[k] = v;
}
return [table, len + 4];
}
setTable(byteOffset, table, littleEndian) {
// skip the first 4 bytes which are for the size
let i = byteOffset + 4;
for (const [key, value] of Object.entries(table)) {
if (value === undefined)
continue;
i += this.setShortString(i, key);
i += this.setField(i, value, littleEndian);
}
this.setUint32(byteOffset, i - byteOffset - 4, littleEndian); // update prefix length
return i - byteOffset;
}
getField(byteOffset, littleEndian) {
let i = byteOffset;
const k = this.getUint8(i);
i += 1;
const type = String.fromCharCode(k);
let v;
let len;
switch (type) {
case 't':
v = this.getUint8(i) === 1;
i += 1;
break;
case 'b':
v = this.getInt8(i);
i += 1;
break;
case 'B':
v = this.getUint8(i);
i += 1;
break;
case 's':
v = this.getInt16(i, littleEndian);
i += 2;
break;
case 'u':
v = this.getUint16(i, littleEndian);
i += 2;
break;
case 'I':
v = this.getInt32(i, littleEndian);
i += 4;
break;
case 'i':
v = this.getUint32(i, littleEndian);
i += 4;
break;
case 'l':
v = this.getInt64(i, littleEndian);
i += 8;
break;
case 'f':
v = this.getFloat32(i, littleEndian);
i += 4;
break;
case 'd':
v = this.getFloat64(i, littleEndian);
i += 8;
break;
case 'S':
[v, len] = this.getLongString(i, littleEndian);
i += len;
break;
case 'F':
[v, len] = this.getTable(i, littleEndian);
i += len;
break;
case 'A':
[v, len] = this.getArray(i, littleEndian);
i += len;
break;
case 'x':
[v, len] = this.getByteArray(i, littleEndian);
i += len;
break;
case 'T':
v = new Date(this.getInt64(i, littleEndian) * 1000);
i += 8;
break;
case 'V':
v = null;
break;
case 'D': {
const scale = this.getUint8(i);
i += 1;
const value = this.getUint32(i, littleEndian);
i += 4;
v = value / 10 ** scale;
break;
}
default:
throw new Error(`Field type '${k}' not supported`);
}
return [v, i - byteOffset];
}
setField(byteOffset, field, littleEndian) {
let i = byteOffset;
switch (typeof field) {
case "string":
this.setUint8(i, 'S'.charCodeAt(0));
i += 1;
i += this.setLongString(i, field, littleEndian);
break;
case "boolean":
this.setUint8(i, 't'.charCodeAt(0));
i += 1;
this.setUint8(i, field ? 1 : 0);
i += 1;
break;
case "bigint":
this.setUint8(i, 'l'.charCodeAt(0));
i += 1;
this.setBigInt64(i, field, littleEndian);
i += 8;
break;
case "number":
if (Number.isInteger(field)) {
if (-(2 ** 32) < field && field < 2 ** 32) {
this.setUint8(i, 'I'.charCodeAt(0));
i += 1;
this.setInt32(i, field, littleEndian);
i += 4;
}
else {
this.setUint8(i, 'l'.charCodeAt(0));
i += 1;
this.setInt64(i, field, littleEndian);
i += 8;
}
}
else { // float
if (-(2 ** 32) < field && field < 2 ** 32) {
this.setUint8(i, 'f'.charCodeAt(0));
i += 1;
this.setFloat32(i, field, littleEndian);
i += 4;
}
else {
this.setUint8(i, 'd'.charCodeAt(0));
i += 1;
this.setFloat64(i, field, littleEndian);
i += 8;
}
}
break;
case "object":
if (Array.isArray(field)) {
this.setUint8(i, 'A'.charCodeAt(0));
i += 1;
i += this.setArray(i, field, littleEndian);
}
else if (field instanceof Uint8Array) {
this.setUint8(i, 'x'.charCodeAt(0));
i += 1;
i += this.setByteArray(i, field);
}
else if (field instanceof ArrayBuffer) {
this.setUint8(i, 'x'.charCodeAt(0));
i += 1;
i += this.setByteArray(i, new Uint8Array(field));
}
else if (field instanceof Date) {
this.setUint8(i, 'T'.charCodeAt(0));
i += 1;
const unixEpoch = Math.floor(Number(field) / 1000);
this.setInt64(i, unixEpoch, littleEndian);
i += 8;
}
else if (field === null || field === undefined) {
this.setUint8(i, 'V'.charCodeAt(0));
i += 1;
}
else { // hopefully it's a hash like object
this.setUint8(i, 'F'.charCodeAt(0));
i += 1;
i += this.setTable(i, field, littleEndian);
}
break;
default:
throw new Error(`Unsupported field type '${field}'`);
}
return i - byteOffset;
}
getArray(byteOffset, littleEndian) {
const len = this.getUint32(byteOffset, littleEndian);
byteOffset += 4;
const endOffset = byteOffset + len;
const v = [];
for (; byteOffset < endOffset;) {
const [field, fieldLen] = this.getField(byteOffset, littleEndian);
byteOffset += fieldLen;
v.push(field);
}
return [v, len + 4];
}
setArray(byteOffset, array, littleEndian) {
const start = byteOffset;
byteOffset += 4; // update the length later
array.forEach((e) => {
byteOffset += this.setField(byteOffset, e, littleEndian);
});
this.setUint32(start, byteOffset - start - 4, littleEndian); // update length
return byteOffset - start;
}
getByteArray(byteOffset, littleEndian) {
const len = this.getUint32(byteOffset, littleEndian);
byteOffset += 4;
const v = new Uint8Array(this.buffer, this.byteOffset + byteOffset, len);
return [v, len + 4];
}
setByteArray(byteOffset, data, littleEndian) {
this.setUint32(byteOffset, data.byteLength, littleEndian);
byteOffset += 4;
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset, data.byteLength);
view.set(data);
return data.byteLength + 4;
}
}
/**
* Convience class for queues
*/
class AMQPQueue {
/**
* @param channel - channel this queue was declared on
* @param name - name of the queue
*/
constructor(channel, name) {
this.channel = channel;
this.name = name;
}
/**
* Bind the queue to an exchange
*/
bind(exchange, routingKey = "", args = {}) {
return new Promise((resolve, reject) => {
this.channel.queueBind(this.name, exchange, routingKey, args)
.then(() => resolve(this))
.catch(reject);
});
}
/**
* Delete a binding between this queue and an exchange
*/
unbind(exchange, routingKey = "", args = {}) {
return new Promise((resolve, reject) => {
this.channel.queueUnbind(this.name, exchange, routingKey, args)
.then(() => resolve(this))
.catch(reject);
});
}
/**
* Publish a message directly to the queue
* @param body - the data to be published, can be a string or an uint8array
* @param properties - publish properties
* @return fulfilled when the message is enqueue on the socket, or if publish confirm is enabled when the message is confirmed by the server
*/
publish(body, properties = {}) {
return new Promise((resolve, reject) => {
this.channel.basicPublish("", this.name, body, properties)
.then(() => resolve(this))
.catch(reject);
});
}
/**
* Subscribe to the queue
* @param params
* @param [params.noAck=true] - if messages are removed from the server upon delivery, or have to be acknowledged
* @param [params.exclusive=false] - if this can be the only consumer of the queue, will return an Error if there are other consumers to the queue already
* @param [params.tag=""] - tag of the consumer, will be server generated if left empty
* @param [params.args={}] - custom arguments
* @param {function(AMQPMessage) : void} callback - Function to be called for each received message
*/
subscribe({ noAck = true, exclusive = false, tag = "", args = {} } = {}, callback) {
return this.channel.basicConsume(this.name, { noAck, exclusive, tag, args }, callback);
}
/**
* Unsubscribe from the queue
*/
unsubscribe(consumerTag) {
return new Promise((resolve, reject) => {
this.channel.basicCancel(consumerTag)
.then(() => resolve(this))
.catch(reject);
});
}
/**
* Delete the queue
*/
delete() {
return new Promise((resolve, reject) => {
this.channel.queueDelete(this.name)
.then(() => resolve(this))
.catch(reject);
});
}
/**
* Poll the queue for messages
* @param params
* @param params.noAck - automatically acknowledge messages when received
*/
get({ noAck = true } = {}) {
return this.channel.basicGet(this.name, { noAck });
}
purge() {
return this.channel.queuePurge(this.name);
}
}
/**
* A consumer, subscribed to a queue
*/
class AMQPConsumer {
/**
* @param channel - the consumer is created on
* @param tag - consumer tag
* @param onMessage - callback executed when a message arrive
*/
constructor(channel, tag, onMessage) {
this.closed = false;
this.channel = channel;
this.tag = tag;
this.onMessage = onMessage;
}
/**
* Wait for the consumer to finish.
* @param [timeout] wait for this many milliseconds and then return regardless
* @return Fulfilled when the consumer/channel/connection is closed by the client. Rejected if the timeout is hit.
*/
wait(timeout) {
if (this.closedError)
return Promise.reject(this.closedError);
if (this.closed)
return Promise.resolve();
return new Promise((resolve, reject) => {
this.resolveWait = resolve;
this.rejectWait = reject;
if (timeout) {
const onTimeout = () => reject(new AMQPError("Timeout", this.channel.connection));
this.timeoutId = setTimeout(onTimeout, timeout);
}
});
}
/**
* Cancel/abort/stop the consumer. No more messages will be deliviered to the consumer.
* Note that any unacked messages are still unacked as they belong to the channel and not the consumer.
*/
cancel() {
return this.channel.basicCancel(this.tag);
}
/**
* @ignore
* @param [err] - why the consumer was closed
*/
setClosed(err) {
this.closed = true;
if (err)
this.closedError = err;
if (this.timeoutId)
clearTimeout(this.timeoutId);
if (err) {
if (this.rejectWait)
this.rejectWait(err);
}
else {
if (this.resolveWait)
this.resolveWait();
}
}
}
/**
* Represents an AMQP Channel. Almost all actions in AMQP are performed on a Channel.
*/
class AMQPChannel {
/**
* @param connection - The connection this channel belongs to
* @param id - ID of the channel
*/
constructor(connection, id) {
this.consumers = new Map();
this.rpcQueue = Promise.resolve(true);
this.unconfirmedPublishes = [];
this.closed = false;
this.confirmId = 0;
this.connection = connection;
this.id = id;
this.onerror = (reason) => this.logger?.error(`channel ${this.id} closed: ${reason}`);
}
get logger() {
return this.connection.logger;
}
open() {
let j = 0;
const channelOpen = new AMQPView(new ArrayBuffer(13));
channelOpen.setUint8(j, 1);
j += 1; // type: method
channelOpen.setUint16(j, this.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 this.sendRpc(channelOpen, j);
}
/**
* Declare a queue and return an AMQPQueue instance.
*/
queue(name = "", { passive = false, durable = name !== "", autoDelete = name === "", exclusive = name === "" } = {}, args = {}) {
return new Promise((resolve, reject) => {
this.queueDeclare(name, { passive, durable, autoDelete, exclusive }, args)
.then(({ name }) => resolve(new AMQPQueue(this, name)))
.catch(reject);
});
}
/**
* Alias for basicQos
* @param prefetchCount - max inflight messages
*/
prefetch(prefetchCount) {
return this.basicQos(prefetchCount);
}
/**
* Default handler for Returned messages
* @param message returned from server
*/
onReturn(message) {
this.logger?.error("Message returned from server", message);
}
/**
* Close the channel gracefully
* @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, this.id);
j += 2; // channel
frame.setUint32(j, 0);
j += 4; // frameSize
frame.setUint16(j, 20);
j += 2; // class: channel
frame.setUint16(j, 40);
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 this.sendRpc(frame, j);
}
/**
* 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, { noAck = true } = {}) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(512));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel: 1
frame.setUint32(j, 11);
j += 4; // frameSize
frame.setUint16(j, 60);
j += 2; // class: basic
frame.setUint16(j, 70);
j += 2; // method: get
frame.setUint16(j, 0);
j += 2; // reserved1
j += frame.setShortString(j, queue); // queue
frame.setUint8(j, noAck ? 1 : 0);
j += 1; // noAck
frame.setUint8(j, 206);
j += 1; // frame end byte
frame.setUint32(3, j - 8); // update frameSize
return this.sendRpc(frame, j);
}
/**
* Consume from a queue. Messages will be delivered asynchronously.
* @param queue - name of the queue to poll
* @param param
* @param [param.tag=""] - tag of the consumer, will be server generated if left empty
* @param [param.noAck=true] - if messages are removed from the server upon delivery, or have to be acknowledged
* @param [param.exclusive=false] - if this can be the only consumer of the queue, will return an Error if there are other consumers to the queue already
* @param [param.args={}] - custom arguments
* @param {function(AMQPMessage) : void} callback - will be called for each message delivered to this consumer
*/
basicConsume(queue, { tag = "", noAck = true, exclusive = false, args = {} } = {}, callback) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(4096));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel: 1
frame.setUint32(j, 0);
j += 4; // frameSize
frame.setUint16(j, 60);
j += 2; // class: basic
frame.setUint16(j, 20);
j += 2; // method: consume
frame.setUint16(j, 0);
j += 2; // reserved1
j += frame.setShortString(j, queue); // queue
j += frame.setShortString(j, tag); // tag
let bits = 0;
if (noAck)
bits = bits | (1 << 1);
if (exclusive)
bits = bits | (1 << 2);
frame.setUint8(j, bits);
j += 1; // noLocal/noAck/exclusive/noWait
j += frame.setTable(j, args); // arguments table
frame.setUint8(j, 206);
j += 1; // frame end byte
frame.setUint32(3, j - 8); // update frameSize
return new Promise((resolve, reject) => {
this.sendRpc(frame, j).then((consumerTag) => {
const consumer = new AMQPConsumer(this, consumerTag, callback);
this.consumers.set(consumerTag, consumer);
resolve(consumer);
}).catch(reject);
});
}
/**
* Cancel/stop a consumer
* @param tag - consumer tag
*/
basicCancel(tag) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(512));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel: 1
frame.setUint32(j, 0);
j += 4; // frameSize
frame.setUint16(j, 60);
j += 2; // class: basic
frame.setUint16(j, 30);
j += 2; // method: cancel
j += frame.setShortString(j, tag); // tag
frame.setUint8(j, 0);
j += 1; // noWait
frame.setUint8(j, 206);
j += 1; // frame end byte
frame.setUint32(3, j - 8); // update frameSize
return new Promise((resolve, reject) => {
this.sendRpc(frame, j).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, multiple = false) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(21));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel
frame.setUint32(j, 13);
j += 4; // frameSize
frame.setUint16(j, 60);
j += 2; // class: basic
frame.setUint16(j, 80);
j += 2; // method: ack
frame.setUint64(j, deliveryTag);
j += 8;
frame.setUint8(j, multiple ? 1 : 0);
j += 1;
frame.setUint8(j, 206);
j += 1; // frame end byte
return this.connection.send(new Uint8Array(frame.buffer, 0, 21));
}
/**
* 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, requeue = false, multiple = false) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(21));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel
frame.setUint32(j, 13);
j += 4; // frameSize
frame.setUint16(j, 60);
j += 2; // class: basic
frame.setUint16(j, 120);
j += 2; // method: nack
frame.setUint64(j, deliveryTag);
j += 8;
let bits = 0;
if (multiple)
bits = bits | (1 << 0);
if (requeue)
bits = bits | (1 << 1);
frame.setUint8(j, bits);
j += 1;
frame.setUint8(j, 206);
j += 1; // frame end byte
return this.connection.send(new Uint8Array(frame.buffer, 0, 21));
}
/**
* Acknowledge a delivered message
* @param deliveryTag - tag of the message
* @param [requeue=false] - if the message should be requeued or removed
*/
basicReject(deliveryTag, requeue = false) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(21));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel
frame.setUint32(j, 13);
j += 4; // frameSize
frame.setUint16(j, 60);
j += 2; // class: basic
frame.setUint16(j, 90);
j += 2; // method: reject
frame.setUint64(j, deliveryTag);
j += 8;
frame.setUint8(j, requeue ? 1 : 0);
j += 1;
frame.setUint8(j, 206);
j += 1; // frame end byte
return this.connection.send(new Uint8Array(frame.buffer, 0, 21));
}
/**
* 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) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(13));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel
frame.setUint32(j, 5);
j += 4; // frameSize
frame.setUint16(j, 60);
j += 2; // class: basic
frame.setUint16(j, 110);
j += 2; // method: recover
frame.setUint8(j, requeue ? 1 : 0);
j += 1;
frame.setUint8(j, 206);
j += 1; // frame end byte
return this.sendRpc(frame, j);
}
/**
* 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, routingKey, data, properties = {}, mandatory = false, immediate = false) {
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;
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, 1);
j += 1; // type: method
buffer.setUint16(j, this.id);
j += 2; // channel
j += 4; // frame size, update later
buffer.setUint16(j, 60);
j += 2; // class: basic
buffer.setUint16(j, 40);
j += 2; // method: publish
buffer.setUint16(j, 0);
j += 2; // reserved1
j += buffer.setShortString(j, exchange); // exchange
j += buffer.setShortString(j, routingKey); // routing key
let bits = 0;
if (mandatory)
bits = bits | (1 << 0);
if (immediate)
bits = bits | (1 << 1);
buffer.setUint8(j, bits);
j += 1; // mandatory/immediate
buffer.setUint8(j, 206);
j += 1; // frame end byte
buffer.setUint32(3, j - 8); // update frameSize
const headerStart = j;
buffer.setUint8(j, 2);
j += 1; // type: header
buffer.setUint16(j, this.id);
j += 2; // channel
j += 4; // frame size, update later
buffer.setUint16(j, 60);
j += 2; // class: basic
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); // properties
buffer.setUint8(j, 206);
j += 1; // frame end byte
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, 3);
j += 1; // type: body
buffer.setUint16(j, this.id);
j += 2; // channel
buffer.setUint32(j, frameSize);
j += 4; // frameSize
bufferView.set(dataSlice, j);
j += frameSize; // body content
buffer.setUint8(j, 206);
j += 1; // frame end byte
bodyPos += frameSize;
}
const sendFrames = this.connection.send(bufferView.subarray(0, j));
this.connection.bufferPool.push(buffer); // 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((resolve, reject) => this.unconfirmedPublishes.push([this.confirmId++, resolve, reject]));
return sendFrames.then(() => wait4Confirm);
}
else {
return sendFrames.then(() => 0);
}
}
/**
* 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, prefetchSize = 0, global = false) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(19));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel: 1
frame.setUint32(j, 11);
j += 4; // frameSize
frame.setUint16(j, 60);
j += 2; // class: basic
frame.setUint16(j, 10);
j += 2; // method: qos
frame.setUint32(j, prefetchSize);
j += 4; // prefetch size
frame.setUint16(j, prefetchCount);
j += 2; // prefetch count
frame.setUint8(j, global ? 1 : 0);
j += 1; // glocal
frame.setUint8(j, 206);
j += 1; // frame end byte
return this.sendRpc(frame, j);
}
/**
* 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) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(13));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel: 1
frame.setUint32(j, 5);
j += 4; // frameSize
frame.setUint16(j, 20);
j += 2; // class: channel
frame.setUint16(j, 20);
j += 2; // method: flow
frame.setUint8(j, active ? 1 : 0);
j += 1; // active flow
frame.setUint8(j, 206);
j += 1; // frame end byte
return this.sendRpc(frame, j);
}
/**
* Enable publish confirm. The server will then confirm each publish with an Ack or Nack when the message is enqueued.
*/
confirmSelect() {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(13));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel
frame.setUint32(j, 5);
j += 4; // frame size
frame.setUint16(j, 85);
j += 2; // class: confirm
frame.setUint16(j, 10);
j += 2; // method: select
frame.setUint8(j, 0);
j += 1; // noWait
frame.setUint8(j, 206);
j += 1; // frame end byte
return this.sendRpc(frame, j); // 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 === "" } = {}, args = {}) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const declare = new AMQPView(new ArrayBuffer(4096));
declare.setUint8(j, 1);
j += 1; // type: method
declare.setUint16(j, this.id);
j += 2; // channel: 1
declare.setUint32(j, 0);
j += 4; // frameSize
declare.setUint16(j, 50);
j += 2; // class: queue
declare.setUint16(j, 10);
j += 2; // method: declare
declare.setUint16(j, 0);
j += 2; // reserved1
j += declare.setShortString(j, name); // 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);
declare.setUint8(j, bits);
j += 1;
j += declare.setTable(j, args); // arguments
declare.setUint8(j, 206);
j += 1; // frame end byte
declare.setUint32(3, j - 8); // update frameSize
return this.sendRpc(declare, j);
}
/**
* 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 } = {}) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const frame = new AMQPView(new ArrayBuffer(512));
frame.setUint8(j, 1);
j += 1; // type: method
frame.setUint16(j, this.id);
j += 2; // channel: 1
frame.setUint32(j, 0);
j += 4; // frameSize
frame.setUint16(j, 50);
j += 2; // class: queue
frame.setUint16(j, 40);
j += 2; // method: delete
frame.setUint16(j, 0);
j += 2; // reserved1
j += frame.setShortString(j, name); // name
let bits = 0;
if (ifUnused)
bits = bits | (1 << 0);
if (ifEmpty)
bits = bits | (1 << 1);
frame.setUint8(j, bits);
j += 1;
frame.setUint8(j, 206);
j += 1; // frame end byte
frame.setUint32(3, j - 8); // update frameSize
return this.sendRpc(frame, j);
}
/**
* 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, exchange, routingKey, args = {}) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const bind = new AMQPView(new ArrayBuffer(4096));
bind.setUint8(j, 1);
j += 1; // type: method
bind.setUint16(j, this.id);
j += 2; // channel: 1
bind.setUint32(j, 0);
j += 4; // frameSize
bind.setUint16(j, 50);
j += 2; // class: queue
bind.setUint16(j, 20);
j += 2; // method: bind
bind.setUint16(j, 0);
j += 2; // reserved1
j += bind.setShortString(j, queue);
j += bind.setShortString(j, exchange);
j += bind.setShortString(j, routingKey);
bind.setUint8(j, 0);
j += 1; // noWait
j += bind.setTable(j, args);
bind.setUint8(j, 206);
j += 1; // frame end byte
bind.setUint32(3, j - 8); // update frameSize
return this.sendRpc(bind, j);
}
/**
* 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, exchange, routingKey, args = {}) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const unbind = new AMQPView(new ArrayBuffer(4096));
unbind.setUint8(j, 1);
j += 1; // type: method
unbind.setUint16(j, this.id);
j += 2; // channel: 1
unbind.setUint32(j, 0);
j += 4; // frameSize
unbind.setUint16(j, 50);
j += 2; // class: queue
unbind.setUint16(j, 50);
j += 2; // method: unbind
unbind.setUint16(j, 0);
j += 2; // reserved1
j += unbind.setShortString(j, queue);
j += unbind.setShortString(j, exchange);
j += unbind.setShortString(j, routingKey);
j += unbind.setTable(j, args);
unbind.setUint8(j, 206);
j += 1; // frame end byte
unbind.setUint32(3, j - 8); // update frameSize
return this.sendRpc(unbind, j);
}
/**
* Purge a queue
* @param queue - name of the queue
* @return fulfilled when confirmed by the server
*/
queuePurge(queue) {
if (this.closed)
return this.rejectClosed();
let j = 0;
const purge = new AMQPView(new ArrayBuffer(512));
purge.setUint8(j, 1);
j += 1; // type: method
purge.setUint16(j, this.id);
j += 2; // channel: 1
purge.setUint32(j, 0);
j += 4; // frameSize
purge.setUint16(j, 50);
j += 2; // class: queue
purge.setUint16(j, 30);
j += 2; // method: purge
purge.setUint16(j, 0);
j += 2; // reserved1
j += purge.setShortString(j, queue);
purge.setUint8(j, 0);
j += 1; // noWait
purge.setUint8(j, 206);
j += 1; // frame end byte
purge.setUint32(3, j - 8); // update frameSize
return this.sendRpc(purge, j);
}
/**
* 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 e