import { getErrorMessage } from "../../../shared/src/error.js";
import { TransformFailed } from "../../../zero-protocol/src/error-kind-enum.js";
import { ZeroCache } from "../../../zero-protocol/src/error-origin-enum.js";
import { Internal } from "../../../zero-protocol/src/error-reason-enum.js";
import { isProtocolError } from "../../../zero-protocol/src/error.js";
import { transformResponseMessageSchema } from "../../../zero-protocol/src/custom-queries.js";
import { hashOfAST } from "../../../zero-protocol/src/query-hash.js";
import { startAsyncSpan } from "../../../otel/src/span.js";
import { TimedCache } from "../../../shared/src/cache.js";
import { sortedEntries } from "../../../shared/src/sorted-entries.js";
import { fetchFromAPIServer } from "../custom/fetch.js";
import { trace } from "@opentelemetry/api";
//#region ../zero-cache/src/custom-queries/transform-query.ts
var tracer = trace.getTracer("custom-query-transformer");
/**
* Transforms a custom query by calling the user's API server.
* Caches the transformed queries for 5 seconds to avoid unnecessary API calls.
*
* Error responses are not cached as the user may want to retry the query
* and the error may be transient.
*
* The TTL was chosen to be 5 seconds since custom query requests come with
* a token which itself may have a short TTL (e.g., 10 seconds).
*
* Token expiration isn't expected to be exact so this 5 second
* caching shouldn't cause unexpected behavior. E.g., many JWT libraries
* implement leeway for expiration checks: https://github.com/panva/jose/blob/main/docs/jwt/verify/interfaces/JWTVerifyOptions.md#clocktolerance
*
* The ViewSyncer will call the API server 3-4 times with the exact same queries
* if we do not cache requests.
*
* Caching is safe here because the cache key encodes the effective request
* identity used for `/query`: auth, userID, forwarded cookies, origin,
* custom headers, target URL, and the query hash itself.
*/
var CustomQueryTransformer = class {
	#shard;
	#cache;
	#lc;
	constructor(lc, shard) {
		this.#shard = shard;
		this.#lc = lc;
		this.#cache = new TimedCache(5e3);
	}
	/**
	* Forces the empty `/query` validation request used by auth maintenance.
	*
	* This stays separate from `transform()` because `transform([], ...)`
	* short-circuits locally and never hits the API server, while validation
	* still needs to make the request so auth failures are surfaced.
	* Successful validation is intentionally opaque because callers only care
	* whether the request succeeded or failed.
	*/
	async validate(ctx) {
		const response = await this.#requestTransform(ctx, [], "validate");
		return Array.isArray(response) ? void 0 : response;
	}
	async transform(ctx, queries) {
		const request = [];
		const cachedResponses = [];
		for (const query of queries) {
			const cacheKey = getCacheKey(ctx, query.id);
			const cached = this.#cache.get(cacheKey);
			if (cached) cachedResponses.push(cached);
			else request.push({
				id: query.id,
				name: query.name,
				args: query.args
			});
		}
		let cached = true;
		if (request.length === 0) return {
			result: cachedResponses,
			cached: true
		};
		else cached = false;
		const response = await this.#requestTransform(ctx, request, "transform");
		if (!Array.isArray(response)) return {
			result: response,
			cached
		};
		const newResponses = response.map((transformed) => {
			if ("error" in transformed) return transformed;
			return {
				id: transformed.id,
				transformedAst: transformed.ast,
				transformationHash: hashOfAST(transformed.ast)
			};
		});
		for (const transformed of newResponses) {
			if ("error" in transformed) continue;
			const cacheKey = getCacheKey(ctx, transformed.id);
			this.#cache.set(cacheKey, transformed);
		}
		return {
			result: [...newResponses, ...cachedResponses],
			cached
		};
	}
	async #requestTransform(ctx, request, operation) {
		const queryIDs = request.map(({ id }) => id);
		try {
			return (await startAsyncSpan(tracer, "customQueryTransformer.fetchFromAPIServer", () => fetchFromAPIServer(transformResponseMessageSchema, "transform", this.#lc, ctx, this.#shard, ["transform", request])))[1];
		} catch (e) {
			if (isProtocolError(e) && e.errorBody.kind === "TransformFailed") return {
				...e.errorBody,
				queryIDs
			};
			return {
				kind: TransformFailed,
				origin: ZeroCache,
				reason: Internal,
				message: `Failed to ${operation} queries: ${getErrorMessage(e)}`,
				queryIDs
			};
		}
	}
};
function getCacheKey(ctx, queryID) {
	return JSON.stringify({
		queryID,
		token: ctx.auth?.raw,
		cookie: ctx.queryContext.headerOptions.cookie,
		origin: ctx.queryContext.headerOptions.origin,
		userID: ctx.userID,
		url: ctx.queryContext.url,
		customHeaders: normalizedForwardedHeaders(ctx.queryContext.headerOptions)
	});
}
function normalizedForwardedHeaders(headerOptions) {
	const { allowedClientHeaders, customHeaders } = headerOptions;
	if (!customHeaders || !allowedClientHeaders || allowedClientHeaders.length === 0) return;
	const allowedHeaders = new Set(allowedClientHeaders.map((header) => header.toLowerCase()));
	const forwardedHeaders = sortedEntries(customHeaders).filter(([header]) => allowedHeaders.has(header.toLowerCase()));
	return forwardedHeaders.length === 0 ? void 0 : JSON.stringify(forwardedHeaders);
}
//#endregion
export { CustomQueryTransformer };

//# sourceMappingURL=transform-query.js.map