const require_reconnect = require("../../../utils/reconnect.cjs");
const require_error = require("../error.cjs");
const require_queue = require("./queue.cjs");
const require_utils = require("./utils.cjs");
//#region src/client/stream/transport/websocket.ts
const WEB_SOCKET_CONNECTING = 0;
const WEB_SOCKET_OPEN = 1;
const WEB_SOCKET_CLOSED = 3;
/**
* Transport adapter that speaks the thread-centric protocol over a
* bidirectional WebSocket. Bound to a `threadId` at construction or later
* via {@link setThreadId} — the socket connects to
* `ws://.../threads/:thread_id/stream/events`.
*
* On unexpected disconnect the adapter reconnects with exponential
* backoff (see {@link ProtocolWebSocketTransportOptions.maxReconnectAttempts}).
* The server replays buffered events on the new socket; the SDK
* deduplicates by `event_id`. {@link ProtocolWebSocketTransportOptions.onReconnected}
* runs after each successful reconnect so `ThreadStream` can re-issue
* `subscription.subscribe` commands.
*/
var ProtocolWebSocketTransportAdapter = class {
	threadId;
	queue = new require_queue.AsyncQueue();
	apiUrl;
	defaultHeaders;
	onRequest;
	webSocketFactory;
	paths;
	maxReconnectAttempts;
	onReconnect;
	reconnectDelayMs;
	onReconnected;
	pending = /* @__PURE__ */ new Map();
	socket = null;
	closed = false;
	intentionalClose = false;
	reconnectInFlight = null;
	constructor(options) {
		this.apiUrl = options.apiUrl;
		this.threadId = options.threadId ?? "";
		this.defaultHeaders = options.defaultHeaders;
		this.onRequest = options.onRequest;
		this.webSocketFactory = options.webSocketFactory ?? ((url) => new WebSocket(url));
		this.paths = options.paths;
		this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
		this.onReconnect = options.onReconnect;
		this.onReconnected = options.onReconnected;
		this.reconnectDelayMs = options.reconnectDelayMs ?? require_reconnect.reconnectDelayMs;
	}
	/** {@inheritDoc TransportAdapter.setThreadId} */
	setThreadId(threadId) {
		if (threadId === this.threadId) return;
		if (this.reconnectInFlight != null || this.socket != null && this.socket.readyState !== WEB_SOCKET_CLOSED) throw new Error("Protocol WebSocket transport cannot be rebound to a different thread while the socket is open. Close the current stream and create a new WebSocket transport for the new thread.");
		this.threadId = threadId;
	}
	/**
	* Socket URL derives from the currently-bound thread so a single adapter
	* can follow {@link setThreadId} re-binds; the next {@link open} connects
	* to the new thread. A fixed `paths.stream` string overrides the default.
	*/
	get streamUrl() {
		return require_utils.resolveProtocolPath(this.paths?.stream, this.threadId, (id) => `/threads/${id}/stream/events`);
	}
	/**
	* Register a callback invoked after each successful reconnect. Used
	* by {@link ThreadStream} to re-send active `subscription.subscribe`
	* commands.
	*/
	setOnReconnected(handler) {
		this.onReconnected = handler;
	}
	async open() {
		if (this.closed) throw new Error("Protocol WebSocket transport is closed.");
		if (this.socket?.readyState === WEB_SOCKET_OPEN) return;
		if (this.socket != null) {
			this.#detachSocket(this.socket);
			this.socket = null;
		}
		this.assertBrowserSafeTransportConfig();
		const wsUrl = require_utils.toWebSocketUrl(require_utils.toAbsoluteUrl(this.apiUrl, this.streamUrl).toString());
		const socket = this.webSocketFactory(wsUrl);
		this.socket = socket;
		this.intentionalClose = false;
		this.#attachSocket(socket);
		await new Promise((resolve, reject) => {
			const onOpen = () => {
				cleanup();
				resolve();
			};
			const onError = () => {
				cleanup();
				reject(/* @__PURE__ */ new Error("Failed to open protocol WebSocket."));
			};
			const cleanup = () => {
				socket.removeEventListener("open", onOpen);
				socket.removeEventListener("error", onError);
			};
			socket.addEventListener("open", onOpen, { once: true });
			socket.addEventListener("error", onError, { once: true });
		});
	}
	async send(command) {
		return await this.sendCommand(command);
	}
	events() {
		const queue = this.queue;
		return { [Symbol.asyncIterator]: () => ({
			next: async () => await queue.shift(),
			return: async () => {
				queue.close();
				return {
					done: true,
					value: void 0
				};
			}
		}) };
	}
	async close() {
		if (this.closed) return;
		this.closed = true;
		this.intentionalClose = true;
		for (const { reject } of this.pending.values()) reject(/* @__PURE__ */ new Error("Protocol WebSocket connection closed."));
		this.pending.clear();
		this.queue.close();
		const socket = this.socket;
		this.socket = null;
		if (!socket) return;
		this.#detachSocket(socket);
		await new Promise((resolve) => {
			if (socket.readyState === WEB_SOCKET_CLOSED) {
				resolve();
				return;
			}
			const onClose = () => {
				socket.removeEventListener("close", onClose);
				resolve();
			};
			socket.addEventListener("close", onClose, { once: true });
			if (socket.readyState === WEB_SOCKET_OPEN || socket.readyState === WEB_SOCKET_CONNECTING) socket.close();
			else resolve();
		});
	}
	assertBrowserSafeTransportConfig() {
		if (require_utils.hasHeaders(this.defaultHeaders) || this.onRequest != null) throw new Error("Browser WebSocket protocol transport does not support defaultHeaders or onRequest hooks. Supply a custom protocolWebSocketFactory if you need custom WebSocket setup.");
	}
	async sendCommand(command) {
		let socket = this.socket;
		if (this.reconnectInFlight != null && (socket == null || socket.readyState !== WEB_SOCKET_OPEN)) {
			await this.reconnectInFlight.catch(() => void 0);
			socket = this.socket;
		}
		if (socket == null || socket.readyState !== WEB_SOCKET_OPEN) throw new Error("Protocol WebSocket is not open.");
		return await new Promise((resolve, reject) => {
			this.pending.set(command.id, {
				resolve,
				reject
			});
			try {
				socket.send(JSON.stringify(command));
			} catch (error) {
				this.pending.delete(command.id);
				reject(require_utils.toError(error));
			}
		});
	}
	#attachSocket(socket) {
		socket.addEventListener("message", this.handleMessage);
		socket.addEventListener("close", this.handleClose);
		socket.addEventListener("error", this.handleSocketError);
	}
	#detachSocket(socket) {
		socket.removeEventListener("message", this.handleMessage);
		socket.removeEventListener("close", this.handleClose);
		socket.removeEventListener("error", this.handleSocketError);
	}
	handleMessage = (event) => {
		let payload;
		try {
			payload = JSON.parse(String(event.data));
		} catch {
			return;
		}
		if (require_utils.isRecord(payload) && typeof payload.id === "number" && (payload.type === "success" || payload.type === "error")) {
			const pending = this.pending.get(payload.id);
			if (pending) {
				this.pending.delete(payload.id);
				pending.resolve(payload);
			}
			return;
		}
		if (require_utils.isRecord(payload) && payload.type === "event") this.queue.push(payload);
	};
	handleClose = () => {
		const socket = this.socket;
		if (socket != null) this.#detachSocket(socket);
		this.socket = null;
		if (this.intentionalClose || this.closed) {
			this.queue.close();
			return;
		}
		this.#handleUnexpectedDisconnect(/* @__PURE__ */ new Error("Protocol WebSocket closed unexpectedly."));
	};
	handleSocketError = () => {
		if (this.closed || this.intentionalClose) return;
		this.#handleUnexpectedDisconnect(/* @__PURE__ */ new Error("Protocol WebSocket encountered an error."));
	};
	#handleUnexpectedDisconnect(cause) {
		const error = require_utils.toError(cause);
		for (const { reject } of this.pending.values()) reject(error);
		this.pending.clear();
		if (this.maxReconnectAttempts <= 0) {
			this.queue.close(error);
			return;
		}
		this.#scheduleReconnect(cause);
	}
	#scheduleReconnect(cause) {
		if (this.closed || this.intentionalClose) return;
		if (this.reconnectInFlight != null) return;
		this.reconnectInFlight = this.#runReconnectLoop(cause).finally(() => {
			this.reconnectInFlight = null;
		});
	}
	async #runReconnectLoop(initialCause) {
		let lastError = initialCause;
		for (let attempt = 1; attempt <= this.maxReconnectAttempts; attempt += 1) {
			if (this.closed || this.intentionalClose) return;
			this.onReconnect?.({
				attempt,
				cause: lastError
			});
			const delay = this.reconnectDelayMs(attempt);
			if (delay > 0) await new Promise((resolve) => {
				setTimeout(resolve, delay);
			});
			if (this.closed || this.intentionalClose) return;
			try {
				await this.open();
				if (this.onReconnected) await this.onReconnected();
				return;
			} catch (error) {
				lastError = error;
			}
		}
		this.queue.close(new require_error.MaxWebSocketReconnectAttemptsError(this.maxReconnectAttempts, lastError));
	}
};
//#endregion
exports.ProtocolWebSocketTransportAdapter = ProtocolWebSocketTransportAdapter;

//# sourceMappingURL=websocket.cjs.map