//#region src/client.ts
const WS_CONNECTING = 0;
const WS_OPEN = 1;
const DEFAULT_RECONNECT = {
	initialDelayMs: 500,
	maxDelayMs: 3e4,
	factor: 2,
	jitter: .2
};
function defaultParse(data) {
	if (typeof data !== "string") return null;
	let parsed;
	try {
		parsed = JSON.parse(data);
	} catch {
		return null;
	}
	if (typeof parsed !== "object" || parsed === null) return null;
	const candidate = parsed;
	if (typeof candidate.v !== "number" || !Number.isFinite(candidate.v)) return null;
	if (typeof candidate.type !== "string") return null;
	return parsed;
}
function normalizeReconnect(policy) {
	const initialDelayMs = Math.max(0, policy?.initialDelayMs ?? DEFAULT_RECONNECT.initialDelayMs);
	return {
		initialDelayMs,
		maxDelayMs: Math.max(initialDelayMs, policy?.maxDelayMs ?? DEFAULT_RECONNECT.maxDelayMs),
		factor: Math.max(1, policy?.factor ?? DEFAULT_RECONNECT.factor),
		jitter: Math.min(1, Math.max(0, policy?.jitter ?? DEFAULT_RECONNECT.jitter))
	};
}
function createRealtimeClient(options) {
	const parse = options.parse ?? ((data) => defaultParse(data));
	const createSocket = options.createWebSocket ?? ((url, protocols) => new WebSocket(url, protocols));
	const random = options.random ?? Math.random;
	const reconnectPolicy = options.reconnect === false ? null : normalizeReconnect(options.reconnect);
	let status = "idle";
	let lastFrame = null;
	let socket = null;
	let retainCount = 0;
	let reconnectAttempt = 0;
	let reconnectTimer = null;
	let terminal = false;
	let generation = 0;
	const frameListeners = /* @__PURE__ */ new Set();
	const statusListeners = /* @__PURE__ */ new Set();
	const transition = (next) => {
		if (status === next) return;
		status = next;
		for (const listener of [...statusListeners]) listener(next);
	};
	const cancelReconnect = () => {
		if (reconnectTimer !== null) {
			clearTimeout(reconnectTimer);
			reconnectTimer = null;
		}
	};
	const detachAndCloseSocket = () => {
		const current = socket;
		socket = null;
		generation += 1;
		if (!current) return;
		current.onopen = null;
		current.onclose = null;
		current.onmessage = null;
		current.onerror = null;
		if (current.readyState === WS_OPEN || current.readyState === WS_CONNECTING) current.close(1e3);
	};
	const reconnectDelay = () => {
		if (!reconnectPolicy) return 0;
		const base = Math.min(reconnectPolicy.maxDelayMs, reconnectPolicy.initialDelayMs * reconnectPolicy.factor ** reconnectAttempt);
		reconnectAttempt += 1;
		const spread = base * reconnectPolicy.jitter;
		const sample = Math.min(1, Math.max(0, random()));
		return Math.max(0, base - spread + 2 * spread * sample);
	};
	const scheduleReconnect = () => {
		if (terminal || retainCount === 0 || reconnectPolicy === null || reconnectTimer !== null) return;
		transition("reconnecting");
		const delay = reconnectDelay();
		reconnectTimer = setTimeout(() => {
			reconnectTimer = null;
			connect(true);
		}, delay);
	};
	const connect = (isReconnect = false) => {
		if (terminal || retainCount === 0 || socket !== null) return;
		cancelReconnect();
		transition(isReconnect ? "reconnecting" : "connecting");
		const currentGeneration = ++generation;
		const ws = createSocket(options.url, options.protocols);
		socket = ws;
		ws.onopen = () => {
			if (terminal || socket !== ws || generation !== currentGeneration) return;
			reconnectAttempt = 0;
			transition("open");
		};
		ws.onmessage = (event) => {
			if (terminal || socket !== ws || generation !== currentGeneration) return;
			const frame = parse(event.data);
			if (frame === null) return;
			lastFrame = frame;
			for (const listener of [...frameListeners]) listener(frame);
		};
		ws.onclose = () => {
			if (socket !== ws || generation !== currentGeneration) return;
			socket = null;
			if (terminal) {
				transition("closed");
				return;
			}
			if (retainCount === 0) {
				transition("idle");
				return;
			}
			if (reconnectPolicy === null) {
				transition("closed");
				return;
			}
			scheduleReconnect();
		};
		ws.onerror = () => {};
	};
	return {
		retain() {
			if (terminal) return () => {};
			retainCount += 1;
			if (retainCount === 1) connect(false);
			let released = false;
			return () => {
				if (released) return;
				released = true;
				retainCount = Math.max(0, retainCount - 1);
				if (retainCount !== 0 || terminal) return;
				cancelReconnect();
				reconnectAttempt = 0;
				detachAndCloseSocket();
				transition("idle");
			};
		},
		subscribe(listener) {
			frameListeners.add(listener);
			return () => frameListeners.delete(listener);
		},
		subscribeStatus(listener) {
			statusListeners.add(listener);
			return () => statusListeners.delete(listener);
		},
		getStatus() {
			return status;
		},
		getLastFrame() {
			return lastFrame;
		},
		send(data) {
			if (!socket || socket.readyState !== WS_OPEN) return false;
			socket.send(data);
			return true;
		},
		close() {
			if (terminal) return;
			terminal = true;
			retainCount = 0;
			cancelReconnect();
			frameListeners.clear();
			detachAndCloseSocket();
			transition("closed");
			statusListeners.clear();
		}
	};
}
//#endregion
export { createRealtimeClient };
