@cloudamqp/amqp-client
Version:
AMQP 0-9-1 client, both for browsers (WebSocket) and node (TCP Socket)
379 lines • 14.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AMQPSession = void 0;
const amqp_codec_registry_js_1 = require("./amqp-codec-registry.js");
const amqp_queue_js_1 = require("./amqp-queue.js");
const amqp_exchange_js_1 = require("./amqp-exchange.js");
const amqp_rpc_client_js_1 = require("./amqp-rpc-client.js");
const amqp_rpc_server_js_1 = require("./amqp-rpc-server.js");
class AMQPSession {
get closed() {
return this.client.closed;
}
get logger() {
return this.client.logger;
}
constructor(client, options) {
this.queues = new Map();
this.rpcClients = new Set();
this.reconnectAttempts = 0;
this.reconnecting = false;
this.stopped = false;
this.opsChannel = null;
this.confirmChannel = null;
this.opsChannelPromise = null;
this.confirmChannelPromise = null;
this.opsLock = Promise.resolve();
this.client = client;
if (options?.parsers)
this.parsers = options.parsers;
if (options?.coders)
this.coders = options.coders;
if (options?.defaultContentType)
this.defaultContentType = options.defaultContentType;
if (options?.defaultContentEncoding)
this.defaultContentEncoding = options.defaultContentEncoding;
if (options?.onconnect)
this.onconnect = options.onconnect;
if (options?.ondisconnect)
this.ondisconnect = options.ondisconnect;
if (options?.onfailed)
this.onfailed = options.onfailed;
if (options?.onreturn)
this.onreturn = options.onreturn;
if (options?.onblocked)
this.client.onblocked = options.onblocked;
if (options?.onunblocked)
this.client.onunblocked = options.onunblocked;
this.options = {
reconnectInterval: options?.reconnectInterval ?? 1000,
maxReconnectInterval: options?.maxReconnectInterval ?? 30000,
backoffMultiplier: options?.backoffMultiplier ?? 2,
maxRetries: options?.maxRetries ?? 0,
};
this.client.ondisconnect = (err) => {
this.client.logger?.warn(`${this.logTag()}: disconnected`);
this.opsChannel = null;
this.confirmChannel = null;
this.opsChannelPromise = null;
this.confirmChannelPromise = null;
this.ondisconnect?.(err);
if (!this.stopped && !this.reconnecting) {
void this.reconnectLoop();
}
};
}
logTag() {
return this.client.name ? `AMQPSession[${this.client.name}]` : "AMQPSession";
}
static async connect(url, options) {
const u = new URL(url);
let client;
if (u.protocol === "ws:" || u.protocol === "wss:") {
const { AMQPWebSocketClient } = await Promise.resolve().then(() => __importStar(require("./amqp-websocket-client.js")));
const vhost = options?.vhost ?? "/";
const username = decodeURIComponent(u.username) || "guest";
const password = decodeURIComponent(u.password) || "guest";
const name = options?.name ?? u.searchParams.get("name") ?? undefined;
const heartbeat = options?.heartbeat ?? numericParam(u.searchParams.get("heartbeat"));
const frameMax = options?.frameMax ?? numericParam(u.searchParams.get("frameMax"));
const channelMax = options?.channelMax ?? numericParam(u.searchParams.get("channelMax"));
const wsUrl = `${u.protocol}//${u.host}${u.pathname}${u.search}`;
const init = {
url: wsUrl,
vhost,
username,
password,
logger: options?.logger ?? null,
};
if (name)
init.name = name;
if (heartbeat !== undefined)
init.heartbeat = heartbeat;
if (frameMax !== undefined)
init.frameMax = frameMax;
if (channelMax !== undefined)
init.channelMax = channelMax;
client = new AMQPWebSocketClient(init);
}
else {
const { AMQPClient } = await Promise.resolve().then(() => __importStar(require("./amqp-socket-client.js")));
const overrides = {
name: options?.name,
heartbeat: options?.heartbeat?.toString(),
frameMax: options?.frameMax?.toString(),
channelMax: options?.channelMax?.toString(),
};
for (const [key, val] of Object.entries(overrides)) {
if (val !== undefined)
u.searchParams.set(key, val);
}
client = new AMQPClient(u.toString(), options?.tlsOptions, options?.logger);
}
await client.connect();
const session = new AMQPSession(client, options);
client.logger?.info(`${session.logTag()}: connected`);
options?.onconnect?.(session);
return session;
}
getOpsChannel() {
if (this.opsChannel && !this.opsChannel.closed)
return Promise.resolve(this.opsChannel);
if (this.opsChannelPromise)
return this.opsChannelPromise;
this.opsChannelPromise = this.client.channel().then((ch) => {
this.wireReturnHandler(ch);
this.wireManagedChannelErrorHandler(ch);
this.opsChannel = ch;
this.opsChannelPromise = null;
return ch;
});
this.opsChannelPromise.catch(() => {
this.opsChannelPromise = null;
});
return this.opsChannelPromise;
}
wireReturnHandler(ch) {
if (!this.onreturn)
return;
ch.onReturn = (msg) => {
const handler = this.onreturn;
if (!handler)
return;
void (async () => {
try {
if (this.parsers || this.coders) {
await (0, amqp_codec_registry_js_1.decodeMessage)(msg, this.parsers ?? {}, this.coders ?? {});
}
handler(msg);
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
this.client.logger?.warn(`${this.logTag()}: onreturn handler failed:`, error.message);
}
})();
};
}
wireManagedChannelErrorHandler(ch) {
ch.onerror = (reason) => {
this.client.logger?.debug(`${this.logTag()}: channel ${ch.id} closed (${reason}); will reopen on next use`);
};
}
withOpsChannel(fn) {
const result = this.opsLock.then(async () => {
const ch = await this.getOpsChannel();
return fn(ch);
});
this.opsLock = result.catch(() => { });
return result;
}
getConfirmChannel() {
if (this.confirmChannel && !this.confirmChannel.closed)
return Promise.resolve(this.confirmChannel);
if (this.confirmChannelPromise)
return this.confirmChannelPromise;
this.confirmChannelPromise = this.client.channel().then(async (ch) => {
await ch.confirmSelect();
this.wireReturnHandler(ch);
this.wireManagedChannelErrorHandler(ch);
this.confirmChannel = ch;
this.confirmChannelPromise = null;
return ch;
});
this.confirmChannelPromise.catch(() => {
this.confirmChannelPromise = null;
});
return this.confirmChannelPromise;
}
openChannel() {
return this.client.channel();
}
removeQueue(name) {
this.queues.delete(name);
}
async queue(name, options) {
if (name !== "") {
const cached = this.queues.get(name);
if (cached)
return cached;
}
const { arguments: queueArguments, ...declarationParams } = options ?? {};
return this.withOpsChannel(async (ch) => {
const res = await ch.queueDeclare(name, declarationParams, queueArguments);
if (name === "")
return new amqp_queue_js_1.AMQPQueue(this, res.name);
const existing = this.queues.get(res.name);
if (existing)
return existing;
const q = new amqp_queue_js_1.AMQPQueue(this, res.name);
this.queues.set(res.name, q);
return q;
});
}
async exchange(name, type, options) {
const { arguments: exchangeArguments, ...declarationParams } = options ?? {};
return this.withOpsChannel(async (ch) => {
await ch.exchangeDeclare(name, type, declarationParams, exchangeArguments);
return new amqp_exchange_js_1.AMQPExchange(this, name);
});
}
async directExchange(name = "amq.direct", options) {
if (name === "")
return new amqp_exchange_js_1.AMQPExchange(this, "");
return this.exchange(name, "direct", options);
}
fanoutExchange(name = "amq.fanout", options) {
return this.exchange(name, "fanout", options);
}
topicExchange(name = "amq.topic", options) {
return this.exchange(name, "topic", options);
}
headersExchange(name = "amq.headers", options) {
return this.exchange(name, "headers", options);
}
async rpcCall(queue, body, options) {
const rpc = new amqp_rpc_client_js_1.AMQPRPCClient(this);
await rpc.start();
try {
return await rpc.call(queue, body, options);
}
finally {
await rpc.close();
}
}
async rpcClient() {
const rpc = new amqp_rpc_client_js_1.AMQPRPCClient(this);
await rpc.start();
this.rpcClients.add(rpc);
return rpc;
}
untrackRPCClient(rpc) {
this.rpcClients.delete(rpc);
}
async rpcServer(queue, handler, prefetch) {
const server = new amqp_rpc_server_js_1.AMQPRPCServer(this);
await server.start(queue, handler, prefetch);
return server;
}
async stop(reason) {
this.stopped = true;
this.cancelWait();
for (const queue of this.queues.values()) {
queue.cancelAll();
}
this.queues.clear();
for (const rpc of this.rpcClients) {
rpc.close().catch(() => { });
}
this.rpcClients.clear();
delete this.client.ondisconnect;
if (!this.client.closed) {
await this.client.close(reason);
}
}
async reconnectLoop() {
if (this.reconnecting)
return;
this.reconnecting = true;
while (!this.stopped) {
this.reconnectAttempts++;
if (this.options.maxRetries > 0 && this.reconnectAttempts > this.options.maxRetries) {
this.stopped = true;
this.client.logger?.error(`${this.logTag()}: reconnect gave up`);
this.onfailed?.(new Error(`Max reconnection attempts (${this.options.maxRetries}) reached`));
continue;
}
const delay = Math.min(this.options.reconnectInterval * Math.pow(this.options.backoffMultiplier, this.reconnectAttempts - 1), this.options.maxReconnectInterval);
this.client.logger?.debug(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
await this.waitBeforeRetry(delay);
if (this.stopped)
continue;
try {
await this.client.connect();
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
this.client.logger?.warn("AMQP-Client reconnect error:", error.message);
continue;
}
this.reconnectAttempts = 0;
await this.recoverQueues();
if (this.stopped || this.client.closed)
continue;
this.client.logger?.info(`${this.logTag()}: reconnected`);
this.onconnect?.(this);
break;
}
this.reconnecting = false;
}
waitBeforeRetry(ms) {
return new Promise((resolve) => {
this.reconnectResolve = resolve;
this.reconnectTimer = setTimeout(resolve, ms);
});
}
cancelWait() {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = undefined;
this.reconnectResolve?.();
this.reconnectResolve = undefined;
}
async recoverQueues() {
if (this.client.closed)
return;
for (const queue of this.queues.values()) {
await queue.recover();
}
for (const rpc of this.rpcClients) {
try {
await rpc.recover();
this.client.logger?.debug("Recovered RPC client");
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
this.client.logger?.warn("Failed to recover RPC client:", error.message);
}
}
}
}
exports.AMQPSession = AMQPSession;
function numericParam(value) {
if (value === null)
return undefined;
const n = Number(value);
return Number.isFinite(n) ? n : undefined;
}
//# sourceMappingURL=amqp-session.js.map