import { assert } from "../../../shared/src/asserts.js";
import { parse } from "../../../shared/src/valita.js";
import { Internal, InvalidMessage, VersionNotSupported } from "../../../zero-protocol/src/error-kind-enum.js";
import { ZeroCache } from "../../../zero-protocol/src/error-origin-enum.js";
import { isProtocolError } from "../../../zero-protocol/src/error.js";
import "../../../zero-protocol/src/protocol-version.js";
import { ProtocolErrorWithLevel, getLogLevel, wrapWithProtocolError } from "../types/error-with-level.js";
import { upstreamSchema } from "../../../zero-protocol/src/up.js";
import WebSocket$1, { createWebSocketStream } from "ws";
import { Readable, Writable, pipeline } from "node:stream";
//#region ../zero-cache/src/workers/connection.ts
var DOWNSTREAM_MSG_INTERVAL_MS = 6e3;
/**
* Represents a connection between the client and server.
*
* Handles incoming messages on the connection and dispatches
* them to the correct service.
*
* Listens to the ViewSyncer and sends messages to the client.
*/
var Connection = class {
	#ws;
	#wsID;
	#protocolVersion;
	#lc;
	#onClose;
	#messageHandler;
	#downstreamMsgTimer;
	#viewSyncerOutboundStream;
	#pusherOutboundStream;
	#closed = false;
	constructor(lc, connectParams, ws, messageHandler, onClose) {
		const { clientGroupID, clientID, wsID, protocolVersion } = connectParams;
		this.#messageHandler = messageHandler;
		this.#ws = ws;
		this.#wsID = wsID;
		this.#protocolVersion = protocolVersion;
		this.#lc = lc.withContext("connection").withContext("clientID", clientID).withContext("clientGroupID", clientGroupID).withContext("wsID", wsID);
		this.#lc.debug?.("new connection");
		this.#onClose = onClose;
		this.#ws.addEventListener("close", this.#handleClose);
		this.#ws.addEventListener("error", this.#handleError);
		this.#proxyInbound();
		this.#downstreamMsgTimer = setInterval(this.#maybeSendPong, DOWNSTREAM_MSG_INTERVAL_MS / 2);
	}
	/**
	* Checks the protocol version and errors for unsupported protocols,
	* sending the initial `connected` response on success.
	*
	* This is early in the connection lifecycle because {@link #handleMessage}
	* will only parse messages with schema(s) of supported protocol versions.
	*/
	init() {
		if (this.#protocolVersion > 50 || this.#protocolVersion < 30) this.#closeWithError({
			kind: VersionNotSupported,
			message: `server is at sync protocol v50 and does not support v${this.#protocolVersion}. The ${this.#protocolVersion > 50 ? "server" : "client"} must be updated to a newer release.`,
			origin: ZeroCache
		});
		else {
			const connectedMessage = ["connected", {
				wsid: this.#wsID,
				timestamp: Date.now()
			}];
			this.send(connectedMessage, "ignore-backpressure");
			return true;
		}
		return false;
	}
	close(reason, ...args) {
		if (this.#closed) return;
		this.#closed = true;
		this.#lc.info?.(`closing connection: ${reason}`, ...args);
		this.#ws.removeEventListener("close", this.#handleClose);
		this.#ws.removeEventListener("error", this.#handleError);
		this.#viewSyncerOutboundStream?.cancel();
		this.#viewSyncerOutboundStream = void 0;
		this.#pusherOutboundStream?.cancel();
		this.#pusherOutboundStream = void 0;
		this.#onClose();
		if (this.#ws.readyState !== this.#ws.CLOSED) this.#ws.close();
		clearTimeout(this.#downstreamMsgTimer);
	}
	handleInitConnection(initConnectionMsg) {
		return this.#handleMessage({ data: initConnectionMsg });
	}
	#handleMessage = async (event) => {
		const data = event.data.toString();
		if (this.#closed) {
			this.#lc.debug?.("Ignoring message received after closed", data);
			return;
		}
		let msg;
		try {
			msg = parse(JSON.parse(data), upstreamSchema);
		} catch (e) {
			this.#lc.warn?.(`failed to parse message "${data}": ${String(e)}`);
			this.#closeWithError({
				kind: InvalidMessage,
				message: String(e),
				origin: ZeroCache
			}, e);
			return;
		}
		try {
			if (msg[0] === "ping") {
				this.send(["pong", {}], "ignore-backpressure");
				return;
			}
			const result = await this.#messageHandler.handleMessage(msg);
			for (const r of result) this.#handleMessageResult(r);
		} catch (e) {
			this.#closeWithThrown(e);
		}
	};
	#handleMessageResult(result) {
		switch (result.type) {
			case "fatal":
				this.#closeWithError(result.error);
				break;
			case "ok": break;
			case "stream":
				switch (result.source) {
					case "viewSyncer":
						assert(this.#viewSyncerOutboundStream === void 0, "Outbound stream already set for this connection!");
						this.#viewSyncerOutboundStream = result.stream;
						break;
					case "pusher":
						assert(this.#pusherOutboundStream === void 0, "Outbound stream already set for this connection!");
						this.#pusherOutboundStream = result.stream;
						break;
				}
				this.#proxyOutbound(result.stream);
				break;
			case "transient": for (const error of result.errors) this.sendError(error);
		}
	}
	#handleClose = (e) => {
		const { code, reason, wasClean } = e;
		this.close("WebSocket close event", {
			code,
			reason,
			wasClean
		});
	};
	#handleError = (e) => {
		this.#lc.error?.("WebSocket error event", e.message, e.error);
	};
	#proxyInbound() {
		pipeline(createWebSocketStream(this.#ws), new Writable({ write: (data, _encoding, callback) => {
			this.#handleMessage({ data }).then(() => callback(), callback);
		} }), () => {});
	}
	#proxyOutbound(outboundStream) {
		pipeline(Readable.from(outboundStream), new Writable({
			objectMode: true,
			write: (downstream, _encoding, callback) => this.send(downstream, callback)
		}), (e) => e ? this.#closeWithThrown(e) : this.close(`downstream closed by ViewSyncer`));
	}
	#closeWithThrown(e) {
		const errorBody = findProtocolError(e)?.errorBody ?? wrapWithProtocolError(e).errorBody;
		this.#closeWithError(errorBody, e);
	}
	#closeWithError(errorBody, thrown) {
		this.sendError(errorBody, thrown);
		this.close(`${errorBody.kind} (${errorBody.origin}): ${errorBody.message}`, errorBody);
	}
	#lastDownstreamMsgTime = Date.now();
	#maybeSendPong = () => {
		if (Date.now() - this.#lastDownstreamMsgTime > DOWNSTREAM_MSG_INTERVAL_MS) {
			this.#lc.debug?.("manually sending pong");
			this.send(["pong", {}], "ignore-backpressure");
		}
	};
	send(data, callback) {
		this.#lastDownstreamMsgTime = Date.now();
		return send(this.#lc, this.#ws, data, callback);
	}
	sendError(errorBody, thrown) {
		sendError(this.#lc, this.#ws, errorBody, thrown);
	}
};
function send(lc, ws, data, callback) {
	if (ws.readyState === WebSocket$1.OPEN) ws.send(JSON.stringify(data), callback === "ignore-backpressure" ? void 0 : callback);
	else {
		lc.debug?.(`Dropping outbound message on ws (state: ${ws.readyState})`, { dropped: data });
		if (callback !== "ignore-backpressure") callback(new ProtocolErrorWithLevel({
			kind: Internal,
			message: "WebSocket closed",
			origin: ZeroCache
		}, "info"));
	}
}
function sendError(lc, ws, errorBody, thrown) {
	lc = lc.withContext("errorKind", errorBody.kind);
	let logLevel;
	if (thrown instanceof ProtocolErrorWithLevel) logLevel = thrown.logLevel;
	else if (hasErrno(thrown) || hasTransientSocketCode(thrown) || isTransientSocketMessage(errorBody.message)) logLevel = "warn";
	else if (errorBody.kind === "ClientNotFound" || errorBody.kind === "TransformFailed") logLevel = "warn";
	else logLevel = thrown ? getLogLevel(thrown) : "info";
	lc[logLevel]?.("Sending error on WebSocket", errorBody, thrown ?? "");
	send(lc, ws, ["error", errorBody], "ignore-backpressure");
}
function findProtocolError(error) {
	if (isProtocolError(error)) return error;
	if (error instanceof Error && error.cause) return findProtocolError(error.cause);
}
function hasErrno(error) {
	return Boolean(error && typeof error === "object" && "errno" in error && typeof error.errno !== "undefined");
}
var TRANSIENT_SOCKET_ERROR_CODES = new Set([
	"EPIPE",
	"ECONNRESET",
	"ECANCELED"
]);
var TRANSIENT_SOCKET_MESSAGE_PATTERNS = ["socket was closed while data was being compressed"];
function hasTransientSocketCode(error) {
	if (!error || typeof error !== "object") return false;
	const maybeCode = "code" in error ? String(error.code) : void 0;
	return Boolean(maybeCode && TRANSIENT_SOCKET_ERROR_CODES.has(maybeCode.toUpperCase()));
}
function isTransientSocketMessage(message) {
	if (!message) return false;
	const lower = message.toLowerCase();
	return TRANSIENT_SOCKET_MESSAGE_PATTERNS.some((pattern) => lower.includes(pattern));
}
//#endregion
export { Connection, sendError };

//# sourceMappingURL=connection.js.map