UNPKG

@cloudamqp/amqp-client

Version:

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

379 lines 14.8 kB
"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