/// <reference path="../../vite/version.d.ts" />
import { getRequestContext, } from "../../server/request-context.js";
import { VERSION } from "@rangojs/router:version";
// ============================================================================
// Constants
// ============================================================================
/** Header storing timestamp when entry becomes stale */
export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
/** Header storing cache status: HIT | REVALIDATING */
export const CACHE_STATUS_HEADER = "x-edge-cache-status";
/**
 * Maximum age in seconds for REVALIDATING status before allowing new revalidation.
 * After this period, a stale entry in REVALIDATING status will trigger revalidation again.
 * @internal
 */
export const MAX_REVALIDATION_INTERVAL = 30;
// ============================================================================
// CFCacheStore Implementation
// ============================================================================
export class CFCacheStore {
    constructor(options) {
        if (!options.ctx) {
            throw new Error("[CFCacheStore] ExecutionContext (ctx) is required. " +
                "Pass the Cloudflare ExecutionContext from your worker's fetch handler: " +
                "new CFCacheStore({ ctx: env.ctx })");
        }
        this.namespace = options.namespace;
        this.baseUrl = options.baseUrl ?? this.deriveBaseUrl();
        this.defaults = options.defaults;
        this.version = options.version ?? VERSION;
        this.keyGenerator = options.keyGenerator;
        this.waitUntil = (fn) => options.ctx.waitUntil(fn());
    }
    /**
     * Derive base URL from request hostname via requestContext.
     * Uses internal fallback for dev/preview environments.
     * @internal
     */
    deriveBaseUrl() {
        const fallback = "https://rsc-cache.internal.com/";
        const ctx = getRequestContext();
        if (!ctx?.request) {
            return fallback;
        }
        try {
            const url = new URL(ctx.request.url);
            const hostname = url.hostname;
            // Use fallback for dev/preview environments
            if (hostname === "localhost" ||
                hostname === "127.0.0.1" ||
                hostname.endsWith(".workers.dev") ||
                hostname.endsWith(".pages.dev")) {
                return fallback;
            }
            // Use actual hostname for production
            return `https://${hostname}/`;
        }
        catch {
            return fallback;
        }
    }
    /**
     * Get the cache instance - uses caches.default unless namespace is specified.
     * @internal
     */
    getCache() {
        if (this.namespace) {
            return caches.open(this.namespace);
        }
        return caches.default;
    }
    /**
     * Get cached entry data by key.
     *
     * Handles SWR atomically:
     * - If stale and not already revalidating, marks as REVALIDATING and returns shouldRevalidate: true
     * - If already REVALIDATING (and recent), returns shouldRevalidate: false
     * - If fresh, returns shouldRevalidate: false
     *
     * The atomic mark prevents thundering herd - only first request triggers revalidation.
     */
    async get(key) {
        try {
            const cache = await this.getCache();
            const request = this.keyToRequest(key);
            const response = await cache.match(request);
            if (!response) {
                return null;
            }
            // Read status headers
            const status = response.headers.get(CACHE_STATUS_HEADER);
            const age = Number(response.headers.get("age") ?? "0");
            const staleAt = Number(response.headers.get(CACHE_STALE_AT_HEADER) ?? "0");
            const isStale = staleAt > 0 && Date.now() > staleAt;
            const isRevalidating = status === "REVALIDATING" && age < MAX_REVALIDATION_INTERVAL;
            // Case 1: Fresh or already being revalidated - just return data
            if (!isStale || isRevalidating) {
                const data = (await response.json());
                return { data, shouldRevalidate: false };
            }
            // Case 2: Stale and needs revalidation - atomically mark REVALIDATING
            const [b1, b2] = response.body.tee();
            const headers = new Headers(response.headers);
            headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
            // Blocking write - must complete before returning to prevent race
            await cache.put(request, new Response(b1, { status: response.status, headers }));
            const data = (await new Response(b2).json());
            return { data, shouldRevalidate: true };
        }
        catch (error) {
            console.error("[CFCacheStore] get failed:", error);
            return null;
        }
    }
    /**
     * Store entry data with TTL and optional SWR window.
     * Uses waitUntil for non-blocking write when available.
     */
    async set(key, data, ttl, swr) {
        try {
            const cache = await this.getCache();
            const request = this.keyToRequest(key);
            // Extended TTL covers SWR window
            const swrWindow = swr ?? this.defaults?.swr ?? 0;
            const totalTtl = ttl + swrWindow;
            const staleAt = Date.now() + ttl * 1000;
            const response = new Response(JSON.stringify(data), {
                headers: {
                    "Content-Type": "application/json",
                    "Cache-Control": `public, max-age=${totalTtl}`,
                    [CACHE_STALE_AT_HEADER]: String(staleAt),
                    [CACHE_STATUS_HEADER]: "HIT",
                },
            });
            const putPromise = cache.put(request, response);
            if (this.waitUntil) {
                // Non-blocking write
                this.waitUntil(async () => {
                    await putPromise;
                });
            }
            else {
                // Blocking fallback
                await putPromise;
            }
        }
        catch (error) {
            console.error("[CFCacheStore] set failed:", error);
        }
    }
    /**
     * Delete a cached entry
     */
    async delete(key) {
        try {
            const cache = await this.getCache();
            return await cache.delete(this.keyToRequest(key));
        }
        catch (error) {
            console.error("[CFCacheStore] delete failed:", error);
            return false;
        }
    }
    // ============================================================================
    // Document Cache Methods
    // ============================================================================
    /**
     * Get a cached Response by key (for document-level caching).
     * Returns the response and whether it should be revalidated (SWR).
     */
    async getResponse(key) {
        try {
            const cache = await this.getCache();
            const request = this.keyToRequest(`doc:${key}`);
            const response = await cache.match(request);
            if (!response || response.status !== 200) {
                return null;
            }
            // Check staleness
            const staleAt = Number(response.headers.get(CACHE_STALE_AT_HEADER) || 0);
            const isStale = staleAt > 0 && Date.now() > staleAt;
            return {
                response,
                shouldRevalidate: isStale,
            };
        }
        catch (error) {
            console.error("[CFCacheStore] getResponse failed:", error);
            return null;
        }
    }
    /**
     * Store a Response with TTL and optional SWR window (for document-level caching).
     */
    async putResponse(key, response, ttl, swr) {
        try {
            const cache = await this.getCache();
            const request = this.keyToRequest(`doc:${key}`);
            // Extended TTL covers SWR window
            const swrWindow = swr ?? this.defaults?.swr ?? 0;
            const totalTtl = ttl + swrWindow;
            const staleAt = Date.now() + ttl * 1000;
            // Clone and add cache headers
            const headers = new Headers(response.headers);
            headers.set("Cache-Control", `public, max-age=${totalTtl}`);
            headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
            const toCache = new Response(response.body, {
                status: response.status,
                statusText: response.statusText,
                headers,
            });
            const putPromise = cache.put(request, toCache);
            if (this.waitUntil) {
                // Non-blocking write
                this.waitUntil(async () => {
                    await putPromise;
                });
            }
            else {
                // Blocking fallback
                await putPromise;
            }
        }
        catch (error) {
            console.error("[CFCacheStore] putResponse failed:", error);
        }
    }
    /**
     * Convert string key to Request object for CF Cache API.
     * Includes version in URL if specified (for cache invalidation on code changes).
     * @internal
     */
    keyToRequest(key) {
        const encodedKey = encodeURIComponent(key);
        // Include version in URL path to invalidate cache when version changes
        const versionPath = this.version ? `v/${this.version}/` : "";
        return new Request(`${this.baseUrl}${versionPath}${encodedKey}`, {
            method: "GET",
        });
    }
}
//# sourceMappingURL=cf-cache-store.js.map