//#region node_modules/@supabase/phoenix/priv/static/phoenix.mjs
var closure = (value) => {
	if (typeof value === "function") return value;
	else {
		let closure2 = function() {
			return value;
		};
		return closure2;
	}
};
var globalSelf = typeof self !== "undefined" ? self : null;
var phxWindow = typeof window !== "undefined" ? window : null;
var global = globalSelf || phxWindow || globalThis;
var DEFAULT_VSN = "2.0.0";
var DEFAULT_TIMEOUT = 1e4;
var WS_CLOSE_NORMAL = 1e3;
var MAX_LONGPOLL_BATCH_SIZE = 100;
var SOCKET_STATES = (
/** @type {const} */
{
	connecting: 0,
	open: 1,
	closing: 2,
	closed: 3
});
var CHANNEL_STATES = (
/** @type {const} */
{
	closed: "closed",
	errored: "errored",
	joined: "joined",
	joining: "joining",
	leaving: "leaving"
});
var CHANNEL_EVENTS = (
/** @type {const} */
{
	close: "phx_close",
	error: "phx_error",
	join: "phx_join",
	reply: "phx_reply",
	leave: "phx_leave"
});
var TRANSPORTS = (
/** @type {const} */
{
	longpoll: "longpoll",
	websocket: "websocket"
});
var XHR_STATES = (
/** @type {const} */
{ complete: 4 });
var AUTH_TOKEN_PREFIX = "base64url.bearer.phx.";
var Push = class {
	/**
	* Initializes the Push
	* @param {Channel} channel - The Channel
	* @param {ChannelEvent} event - The event, for example `"phx_join"`
	* @param {() => Record<string, unknown>} payload - The payload, for example `{user_id: 123}`
	* @param {number} timeout - The push timeout in milliseconds
	*/
	constructor(channel, event, payload, timeout) {
		this.channel = channel;
		this.event = event;
		this.payload = payload || function() {
			return {};
		};
		this.receivedResp = null;
		this.timeout = timeout;
		this.timeoutTimer = null;
		this.recHooks = [];
		this.sent = false;
		this.ref = void 0;
	}
	/**
	*
	* @param {number} timeout
	*/
	resend(timeout) {
		this.timeout = timeout;
		this.reset();
		this.send();
	}
	/**
	*
	*/
	send() {
		if (this.hasReceived("timeout")) return;
		this.startTimeout();
		this.sent = true;
		this.channel.socket.push({
			topic: this.channel.topic,
			event: this.event,
			payload: this.payload(),
			ref: this.ref,
			join_ref: this.channel.joinRef()
		});
	}
	/**
	*
	* @param {string} status
	* @param {(response: any) => void} callback
	*/
	receive(status, callback) {
		if (this.hasReceived(status)) callback(this.receivedResp.response);
		this.recHooks.push({
			status,
			callback
		});
		return this;
	}
	reset() {
		this.cancelRefEvent();
		this.ref = null;
		this.refEvent = null;
		this.receivedResp = null;
		this.sent = false;
	}
	destroy() {
		this.cancelRefEvent();
		this.cancelTimeout();
	}
	/**
	* @private
	*/
	matchReceive({ status, response, _ref }) {
		this.recHooks.filter((h) => h.status === status).forEach((h) => h.callback(response));
	}
	/**
	* @private
	*/
	cancelRefEvent() {
		if (!this.refEvent) return;
		this.channel.off(this.refEvent);
	}
	cancelTimeout() {
		clearTimeout(this.timeoutTimer);
		this.timeoutTimer = null;
	}
	startTimeout() {
		if (this.timeoutTimer) this.cancelTimeout();
		this.ref = this.channel.socket.makeRef();
		this.refEvent = this.channel.replyEventName(this.ref);
		this.channel.on(this.refEvent, (payload) => {
			this.cancelRefEvent();
			this.cancelTimeout();
			this.receivedResp = payload;
			this.matchReceive(payload);
		});
		this.timeoutTimer = setTimeout(() => {
			this.trigger("timeout", {});
		}, this.timeout);
	}
	/**
	* @private
	*/
	hasReceived(status) {
		return this.receivedResp && this.receivedResp.status === status;
	}
	trigger(status, response) {
		this.channel.trigger(this.refEvent, {
			status,
			response
		});
	}
};
var Timer = class {
	/**
	* @param {() => void} callback
	* @param {(tries: number) => number} timerCalc
	*/
	constructor(callback, timerCalc) {
		this.callback = callback;
		this.timerCalc = timerCalc;
		this.timer = void 0;
		this.tries = 0;
	}
	reset() {
		this.tries = 0;
		clearTimeout(this.timer);
	}
	/**
	* Cancels any previous scheduleTimeout and schedules callback
	*/
	scheduleTimeout() {
		clearTimeout(this.timer);
		this.timer = setTimeout(() => {
			this.tries = this.tries + 1;
			this.callback();
		}, this.timerCalc(this.tries + 1));
	}
};
var Channel = class {
	/**
	* @param {string} topic
	* @param {Params | (() => Params)} params
	* @param {Socket} socket
	*/
	constructor(topic, params, socket) {
		this.state = CHANNEL_STATES.closed;
		this.topic = topic;
		this.params = closure(params || {});
		this.socket = socket;
		this.bindings = [];
		this.bindingRef = 0;
		this.timeout = this.socket.timeout;
		this.joinedOnce = false;
		this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout);
		this.pushBuffer = [];
		this.stateChangeRefs = [];
		this.rejoinTimer = new Timer(() => {
			if (this.socket.isConnected()) this.rejoin();
		}, this.socket.rejoinAfterMs);
		this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()));
		this.stateChangeRefs.push(this.socket.onOpen(() => {
			this.rejoinTimer.reset();
			if (this.isErrored()) this.rejoin();
		}));
		this.joinPush.receive("ok", () => {
			this.state = CHANNEL_STATES.joined;
			this.rejoinTimer.reset();
			this.pushBuffer.forEach((pushEvent) => pushEvent.send());
			this.pushBuffer = [];
		});
		this.joinPush.receive("error", (reason) => {
			this.state = CHANNEL_STATES.errored;
			if (this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason);
			if (this.socket.isConnected()) this.rejoinTimer.scheduleTimeout();
		});
		this.onClose(() => {
			this.rejoinTimer.reset();
			if (this.socket.hasLogger()) this.socket.log("channel", `close ${this.topic}`);
			this.state = CHANNEL_STATES.closed;
			this.socket.remove(this);
		});
		this.onError((reason) => {
			if (this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason);
			if (this.isJoining()) this.joinPush.reset();
			this.state = CHANNEL_STATES.errored;
			if (this.socket.isConnected()) this.rejoinTimer.scheduleTimeout();
		});
		this.joinPush.receive("timeout", () => {
			if (this.socket.hasLogger()) this.socket.log("channel", `timeout ${this.topic}`, this.joinPush.timeout);
			new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout).send();
			this.state = CHANNEL_STATES.errored;
			this.joinPush.reset();
			if (this.socket.isConnected()) this.rejoinTimer.scheduleTimeout();
		});
		this.on(CHANNEL_EVENTS.reply, (payload, ref) => {
			this.trigger(this.replyEventName(ref), payload);
		});
	}
	/**
	* Join the channel
	* @param {number} timeout
	* @returns {Push}
	*/
	join(timeout = this.timeout) {
		if (this.joinedOnce) throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");
		else {
			this.timeout = timeout;
			this.joinedOnce = true;
			this.rejoin();
			return this.joinPush;
		}
	}
	/**
	* Teardown the channel.
	*
	* Destroys and stops related timers.
	*/
	teardown() {
		this.pushBuffer.forEach((push) => push.destroy());
		this.pushBuffer = [];
		this.rejoinTimer.reset();
		this.joinPush.destroy();
		this.state = CHANNEL_STATES.closed;
		this.bindings = [];
	}
	/**
	* Hook into channel close
	* @param {ChannelBindingCallback} callback
	*/
	onClose(callback) {
		this.on(CHANNEL_EVENTS.close, callback);
	}
	/**
	* Hook into channel errors
	* @param {ChannelOnErrorCallback} callback
	* @return {number}
	*/
	onError(callback) {
		return this.on(CHANNEL_EVENTS.error, (reason) => callback(reason));
	}
	/**
	* Subscribes on channel events
	*
	* Subscription returns a ref counter, which can be used later to
	* unsubscribe the exact event listener
	*
	* @example
	* const ref1 = channel.on("event", do_stuff)
	* const ref2 = channel.on("event", do_other_stuff)
	* channel.off("event", ref1)
	* // Since unsubscription, do_stuff won't fire,
	* // while do_other_stuff will keep firing on the "event"
	*
	* @param {string} event
	* @param {ChannelBindingCallback} callback
	* @returns {number} ref
	*/
	on(event, callback) {
		let ref = this.bindingRef++;
		this.bindings.push({
			event,
			ref,
			callback
		});
		return ref;
	}
	/**
	* Unsubscribes off of channel events
	*
	* Use the ref returned from a channel.on() to unsubscribe one
	* handler, or pass nothing for the ref to unsubscribe all
	* handlers for the given event.
	*
	* @example
	* // Unsubscribe the do_stuff handler
	* const ref1 = channel.on("event", do_stuff)
	* channel.off("event", ref1)
	*
	* // Unsubscribe all handlers from event
	* channel.off("event")
	*
	* @param {string} event
	* @param {number} [ref]
	*/
	off(event, ref) {
		this.bindings = this.bindings.filter((bind) => {
			return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref));
		});
	}
	/**
	* @private
	*/
	canPush() {
		return this.socket.isConnected() && this.isJoined();
	}
	/**
	* Sends a message `event` to phoenix with the payload `payload`.
	* Phoenix receives this in the `handle_in(event, payload, socket)`
	* function. if phoenix replies or it times out (default 10000ms),
	* then optionally the reply can be received.
	*
	* @example
	* channel.push("event")
	*   .receive("ok", payload => console.log("phoenix replied:", payload))
	*   .receive("error", err => console.log("phoenix errored", err))
	*   .receive("timeout", () => console.log("timed out pushing"))
	* @param {string} event
	* @param {Object} payload
	* @param {number} [timeout]
	* @returns {Push}
	*/
	push(event, payload, timeout = this.timeout) {
		payload = payload || {};
		if (!this.joinedOnce) throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`);
		let pushEvent = new Push(this, event, function() {
			return payload;
		}, timeout);
		if (this.canPush()) pushEvent.send();
		else {
			pushEvent.startTimeout();
			this.pushBuffer.push(pushEvent);
		}
		return pushEvent;
	}
	/** Leaves the channel
	*
	* Unsubscribes from server events, and
	* instructs channel to terminate on server
	*
	* Triggers onClose() hooks
	*
	* To receive leave acknowledgements, use the `receive`
	* hook to bind to the server ack, ie:
	*
	* @example
	* channel.leave().receive("ok", () => alert("left!") )
	*
	* @param {number} timeout
	* @returns {Push}
	*/
	leave(timeout = this.timeout) {
		this.rejoinTimer.reset();
		this.joinPush.cancelTimeout();
		this.state = CHANNEL_STATES.leaving;
		let onClose = () => {
			if (this.socket.hasLogger()) this.socket.log("channel", `leave ${this.topic}`);
			this.trigger(CHANNEL_EVENTS.close, "leave");
		};
		let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout);
		leavePush.receive("ok", () => onClose()).receive("timeout", () => onClose());
		leavePush.send();
		if (!this.canPush()) leavePush.trigger("ok", {});
		return leavePush;
	}
	/**
	* Overridable message hook
	*
	* Receives all events for specialized message handling
	* before dispatching to the channel callbacks.
	*
	* Must return the payload, modified or unmodified
	* @type{ChannelOnMessage}
	*/
	onMessage(_event, payload, _ref) {
		return payload;
	}
	/**
	* Overridable filter hook
	*
	* If this function returns `true`, `binding`'s callback will be called.
	*
	* @type{ChannelFilterBindings}
	*/
	filterBindings(_binding, _payload, _ref) {
		return true;
	}
	isMember(topic, event, payload, joinRef) {
		if (this.topic !== topic) return false;
		if (joinRef && joinRef !== this.joinRef()) {
			if (this.socket.hasLogger()) this.socket.log("channel", "dropping outdated message", {
				topic,
				event,
				payload,
				joinRef
			});
			return false;
		} else return true;
	}
	joinRef() {
		return this.joinPush.ref;
	}
	/**
	* @private
	*/
	rejoin(timeout = this.timeout) {
		if (this.isLeaving()) return;
		this.socket.leaveOpenTopic(this.topic);
		this.state = CHANNEL_STATES.joining;
		this.joinPush.resend(timeout);
	}
	/**
	* @param {string} event
	* @param {unknown} [payload]
	* @param {?string} [ref]
	* @param {?string} [joinRef]
	*/
	trigger(event, payload, ref, joinRef) {
		let handledPayload = this.onMessage(event, payload, ref, joinRef);
		if (payload && !handledPayload) throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");
		let eventBindings = this.bindings.filter((bind) => bind.event === event && this.filterBindings(bind, payload, ref));
		for (let i = 0; i < eventBindings.length; i++) eventBindings[i].callback(handledPayload, ref, joinRef || this.joinRef());
	}
	/**
	* @param {string} ref
	*/
	replyEventName(ref) {
		return `chan_reply_${ref}`;
	}
	isClosed() {
		return this.state === CHANNEL_STATES.closed;
	}
	isErrored() {
		return this.state === CHANNEL_STATES.errored;
	}
	isJoined() {
		return this.state === CHANNEL_STATES.joined;
	}
	isJoining() {
		return this.state === CHANNEL_STATES.joining;
	}
	isLeaving() {
		return this.state === CHANNEL_STATES.leaving;
	}
};
var Ajax = class {
	static request(method, endPoint, headers, body, timeout, ontimeout, callback) {
		if (global.XDomainRequest) {
			let req = new global.XDomainRequest();
			return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
		} else if (global.XMLHttpRequest) {
			let req = new global.XMLHttpRequest();
			return this.xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback);
		} else if (global.fetch && global.AbortController) return this.fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback);
		else throw new Error("No suitable XMLHttpRequest implementation found");
	}
	static fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback) {
		let options = {
			method,
			headers,
			body
		};
		let controller = null;
		if (timeout) {
			controller = new AbortController();
			setTimeout(() => controller.abort(), timeout);
			options.signal = controller.signal;
		}
		global.fetch(endPoint, options).then((response) => response.text()).then((data) => this.parseJSON(data)).then((data) => callback && callback(data)).catch((err) => {
			if (err.name === "AbortError" && ontimeout) ontimeout();
			else callback && callback(null);
		});
		return controller;
	}
	static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) {
		req.timeout = timeout;
		req.open(method, endPoint);
		req.onload = () => {
			let response = this.parseJSON(req.responseText);
			callback && callback(response);
		};
		if (ontimeout) req.ontimeout = ontimeout;
		req.onprogress = () => {};
		req.send(body);
		return req;
	}
	static xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback) {
		req.open(method, endPoint, true);
		req.timeout = timeout;
		for (let [key, value] of Object.entries(headers)) req.setRequestHeader(key, value);
		req.onerror = () => callback && callback(null);
		req.onreadystatechange = () => {
			if (req.readyState === XHR_STATES.complete && callback) callback(this.parseJSON(req.responseText));
		};
		if (ontimeout) req.ontimeout = ontimeout;
		req.send(body);
		return req;
	}
	static parseJSON(resp) {
		if (!resp || resp === "") return null;
		try {
			return JSON.parse(resp);
		} catch {
			console && console.log("failed to parse JSON response", resp);
			return null;
		}
	}
	static serialize(obj, parentKey) {
		let queryStr = [];
		for (var key in obj) {
			if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
			let paramKey = parentKey ? `${parentKey}[${key}]` : key;
			let paramVal = obj[key];
			if (typeof paramVal === "object") queryStr.push(this.serialize(paramVal, paramKey));
			else queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal));
		}
		return queryStr.join("&");
	}
	static appendParams(url, params) {
		if (Object.keys(params).length === 0) return url;
		return `${url}${url.match(/\?/) ? "&" : "?"}${this.serialize(params)}`;
	}
};
var arrayBufferToBase64 = (buffer) => {
	let binary = "";
	let bytes = new Uint8Array(buffer);
	let len = bytes.byteLength;
	for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
	return btoa(binary);
};
var LongPoll = class {
	constructor(endPoint, protocols) {
		if (protocols && protocols.length === 2 && protocols[1].startsWith(AUTH_TOKEN_PREFIX)) this.authToken = atob(protocols[1].slice(AUTH_TOKEN_PREFIX.length));
		this.endPoint = null;
		this.token = null;
		this.skipHeartbeat = true;
		this.reqs = /* @__PURE__ */ new Set();
		this.awaitingBatchAck = false;
		this.currentBatch = null;
		this.currentBatchTimer = null;
		this.batchBuffer = [];
		this.onopen = function() {};
		this.onerror = function() {};
		this.onmessage = function() {};
		this.onclose = function() {};
		this.pollEndpoint = this.normalizeEndpoint(endPoint);
		this.readyState = SOCKET_STATES.connecting;
		setTimeout(() => this.poll(), 0);
	}
	normalizeEndpoint(endPoint) {
		return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll);
	}
	endpointURL() {
		return Ajax.appendParams(this.pollEndpoint, { token: this.token });
	}
	closeAndRetry(code, reason, wasClean) {
		this.close(code, reason, wasClean);
		this.readyState = SOCKET_STATES.connecting;
	}
	ontimeout() {
		this.onerror("timeout");
		this.closeAndRetry(1005, "timeout", false);
	}
	isActive() {
		return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting;
	}
	poll() {
		const headers = { "Accept": "application/json" };
		if (this.authToken) headers["X-Phoenix-AuthToken"] = this.authToken;
		this.ajax("GET", headers, null, () => this.ontimeout(), (resp) => {
			if (resp) {
				var { status, token, messages } = resp;
				if (status === 410 && this.token !== null) {
					this.onerror(410);
					this.closeAndRetry(3410, "session_gone", false);
					return;
				}
				this.token = token;
			} else status = 0;
			switch (status) {
				case 200:
					messages.forEach((msg) => {
						setTimeout(() => this.onmessage({ data: msg }), 0);
					});
					this.poll();
					break;
				case 204:
					this.poll();
					break;
				case 410:
					this.readyState = SOCKET_STATES.open;
					this.onopen({});
					this.poll();
					break;
				case 403:
					this.onerror(403);
					this.close(1008, "forbidden", false);
					break;
				case 0:
				case 500:
					this.onerror(500);
					this.closeAndRetry(1011, "internal server error", 500);
					break;
				default: throw new Error(`unhandled poll status ${status}`);
			}
		});
	}
	send(body) {
		if (typeof body !== "string") body = arrayBufferToBase64(body);
		if (this.currentBatch) this.currentBatch.push(body);
		else if (this.awaitingBatchAck) this.batchBuffer.push(body);
		else {
			this.currentBatch = [body];
			this.currentBatchTimer = setTimeout(() => {
				this.batchSend(this.currentBatch);
				this.currentBatch = null;
			}, 0);
		}
	}
	batchSend(messages, offset = 0) {
		this.awaitingBatchAck = true;
		const next = offset + MAX_LONGPOLL_BATCH_SIZE;
		const batch = messages.slice(offset, next);
		this.ajax("POST", { "Content-Type": "application/x-ndjson" }, batch.join("\n"), () => this.onerror("timeout"), (resp) => {
			if (!resp || resp.status !== 200) {
				this.awaitingBatchAck = false;
				this.onerror(resp && resp.status);
				this.closeAndRetry(1011, "internal server error", false);
			} else if (next < messages.length) this.batchSend(messages, next);
			else if (this.batchBuffer.length > 0) {
				this.batchSend(this.batchBuffer);
				this.batchBuffer = [];
			} else this.awaitingBatchAck = false;
		});
	}
	close(code, reason, wasClean) {
		for (let req of this.reqs) req.abort();
		this.readyState = SOCKET_STATES.closed;
		let opts = Object.assign({
			code: 1e3,
			reason: void 0,
			wasClean: true
		}, {
			code,
			reason,
			wasClean
		});
		this.batchBuffer = [];
		clearTimeout(this.currentBatchTimer);
		this.currentBatchTimer = null;
		if (typeof CloseEvent !== "undefined") this.onclose(new CloseEvent("close", opts));
		else this.onclose(opts);
	}
	ajax(method, headers, body, onCallerTimeout, callback) {
		let req;
		let ontimeout = () => {
			this.reqs.delete(req);
			onCallerTimeout();
		};
		req = Ajax.request(method, this.endpointURL(), headers, body, this.timeout, ontimeout, (resp) => {
			this.reqs.delete(req);
			if (this.isActive()) callback(resp);
		});
		this.reqs.add(req);
	}
};
var Presence = class _Presence {
	/**
	* Initializes the Presence
	* @param {Channel} channel - The Channel
	* @param {PresenceOptions} [opts] - The options, for example `{events: {state: "state", diff: "diff"}}`
	*/
	constructor(channel, opts = {}) {
		let events = opts.events || {
			state: "presence_state",
			diff: "presence_diff"
		};
		this.state = /* @__PURE__ */ Object.create(null);
		this.pendingDiffs = [];
		this.channel = channel;
		this.joinRef = null;
		this.caller = {
			onJoin: function() {},
			onLeave: function() {},
			onSync: function() {}
		};
		this.channel.on(events.state, (newState) => {
			let { onJoin, onLeave, onSync } = this.caller;
			this.joinRef = this.channel.joinRef();
			this.state = _Presence.syncState(this.state, newState, onJoin, onLeave);
			this.pendingDiffs.forEach((diff) => {
				this.state = _Presence.syncDiff(this.state, diff, onJoin, onLeave);
			});
			this.pendingDiffs = [];
			onSync();
		});
		this.channel.on(events.diff, (diff) => {
			let { onJoin, onLeave, onSync } = this.caller;
			if (this.inPendingSyncState()) this.pendingDiffs.push(diff);
			else {
				this.state = _Presence.syncDiff(this.state, diff, onJoin, onLeave);
				onSync();
			}
		});
	}
	/**
	* @param {PresenceOnJoin} callback
	*/
	onJoin(callback) {
		this.caller.onJoin = callback;
	}
	/**
	* @param {PresenceOnLeave} callback
	*/
	onLeave(callback) {
		this.caller.onLeave = callback;
	}
	/**
	* @param {PresenceOnSync} callback
	*/
	onSync(callback) {
		this.caller.onSync = callback;
	}
	/**
	* Returns the array of presences, with selected metadata.
	*
	* @template [T=PresenceState]
	* @param {((key: string, obj: PresenceState) => T)} [by]
	*
	* @returns {T[]}
	*/
	list(by) {
		return _Presence.list(this.state, by);
	}
	inPendingSyncState() {
		return !this.joinRef || this.joinRef !== this.channel.joinRef();
	}
	/**
	* Used to sync the list of presences on the server
	* with the client's state. An optional `onJoin` and `onLeave` callback can
	* be provided to react to changes in the client's local presences across
	* disconnects and reconnects with the server.
	*
	* @param {Record<string, PresenceState>} currentState
	* @param {Record<string, PresenceState>} newState
	* @param {PresenceOnJoin} onJoin
	* @param {PresenceOnLeave} onLeave
	*
	* @returns {Record<string, PresenceState>}
	*/
	static syncState(currentState, newState, onJoin, onLeave) {
		let state = this.toNullProtoObj(this.clone(currentState));
		newState = this.toNullProtoObj(newState);
		let joins = /* @__PURE__ */ Object.create(null);
		let leaves = /* @__PURE__ */ Object.create(null);
		this.map(state, (key, presence) => {
			if (!newState[key]) leaves[key] = presence;
		});
		this.map(newState, (key, newPresence) => {
			let currentPresence = state[key];
			if (currentPresence) {
				let newRefs = newPresence.metas.map((m) => m.phx_ref);
				let curRefs = currentPresence.metas.map((m) => m.phx_ref);
				let joinedMetas = newPresence.metas.filter((m) => curRefs.indexOf(m.phx_ref) < 0);
				let leftMetas = currentPresence.metas.filter((m) => newRefs.indexOf(m.phx_ref) < 0);
				if (joinedMetas.length > 0) {
					joins[key] = newPresence;
					joins[key].metas = joinedMetas;
				}
				if (leftMetas.length > 0) {
					leaves[key] = this.clone(currentPresence);
					leaves[key].metas = leftMetas;
				}
			} else joins[key] = newPresence;
		});
		return this.syncDiff(state, {
			joins,
			leaves
		}, onJoin, onLeave);
	}
	/**
	*
	* Used to sync a diff of presence join and leave
	* events from the server, as they happen. Like `syncState`, `syncDiff`
	* accepts optional `onJoin` and `onLeave` callbacks to react to a user
	* joining or leaving from a device.
	*
	* @param {Record<string, PresenceState>} state
	* @param {PresenceDiff} diff
	* @param {PresenceOnJoin} onJoin
	* @param {PresenceOnLeave} onLeave
	*
	* @returns {Record<string, PresenceState>}
	*/
	static syncDiff(state, diff, onJoin, onLeave) {
		state = this.toNullProtoObj(state);
		let { joins, leaves } = this.clone(diff);
		if (!onJoin) onJoin = function() {};
		if (!onLeave) onLeave = function() {};
		this.map(joins, (key, newPresence) => {
			let currentPresence = state[key];
			state[key] = this.clone(newPresence);
			if (currentPresence) {
				let joinedRefs = state[key].metas.map((m) => m.phx_ref);
				let curMetas = currentPresence.metas.filter((m) => joinedRefs.indexOf(m.phx_ref) < 0);
				state[key].metas.unshift(...curMetas);
			}
			onJoin(key, currentPresence, newPresence);
		});
		this.map(leaves, (key, leftPresence) => {
			let currentPresence = state[key];
			if (!currentPresence) return;
			let refsToRemove = leftPresence.metas.map((m) => m.phx_ref);
			currentPresence.metas = currentPresence.metas.filter((p) => {
				return refsToRemove.indexOf(p.phx_ref) < 0;
			});
			onLeave(key, currentPresence, leftPresence);
			if (currentPresence.metas.length === 0) delete state[key];
		});
		return state;
	}
	/**
	* Returns the array of presences, with selected metadata.
	*
	* @template [T=PresenceState]
	* @param {Record<string, PresenceState>} presences
	* @param {((key: string, obj: PresenceState) => T)} [chooser]
	*
	* @returns {T[]}
	*/
	static list(presences, chooser) {
		if (!chooser) chooser = function(key, pres) {
			return pres;
		};
		return this.map(presences, (key, presence) => {
			return chooser(key, presence);
		});
	}
	/**
	* @template T
	* @param {Record<string, PresenceState>} obj
	* @param {(key: string, obj: PresenceState) => T} func
	*/
	static map(obj, func) {
		return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key]));
	}
	static toNullProtoObj(obj) {
		if (Object.getPrototypeOf(obj) === null) return obj;
		let cleaned = /* @__PURE__ */ Object.create(null);
		Object.getOwnPropertyNames(obj).forEach((key) => {
			cleaned[key] = obj[key];
		});
		return cleaned;
	}
	/**
	* @template T
	* @param {T} obj
	* @returns {T}
	*/
	static clone(obj) {
		return JSON.parse(JSON.stringify(obj));
	}
};
var serializer_default = {
	HEADER_LENGTH: 1,
	META_LENGTH: 4,
	KINDS: {
		push: 0,
		reply: 1,
		broadcast: 2
	},
	/**
	* @template T
	* @param {Message<Record<string, any>>} msg
	* @param {(msg: ArrayBuffer | string) => T} callback
	* @returns {T}
	*/
	encode(msg, callback) {
		if (msg.payload.constructor === ArrayBuffer) return callback(this.binaryEncode(msg));
		else {
			let payload = [
				msg.join_ref,
				msg.ref,
				msg.topic,
				msg.event,
				msg.payload
			];
			return callback(JSON.stringify(payload));
		}
	},
	/**
	* @template T
	* @param {ArrayBuffer | string} rawPayload
	* @param {(msg: Message<unknown>) => T} callback
	* @returns {T}
	*/
	decode(rawPayload, callback) {
		if (rawPayload.constructor === ArrayBuffer) return callback(this.binaryDecode(rawPayload));
		else {
			let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload);
			return callback({
				join_ref,
				ref,
				topic,
				event,
				payload
			});
		}
	},
	/** @private */
	binaryEncode(message) {
		let { join_ref, ref, event, topic, payload } = message;
		let encoder = new TextEncoder();
		let joinRefBytes = encoder.encode(join_ref);
		let refBytes = encoder.encode(ref);
		let topicBytes = encoder.encode(topic);
		let eventBytes = encoder.encode(event);
		this.assertFieldSize(joinRefBytes.byteLength, "join_ref");
		this.assertFieldSize(refBytes.byteLength, "ref");
		this.assertFieldSize(topicBytes.byteLength, "topic");
		this.assertFieldSize(eventBytes.byteLength, "event");
		let metaLength = this.META_LENGTH + joinRefBytes.byteLength + refBytes.byteLength + topicBytes.byteLength + eventBytes.byteLength;
		let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength);
		let headerBytes = new Uint8Array(header);
		let view = new DataView(header);
		let offset = 0;
		view.setUint8(offset++, this.KINDS.push);
		view.setUint8(offset++, joinRefBytes.byteLength);
		view.setUint8(offset++, refBytes.byteLength);
		view.setUint8(offset++, topicBytes.byteLength);
		view.setUint8(offset++, eventBytes.byteLength);
		headerBytes.set(joinRefBytes, offset);
		offset += joinRefBytes.byteLength;
		headerBytes.set(refBytes, offset);
		offset += refBytes.byteLength;
		headerBytes.set(topicBytes, offset);
		offset += topicBytes.byteLength;
		headerBytes.set(eventBytes, offset);
		offset += eventBytes.byteLength;
		var combined = new Uint8Array(header.byteLength + payload.byteLength);
		combined.set(headerBytes, 0);
		combined.set(new Uint8Array(payload), header.byteLength);
		return combined.buffer;
	},
	assertFieldSize(size, name) {
		if (size > 255) throw new Error(`unable to convert ${name} to binary: must be less than or equal to 255 bytes, but is ${size} bytes`);
	},
	/**
	* @private
	*/
	binaryDecode(buffer) {
		let view = new DataView(buffer);
		let kind = view.getUint8(0);
		let decoder = new TextDecoder();
		switch (kind) {
			case this.KINDS.push: return this.decodePush(buffer, view, decoder);
			case this.KINDS.reply: return this.decodeReply(buffer, view, decoder);
			case this.KINDS.broadcast: return this.decodeBroadcast(buffer, view, decoder);
		}
	},
	/** @private */
	decodePush(buffer, view, decoder) {
		let joinRefSize = view.getUint8(1);
		let topicSize = view.getUint8(2);
		let eventSize = view.getUint8(3);
		let offset = this.HEADER_LENGTH + this.META_LENGTH - 1;
		let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize));
		offset = offset + joinRefSize;
		let topic = decoder.decode(buffer.slice(offset, offset + topicSize));
		offset = offset + topicSize;
		let event = decoder.decode(buffer.slice(offset, offset + eventSize));
		offset = offset + eventSize;
		return {
			join_ref: joinRef,
			ref: null,
			topic,
			event,
			payload: buffer.slice(offset, buffer.byteLength)
		};
	},
	/** @private */
	decodeReply(buffer, view, decoder) {
		let joinRefSize = view.getUint8(1);
		let refSize = view.getUint8(2);
		let topicSize = view.getUint8(3);
		let eventSize = view.getUint8(4);
		let offset = this.HEADER_LENGTH + this.META_LENGTH;
		let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize));
		offset = offset + joinRefSize;
		let ref = decoder.decode(buffer.slice(offset, offset + refSize));
		offset = offset + refSize;
		let topic = decoder.decode(buffer.slice(offset, offset + topicSize));
		offset = offset + topicSize;
		let event = decoder.decode(buffer.slice(offset, offset + eventSize));
		offset = offset + eventSize;
		let payload = {
			status: event,
			response: buffer.slice(offset, buffer.byteLength)
		};
		return {
			join_ref: joinRef,
			ref,
			topic,
			event: CHANNEL_EVENTS.reply,
			payload
		};
	},
	/** @private */
	decodeBroadcast(buffer, view, decoder) {
		let topicSize = view.getUint8(1);
		let eventSize = view.getUint8(2);
		let offset = this.HEADER_LENGTH + 2;
		let topic = decoder.decode(buffer.slice(offset, offset + topicSize));
		offset = offset + topicSize;
		let event = decoder.decode(buffer.slice(offset, offset + eventSize));
		offset = offset + eventSize;
		return {
			join_ref: null,
			ref: null,
			topic,
			event,
			payload: buffer.slice(offset, buffer.byteLength)
		};
	}
};
var Socket = class {
	/** Initializes the Socket *
	*
	* For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
	*
	* @constructor
	* @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`,
	*                                               `"wss://example.com"`
	*                                               `"/socket"` (inherited host & protocol)
	* @param {SocketOptions} [opts] - Optional configuration
	*/
	constructor(endPoint, opts = {}) {
		this.stateChangeCallbacks = {
			open: [],
			close: [],
			error: [],
			message: []
		};
		this.channels = [];
		this.sendBuffer = [];
		this.ref = 0;
		this.fallbackRef = null;
		this.timeout = opts.timeout || DEFAULT_TIMEOUT;
		this.transport = opts.transport || global.WebSocket || LongPoll;
		this.conn = void 0;
		this.primaryPassedHealthCheck = false;
		this.longPollFallbackMs = opts.longPollFallbackMs;
		this.fallbackTimer = null;
		let envSessionStorage = null;
		try {
			envSessionStorage = global && global.sessionStorage;
		} catch {}
		this.sessionStore = opts.sessionStorage || envSessionStorage;
		this.establishedConnections = 0;
		this.defaultEncoder = serializer_default.encode.bind(serializer_default);
		this.defaultDecoder = serializer_default.decode.bind(serializer_default);
		this.closeWasClean = true;
		this.disconnecting = false;
		this.binaryType = opts.binaryType || "arraybuffer";
		this.connectClock = 1;
		this.pageHidden = false;
		this.encode = void 0;
		this.decode = void 0;
		if (this.transport !== LongPoll) {
			this.encode = opts.encode || this.defaultEncoder;
			this.decode = opts.decode || this.defaultDecoder;
		} else {
			this.encode = this.defaultEncoder;
			this.decode = this.defaultDecoder;
		}
		let awaitingConnectionOnPageShow = null;
		if (phxWindow && phxWindow.addEventListener) {
			phxWindow.addEventListener("pagehide", (_e) => {
				if (this.conn) {
					this.disconnect();
					awaitingConnectionOnPageShow = this.connectClock;
				}
			});
			phxWindow.addEventListener("pageshow", (_e) => {
				if (awaitingConnectionOnPageShow === this.connectClock) {
					awaitingConnectionOnPageShow = null;
					this.connect();
				}
			});
			phxWindow.addEventListener("visibilitychange", () => {
				if (document.visibilityState === "hidden") this.pageHidden = true;
				else {
					this.pageHidden = false;
					if (!this.isConnected() && !this.closeWasClean) this.teardown(() => this.connect());
				}
			});
		}
		this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 3e4;
		this.autoSendHeartbeat = opts.autoSendHeartbeat ?? true;
		this.heartbeatCallback = opts.heartbeatCallback ?? (() => {});
		this.rejoinAfterMs = (tries) => {
			if (opts.rejoinAfterMs) return opts.rejoinAfterMs(tries);
			else return [
				1e3,
				2e3,
				5e3
			][tries - 1] || 1e4;
		};
		this.reconnectAfterMs = (tries) => {
			if (opts.reconnectAfterMs) return opts.reconnectAfterMs(tries);
			else return [
				10,
				50,
				100,
				150,
				200,
				250,
				500,
				1e3,
				2e3
			][tries - 1] || 5e3;
		};
		this.logger = opts.logger || null;
		if (!this.logger && opts.debug) this.logger = (kind, msg, data) => {
			console.log(`${kind}: ${msg}`, data);
		};
		this.longpollerTimeout = opts.longpollerTimeout || 2e4;
		this.params = closure(opts.params || {});
		this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`;
		this.vsn = opts.vsn || DEFAULT_VSN;
		this.heartbeatTimeoutTimer = null;
		this.heartbeatTimer = null;
		this.heartbeatSentAt = null;
		this.pendingHeartbeatRef = null;
		this.reconnectTimer = new Timer(() => {
			if (this.pageHidden) {
				this.log("Not reconnecting as page is hidden!");
				this.teardown();
				return;
			}
			this.teardown(async () => {
				if (opts.beforeReconnect) await opts.beforeReconnect();
				this.connect();
			});
		}, this.reconnectAfterMs);
		this.authToken = opts.authToken && closure(opts.authToken);
	}
	/**
	* Returns the LongPoll transport reference
	*/
	getLongPollTransport() {
		return LongPoll;
	}
	/**
	* Disconnects and replaces the active transport
	*
	* @param {SocketTransport} newTransport - The new transport class to instantiate
	*
	*/
	replaceTransport(newTransport) {
		this.connectClock++;
		this.closeWasClean = true;
		clearTimeout(this.fallbackTimer);
		this.reconnectTimer.reset();
		if (this.conn) {
			this.conn.close();
			this.conn = null;
		}
		this.transport = newTransport;
	}
	/**
	* Returns the socket protocol
	*
	* @returns {"wss" | "ws"}
	*/
	protocol() {
		return location.protocol.match(/^https/) ? "wss" : "ws";
	}
	/**
	* The fully qualified socket url
	*
	* @returns {string}
	*/
	endPointURL() {
		let uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params()), { vsn: this.vsn });
		if (uri.charAt(0) !== "/") return uri;
		if (uri.charAt(1) === "/") return `${this.protocol()}:${uri}`;
		return `${this.protocol()}://${location.host}${uri}`;
	}
	/**
	* Disconnects the socket
	*
	* See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.
	*
	* @param {() => void} [callback] - Optional callback which is called after socket is disconnected.
	* @param {number} [code] - A status code for disconnection (Optional).
	* @param {string} [reason] - A textual description of the reason to disconnect. (Optional)
	*/
	disconnect(callback, code, reason) {
		this.connectClock++;
		this.disconnecting = true;
		this.closeWasClean = true;
		clearTimeout(this.fallbackTimer);
		this.reconnectTimer.reset();
		this.teardown(() => {
			this.disconnecting = false;
			callback && callback();
		}, code, reason);
	}
	/**
	* @param {Params} [params] - [DEPRECATED] The params to send when connecting, for example `{user_id: userToken}`
	*
	* Passing params to connect is deprecated; pass them in the Socket constructor instead:
	* `new Socket("/socket", {params: {user_id: userToken}})`.
	*/
	connect(params) {
		if (params) {
			console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor");
			this.params = closure(params);
		}
		if (this.conn && !this.disconnecting) return;
		if (this.longPollFallbackMs && this.transport !== LongPoll) this.connectWithFallback(LongPoll, this.longPollFallbackMs);
		else this.transportConnect();
	}
	/**
	* Logs the message. Override `this.logger` for specialized logging. noops by default
	* @param {string} kind
	* @param {string} msg
	* @param {Object} data
	*/
	log(kind, msg, data) {
		this.logger && this.logger(kind, msg, data);
	}
	/**
	* Returns true if a logger has been set on this socket.
	*/
	hasLogger() {
		return this.logger !== null;
	}
	/**
	* Registers callbacks for connection open events
	*
	* @example socket.onOpen(function(){ console.info("the socket was opened") })
	*
	* @param {SocketOnOpen} callback
	*/
	onOpen(callback) {
		let ref = this.makeRef();
		this.stateChangeCallbacks.open.push([ref, callback]);
		return ref;
	}
	/**
	* Registers callbacks for connection close events
	* @param {SocketOnClose} callback
	* @returns {string}
	*/
	onClose(callback) {
		let ref = this.makeRef();
		this.stateChangeCallbacks.close.push([ref, callback]);
		return ref;
	}
	/**
	* Registers callbacks for connection error events
	*
	* @example socket.onError(function(error){ alert("An error occurred") })
	*
	* @param {SocketOnError} callback
	* @returns {string}
	*/
	onError(callback) {
		let ref = this.makeRef();
		this.stateChangeCallbacks.error.push([ref, callback]);
		return ref;
	}
	/**
	* Registers callbacks for connection message events
	* @param {SocketOnMessage} callback
	* @returns {string}
	*/
	onMessage(callback) {
		let ref = this.makeRef();
		this.stateChangeCallbacks.message.push([ref, callback]);
		return ref;
	}
	/**
	* Sets a callback that receives lifecycle events for internal heartbeat messages.
	* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
	* @param {HeartbeatCallback} callback
	*/
	onHeartbeat(callback) {
		this.heartbeatCallback = callback;
	}
	/**
	* Pings the server and invokes the callback with the RTT in milliseconds
	* @param {(timeDelta: number) => void} callback
	*
	* Returns true if the ping was pushed or false if unable to be pushed.
	*/
	ping(callback) {
		if (!this.isConnected()) return false;
		let ref = this.makeRef();
		let startTime = Date.now();
		this.push({
			topic: "phoenix",
			event: "heartbeat",
			payload: {},
			ref
		});
		let onMsgRef = this.onMessage((msg) => {
			if (msg.ref === ref) {
				this.off([onMsgRef]);
				callback(Date.now() - startTime);
			}
		});
		return true;
	}
	/**
	* @private
	*
	* @param {Function}
	*/
	transportName(transport) {
		switch (transport) {
			case LongPoll: return "LongPoll";
			default: return transport.name;
		}
	}
	/**
	* @private
	*/
	transportConnect() {
		this.connectClock++;
		this.closeWasClean = false;
		let protocols = void 0;
		if (this.authToken) protocols = ["phoenix", `${AUTH_TOKEN_PREFIX}${btoa(this.authToken()).replace(/=/g, "")}`];
		this.conn = new this.transport(this.endPointURL(), protocols);
		this.conn.binaryType = this.binaryType;
		this.conn.timeout = this.longpollerTimeout;
		this.conn.onopen = () => this.onConnOpen();
		this.conn.onerror = (error) => this.onConnError(error);
		this.conn.onmessage = (event) => this.onConnMessage(event);
		this.conn.onclose = (event) => this.onConnClose(event);
	}
	getSession(key) {
		return this.sessionStore && this.sessionStore.getItem(key);
	}
	storeSession(key, val) {
		this.sessionStore && this.sessionStore.setItem(key, val);
	}
	connectWithFallback(fallbackTransport, fallbackThreshold = 2500) {
		clearTimeout(this.fallbackTimer);
		let established = false;
		let primaryTransport = true;
		let openRef, errorRef;
		let fallbackTransportName = this.transportName(fallbackTransport);
		let fallback = (reason) => {
			this.log("transport", `falling back to ${fallbackTransportName}...`, reason);
			this.off([openRef, errorRef]);
			primaryTransport = false;
			this.replaceTransport(fallbackTransport);
			this.transportConnect();
		};
		if (this.getSession(`phx:fallback:${fallbackTransportName}`)) return fallback("memorized");
		this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
		errorRef = this.onError((reason) => {
			this.log("transport", "error", reason);
			if (primaryTransport && !established) {
				clearTimeout(this.fallbackTimer);
				fallback(reason);
			}
		});
		if (this.fallbackRef) this.off([this.fallbackRef]);
		this.fallbackRef = this.onOpen(() => {
			established = true;
			if (!primaryTransport) {
				let fallbackTransportName2 = this.transportName(fallbackTransport);
				if (!this.primaryPassedHealthCheck) this.storeSession(`phx:fallback:${fallbackTransportName2}`, "true");
				return this.log("transport", `established ${fallbackTransportName2} fallback`);
			}
			clearTimeout(this.fallbackTimer);
			this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
			this.ping((rtt) => {
				this.log("transport", "connected to primary after", rtt);
				this.primaryPassedHealthCheck = true;
				clearTimeout(this.fallbackTimer);
			});
		});
		this.transportConnect();
	}
	clearHeartbeats() {
		clearTimeout(this.heartbeatTimer);
		clearTimeout(this.heartbeatTimeoutTimer);
	}
	onConnOpen() {
		if (this.hasLogger()) this.log("transport", `connected to ${this.endPointURL()}`);
		this.closeWasClean = false;
		this.disconnecting = false;
		this.establishedConnections++;
		this.flushSendBuffer();
		this.reconnectTimer.reset();
		if (this.autoSendHeartbeat) this.resetHeartbeat();
		this.triggerStateCallbacks("open");
	}
	/**
	* @private
	*/
	heartbeatTimeout() {
		if (this.pendingHeartbeatRef) {
			this.pendingHeartbeatRef = null;
			this.heartbeatSentAt = null;
			if (this.hasLogger()) this.log("transport", "heartbeat timeout. Attempting to re-establish connection");
			try {
				this.heartbeatCallback("timeout");
			} catch (e) {
				this.log("error", "error in heartbeat callback", e);
			}
			this.triggerChanError(/* @__PURE__ */ new Error("heartbeat timeout"));
			this.closeWasClean = false;
			this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, "heartbeat timeout");
		}
	}
	resetHeartbeat() {
		if (this.conn && this.conn.skipHeartbeat) return;
		this.pendingHeartbeatRef = null;
		this.clearHeartbeats();
		this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs);
	}
	teardown(callback, code, reason) {
		if (!this.conn) return callback && callback();
		const connToClose = this.conn;
		this.waitForBufferDone(connToClose, () => {
			if (code) connToClose.close(code, reason || "");
			else connToClose.close();
			this.waitForSocketClosed(connToClose, () => {
				if (this.conn === connToClose) {
					this.conn.onopen = function() {};
					this.conn.onerror = function() {};
					this.conn.onmessage = function() {};
					this.conn.onclose = function() {};
					this.conn = null;
				}
				callback && callback();
			});
		});
	}
	waitForBufferDone(conn, callback, tries = 1) {
		if (tries === 5 || !conn.bufferedAmount) {
			callback();
			return;
		}
		setTimeout(() => {
			this.waitForBufferDone(conn, callback, tries + 1);
		}, 150 * tries);
	}
	waitForSocketClosed(conn, callback, tries = 1) {
		if (tries === 5 || conn.readyState === SOCKET_STATES.closed) {
			callback();
			return;
		}
		setTimeout(() => {
			this.waitForSocketClosed(conn, callback, tries + 1);
		}, 150 * tries);
	}
	/**
	* @param {CloseEvent} event
	*/
	onConnClose(event) {
		if (this.conn) this.conn.onclose = () => {};
		if (this.hasLogger()) this.log("transport", "close", event);
		this.triggerChanError(event);
		this.clearHeartbeats();
		if (!this.closeWasClean) this.reconnectTimer.scheduleTimeout();
		this.triggerStateCallbacks("close", event);
	}
	/**
	* @private
	* @param {Event} error
	*/
	onConnError(error) {
		if (this.hasLogger()) this.log("transport", "error", error);
		let transportBefore = this.transport;
		let establishedBefore = this.establishedConnections;
		this.triggerStateCallbacks("error", error, transportBefore, establishedBefore);
		if (transportBefore === this.transport || establishedBefore > 0) this.triggerChanError(error);
	}
	/**
	* @private
	* @param {unknown} [reason] underlying close/error event forwarded to channel error listeners
	*/
	triggerChanError(reason) {
		this.channels.forEach((channel) => {
			if (!(channel.isErrored() || channel.isLeaving() || channel.isClosed())) channel.trigger(CHANNEL_EVENTS.error, reason);
		});
	}
	/**
	* @returns {string}
	*/
	connectionState() {
		switch (this.conn && this.conn.readyState) {
			case SOCKET_STATES.connecting: return "connecting";
			case SOCKET_STATES.open: return "open";
			case SOCKET_STATES.closing: return "closing";
			default: return "closed";
		}
	}
	/**
	* @returns {boolean}
	*/
	isConnected() {
		return this.connectionState() === "open";
	}
	/**
	*
	* @param {Channel} channel
	*/
	remove(channel) {
		this.off(channel.stateChangeRefs);
		this.channels = this.channels.filter((c) => c !== channel);
	}
	/**
	* Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
	*
	* @param {string[]} refs - list of refs returned by calls to
	*                 `onOpen`, `onClose`, `onError,` and `onMessage`
	*/
	off(refs) {
		for (let key in this.stateChangeCallbacks) this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {
			return refs.indexOf(ref) === -1;
		});
	}
	/**
	* Initiates a new channel for the given topic
	*
	* @param {string} topic
	* @param {Params | (() => Params)} [chanParams]- Parameters for the channel
	* @returns {Channel}
	*/
	channel(topic, chanParams = {}) {
		let chan = new Channel(topic, chanParams, this);
		this.channels.push(chan);
		return chan;
	}
	/**
	* @param {Message<Record<string, any>>} data
	*/
	push(data) {
		if (this.hasLogger()) {
			let { topic, event, payload, ref, join_ref } = data;
			this.log("push", `${topic} ${event} (${join_ref}, ${ref})`, payload);
		}
		if (this.isConnected()) this.encode(data, (result) => this.conn.send(result));
		else this.sendBuffer.push(() => this.encode(data, (result) => this.conn.send(result)));
	}
	/**
	* Return the next message ref, accounting for overflows
	* @returns {string}
	*/
	makeRef() {
		let newRef = this.ref + 1;
		if (newRef === this.ref) this.ref = 0;
		else this.ref = newRef;
		return this.ref.toString();
	}
	sendHeartbeat() {
		if (!this.isConnected()) {
			try {
				this.heartbeatCallback("disconnected");
			} catch (e) {
				this.log("error", "error in heartbeat callback", e);
			}
			return;
		}
		if (this.pendingHeartbeatRef) {
			this.heartbeatTimeout();
			return;
		}
		this.pendingHeartbeatRef = this.makeRef();
		this.heartbeatSentAt = Date.now();
		this.push({
			topic: "phoenix",
			event: "heartbeat",
			payload: {},
			ref: this.pendingHeartbeatRef
		});
		try {
			this.heartbeatCallback("sent");
		} catch (e) {
			this.log("error", "error in heartbeat callback", e);
		}
		this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs);
	}
	flushSendBuffer() {
		if (this.isConnected() && this.sendBuffer.length > 0) {
			this.sendBuffer.forEach((callback) => callback());
			this.sendBuffer = [];
		}
	}
	/**
	* @param {MessageEvent<any>} rawMessage
	*/
	onConnMessage(rawMessage) {
		this.decode(rawMessage.data, (msg) => {
			let { topic, event, payload, ref, join_ref } = msg;
			if (ref && ref === this.pendingHeartbeatRef) {
				const latency = this.heartbeatSentAt ? Date.now() - this.heartbeatSentAt : void 0;
				this.clearHeartbeats();
				try {
					this.heartbeatCallback(payload.status === "ok" ? "ok" : "error", latency);
				} catch (e) {
					this.log("error", "error in heartbeat callback", e);
				}
				this.pendingHeartbeatRef = null;
				this.heartbeatSentAt = null;
				if (this.autoSendHeartbeat) this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs);
			}
			if (this.hasLogger()) this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`.trim(), payload);
			for (let i = 0; i < this.channels.length; i++) {
				const channel = this.channels[i];
				if (!channel.isMember(topic, event, payload, join_ref)) continue;
				channel.trigger(event, payload, ref, join_ref);
			}
			this.triggerStateCallbacks("message", msg);
		});
	}
	/**
	* @private
	* @template {keyof SocketStateChangeCallbacks} K
	* @param {K} event
	* @param {...Parameters<SocketStateChangeCallbacks[K][number][1]>} args
	* @returns {void}
	*/
	triggerStateCallbacks(event, ...args) {
		try {
			this.stateChangeCallbacks[event].forEach(([_, callback]) => {
				try {
					callback(...args);
				} catch (e) {
					this.log("error", `error in ${event} callback`, e);
				}
			});
		} catch (e) {
			this.log("error", `error triggering ${event} callbacks`, e);
		}
	}
	leaveOpenTopic(topic) {
		let dupChannel = this.channels.find((c) => c.topic === topic && (c.isJoined() || c.isJoining()));
		if (dupChannel) {
			if (this.hasLogger()) this.log("transport", `leaving duplicate topic "${topic}"`);
			dupChannel.leave();
		}
	}
};
//#endregion
export { Socket as n, Presence as t };
