import { InvalidConnectionRequest, Unauthorized } from "../../../../zero-protocol/src/error-kind-enum.js";
import { ZeroCache } from "../../../../zero-protocol/src/error-origin-enum.js";
import { ProtocolErrorWithLevel } from "../../types/error-with-level.js";
import { compileUrlPattern } from "../../custom/fetch.js";
import { authEquals, resolveAuth } from "../../auth/auth.js";
//#region ../zero-cache/src/services/view-syncer/connection-context-manager.ts
/**
* State machine for the auth state of a single `ViewSyncerService`.
*
* Connections are registered as `provisional`, optionally backfilled with
* `initConnection` metadata, and then promoted to `validated` once their
* stored `userID` is confirmed as valid. The manager also tracks which
* validated connection currently serves as the group's background connection.
*
* This is intentionally side-effect free.
*/
var ConnectionContextManagerImpl = class {
	#lc;
	#connections = /* @__PURE__ */ new Map();
	#group = {
		userID: void 0,
		backgroundConnection: void 0,
		retransformAt: void 0,
		maintenanceNotBeforeAt: void 0,
		validated: false
	};
	#validateLegacyJWT;
	#now;
	#revalidateIntervalMs;
	#retransformIntervalMs;
	#queryConfig;
	#pushConfig;
	#nextInsertionOrder = 0;
	constructor(lc, revalidateIntervalSeconds, retransformIntervalSeconds, queryConfig, pushConfig, validateLegacyJWT, now) {
		this.#lc = lc;
		this.#now = now ?? Date.now;
		this.#revalidateIntervalMs = revalidateIntervalSeconds === void 0 ? void 0 : revalidateIntervalSeconds * 1e3;
		this.#retransformIntervalMs = retransformIntervalSeconds === void 0 ? void 0 : retransformIntervalSeconds * 1e3;
		this.#queryConfig = queryConfig;
		this.#pushConfig = pushConfig;
		this.#validateLegacyJWT = validateLegacyJWT;
	}
	/**
	* Creates or replaces the live record for a websocket connection.
	*
	* Re-registering the same `clientID` drops the old socket record and starts
	* the replacement back in `provisional` state.
	*/
	registerConnection(selector, connectParams, auth) {
		this.#removeConnection(selector);
		const sharedHeaders = {
			customHeaders: void 0,
			token: auth?.raw,
			origin: connectParams.origin,
			userID: connectParams.userID
		};
		const connection = {
			state: "provisional",
			clientID: connectParams.clientID,
			wsID: connectParams.wsID,
			revision: 0,
			userID: connectParams.userID,
			auth,
			profileID: connectParams.profileID,
			baseCookie: connectParams.baseCookie,
			protocolVersion: connectParams.protocolVersion,
			revalidateAt: void 0,
			queryContext: {
				url: this.#queryConfig?.url?.[0],
				allowedUrlPatterns: this.#queryConfig?.url?.map(compileUrlPattern),
				headerOptions: {
					...sharedHeaders,
					apiKey: this.#queryConfig?.apiKey,
					allowedClientHeaders: this.#queryConfig?.allowedClientHeaders,
					cookie: this.#queryConfig?.forwardCookies ? connectParams.httpCookie : void 0
				}
			},
			pushContext: {
				url: this.#pushConfig?.url?.[0],
				allowedUrlPatterns: this.#pushConfig?.url?.map(compileUrlPattern),
				headerOptions: {
					...sharedHeaders,
					apiKey: this.#pushConfig?.apiKey,
					allowedClientHeaders: this.#pushConfig?.allowedClientHeaders,
					cookie: this.#pushConfig?.forwardCookies ? connectParams.httpCookie : void 0
				}
			},
			insertionOrder: ++this.#nextInsertionOrder
		};
		this.#connections.set(connection.clientID, connection);
		this.#refreshBackgroundConnectionContext();
		this.#updateBackgroundRetransformDeadline(false);
		return snapshotConnection(connection);
	}
	/**
	* Backfills `initConnection` data for sockets that were registered before the
	* client could send its full init payload.
	*
	* This updates metadata only; it does not validate the connection.
	*/
	initConnection(selector, body) {
		const connection = this.#mustGetConnectionContext(selector);
		if (body.userQueryURL) connection.queryContext.url = body.userQueryURL;
		if (body.userQueryHeaders) connection.queryContext.headerOptions.customHeaders = body.userQueryHeaders;
		if (body.userPushURL) connection.pushContext.url = body.userPushURL;
		if (body.userPushHeaders) connection.pushContext.headerOptions.customHeaders = body.userPushHeaders;
		connection.revision++;
		this.#demoteConnection(connection);
		return snapshotConnection(connection);
	}
	/**
	* A material auth change demotes the connection back to provisional until it
	* is validated again.
	*/
	async updateAuth(selector, body) {
		const connection = this.#mustGetConnectionContext(selector);
		const nextAuth = await resolveAuth(this.#lc, connection.auth, connection.userID, body.auth, this.#validateLegacyJWT);
		const authChanged = !authEquals(connection.auth, nextAuth);
		connection.auth = nextAuth;
		if (authChanged) {
			connection.revision++;
			this.#demoteConnection(connection);
		}
		return snapshotConnection(connection);
	}
	/**
	* Validates one connection against the group's pinned `userID`.
	*
	* The first successful validation binds the group `userID`. Later
	* validations must match it. Validation also refreshes the connection's
	* revalidation deadline and may pick the connection as the group
	* background connection if none is currently available. If the websocket is
	* gone by the time async validation finishes, this becomes a no-op.
	*/
	validateConnection(selector, revision) {
		const connection = this.#getConnectionContext(selector);
		if (!connection) return;
		if (connection.revision !== revision) {
			this.#lc.debug?.("Skipping validateConnection for stale revision", {
				clientID: selector.clientID,
				attemptedRevision: revision,
				currentRevision: connection.revision
			});
			return;
		}
		if (this.#group.validated && this.#group.userID !== connection.userID) throw new ProtocolErrorWithLevel({
			kind: Unauthorized,
			message: "Client groups are pinned to a single userID. Connection userID does not match existing client group userID.",
			origin: ZeroCache
		}, "warn");
		if (!this.#group.validated) {
			this.#group.validated = true;
			this.#group.userID = connection.userID;
		}
		connection.state = "validated";
		connection.revalidateAt = this.#nextRevalidateAt();
		this.#refreshBackgroundConnectionContext(connection);
		this.#updateBackgroundRetransformDeadline(false);
		return {
			connection: snapshotConnection(connection),
			group: this.getGroupState()
		};
	}
	/** Removes one connection due to failed auth and updates all derived background/deadline state. */
	failConnection(selector, revision) {
		return this.#removeConnection(selector, revision);
	}
	/** Removes one disconnected connection and updates all derived background/deadline state. */
	closeConnection(selector) {
		return this.#removeConnection(selector);
	}
	/**
	* Records a successful background retransform. This starts a fresh interval
	* from the manager clock when shared retransform is schedulable, or
	* clears the deadline if it is not.
	*/
	markBackgroundRetransformSuccess(selector, revision) {
		const backgroundConnection = this.#getBackgroundConnectionContext();
		if (!backgroundConnection) return;
		if (selector !== void 0 && (backgroundConnection.clientID !== selector.clientID || backgroundConnection.wsID !== selector.wsID || backgroundConnection.revision !== revision)) return;
		this.#updateBackgroundRetransformDeadline(true);
	}
	deferMaintenance(kind) {
		const intervalMs = kind === "revalidate" ? this.#revalidateIntervalMs : this.#retransformIntervalMs;
		if (intervalMs === void 0) return;
		this.#group.maintenanceNotBeforeAt = Math.max(this.#group.maintenanceNotBeforeAt ?? 0, this.#now() + intervalMs);
	}
	/** Returns the current live record for a client slot, if any. */
	getConnectionContext(selector) {
		return snapshotConnection(this.#getConnectionContext(selector));
	}
	/** Returns the live record for one websocket or throws if it is unavailable. */
	mustGetConnectionContext(selector) {
		return snapshotConnection(this.#mustGetConnectionContext(selector));
	}
	/** Returns the current background connection, if one exists. */
	getBackgroundConnectionContext() {
		return snapshotConnection(this.#getBackgroundConnectionContext());
	}
	mustGetBackgroundConnectionContext() {
		const backgroundConnection = this.#getBackgroundConnectionContext();
		if (!backgroundConnection) throw new ProtocolErrorWithLevel({
			kind: InvalidConnectionRequest,
			message: "No validated connection is available for shared query work.",
			origin: ZeroCache
		}, "warn");
		return backgroundConnection;
	}
	/** Returns the shared group auth state. */
	getGroupState() {
		return snapshotGroup(this.#group);
	}
	/**
	* Reports which maintenance work is currently due.
	*
	* The result is a pure snapshot: callers decide which actions to run and
	* when to wake up next. `earliestDeadlineAt` is the earliest outstanding
	* maintenance deadline, including overdue work, unless a transient failure
	* has deferred all scheduled maintenance until `maintenanceNotBeforeAt`.
	*/
	planMaintenance() {
		const dueRevalidations = [];
		const now = this.#now();
		let earliestDeadlineAt = this.#group.retransformAt;
		for (const connection of this.#connections.values()) {
			if (connection.state !== "validated" || connection.revalidateAt === void 0) continue;
			if (connection.revalidateAt <= now) dueRevalidations.push(snapshotConnection(connection));
			earliestDeadlineAt = minDefined(earliestDeadlineAt, connection.revalidateAt);
		}
		const dueRetransform = this.#group.retransformAt !== void 0 && this.#group.retransformAt <= now;
		const maintenanceNotBeforeAt = this.#group.maintenanceNotBeforeAt;
		if (maintenanceNotBeforeAt !== void 0 && maintenanceNotBeforeAt > now && earliestDeadlineAt !== void 0) return {
			dueRevalidations: [],
			dueRetransform: false,
			earliestDeadlineAt: Math.max(earliestDeadlineAt, maintenanceNotBeforeAt)
		};
		return {
			dueRevalidations: dueRevalidations.sort(compareByInsertionOrder),
			dueRetransform,
			earliestDeadlineAt
		};
	}
	#removeConnection(selector, revision) {
		const connection = this.#getConnectionContext(selector);
		if (!connection) return;
		if (revision !== void 0 && connection.revision !== revision) {
			this.#lc.debug?.("Ignoring failConnection for stale revision", {
				clientID: selector.clientID,
				wsID: selector.wsID,
				attemptedRevision: revision,
				currentRevision: connection.revision
			});
			return;
		}
		const snapshot = snapshotConnection(connection);
		this.#connections.delete(connection.clientID);
		this.#refreshBackgroundConnectionContext();
		this.#updateBackgroundRetransformDeadline(false);
		return snapshot;
	}
	#demoteConnection(connection) {
		connection.state = "provisional";
		connection.revalidateAt = void 0;
		this.#refreshBackgroundConnectionContext();
		this.#updateBackgroundRetransformDeadline(false);
	}
	/**
	* Keeps the background connection sticky while it remains validated.
	*
	* When a newly validated `preferred` connection is provided, it is promoted
	* only if there is no current validated background connection. Otherwise the
	* existing background connection stays in place until it disappears or is
	* demoted, at which point the newest validated connection is selected.
	*/
	#refreshBackgroundConnectionContext(preferred) {
		if (preferred?.state === "validated") {
			const currentBackgroundConnection = this.#getBackgroundConnectionContext();
			if (currentBackgroundConnection?.clientID === preferred.clientID && currentBackgroundConnection.wsID === preferred.wsID) return;
			if (currentBackgroundConnection !== void 0) return;
			this.#group.backgroundConnection = {
				clientID: preferred.clientID,
				wsID: preferred.wsID
			};
			this.#lc.debug?.("Selected background connection for shared auth work", {
				clientID: preferred.clientID,
				wsID: preferred.wsID,
				revision: preferred.revision,
				reason: "preferred-validated"
			});
			return;
		}
		if (this.#getBackgroundConnectionContext()?.state === "validated") return;
		const nextBackgroundConnection = [...this.#connections.values()].filter((connection) => connection.state === "validated").sort(comparePreferredValidatedConnection).at(0);
		this.#group.backgroundConnection = nextBackgroundConnection ? {
			clientID: nextBackgroundConnection.clientID,
			wsID: nextBackgroundConnection.wsID
		} : void 0;
		if (nextBackgroundConnection) this.#lc.debug?.("Selected background connection for shared auth work", {
			clientID: nextBackgroundConnection.clientID,
			wsID: nextBackgroundConnection.wsID,
			revision: nextBackgroundConnection.revision,
			reason: "fallback-validated"
		});
	}
	#getBackgroundConnectionContext() {
		const backgroundConnection = this.#group.backgroundConnection;
		if (!backgroundConnection) return;
		return this.#getConnectionContext(backgroundConnection);
	}
	#getConnectionContext(selector) {
		const connection = this.#connections.get(selector.clientID);
		if (!connection) return;
		if (connection.wsID !== selector.wsID) return;
		return connection;
	}
	#mustGetConnectionContext(selector) {
		const connection = this.#getConnectionContext(selector);
		if (!connection) throw new ProtocolErrorWithLevel({
			kind: InvalidConnectionRequest,
			message: "Connection auth state was not available for this websocket.",
			origin: ZeroCache
		}, "warn");
		return connection;
	}
	/**
	* Keeps the group background retransform deadline coherent with current
	* schedulability.
	*
	* When `reset` is false, this seeds a deadline only when shared retransform
	* is now possible and no deadline exists yet, preserving any existing
	* cadence. When `reset` is true, it starts a fresh interval from `#now()` if
	* retransform is schedulable, or clears the deadline if it is not.
	*/
	#updateBackgroundRetransformDeadline(reset) {
		if (!this.#getBackgroundConnectionContext() || this.#retransformIntervalMs === void 0) {
			this.#group.retransformAt = void 0;
			return;
		}
		if (reset || this.#group.retransformAt === void 0) this.#group.retransformAt = this.#now() + this.#retransformIntervalMs;
	}
	#nextRevalidateAt() {
		return this.#revalidateIntervalMs === void 0 ? void 0 : this.#now() + this.#revalidateIntervalMs;
	}
};
function snapshotConnection(connection) {
	if (!connection) return;
	return {
		...connection,
		queryContext: {
			...connection.queryContext,
			headerOptions: {
				...connection.queryContext.headerOptions,
				customHeaders: connection.queryContext.headerOptions.customHeaders ? { ...connection.queryContext.headerOptions.customHeaders } : void 0
			}
		},
		pushContext: {
			...connection.pushContext,
			headerOptions: {
				...connection.pushContext.headerOptions,
				customHeaders: connection.pushContext.headerOptions.customHeaders ? { ...connection.pushContext.headerOptions.customHeaders } : void 0
			}
		}
	};
}
function snapshotGroup(group) {
	return {
		...group,
		backgroundConnection: group.backgroundConnection ? { ...group.backgroundConnection } : void 0
	};
}
function compareByInsertionOrder(a, b) {
	return a.insertionOrder - b.insertionOrder || a.wsID.localeCompare(b.wsID);
}
function comparePreferredValidatedConnection(a, b) {
	return b.insertionOrder - a.insertionOrder || b.wsID.localeCompare(a.wsID);
}
function minDefined(a, b) {
	if (a === void 0) return b;
	if (b === void 0) return a;
	return Math.min(a, b);
}
//#endregion
export { ConnectionContextManagerImpl };

//# sourceMappingURL=connection-context-manager.js.map