UNPKG

@cloudamqp/amqp-client

Version:

AMQP 0-9-1 client, both for browsers (WebSocket) and node (TCP Socket)

342 lines 13 kB
import { decodeMessage } from "./amqp-codec-registry.js"; import { AMQPQueue } from "./amqp-queue.js"; import { AMQPExchange } from "./amqp-exchange.js"; import { AMQPRPCClient } from "./amqp-rpc-client.js"; import { AMQPRPCServer } from "./amqp-rpc-server.js"; export 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 import("./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 import("./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 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 AMQPQueue(this, res.name); const existing = this.queues.get(res.name); if (existing) return existing; const q = new 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 AMQPExchange(this, name); }); } async directExchange(name = "amq.direct", options) { if (name === "") return new 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 AMQPRPCClient(this); await rpc.start(); try { return await rpc.call(queue, body, options); } finally { await rpc.close(); } } async rpcClient() { const rpc = new 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 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); } } } } function numericParam(value) { if (value === null) return undefined; const n = Number(value); return Number.isFinite(n) ? n : undefined; } //# sourceMappingURL=amqp-session.js.map