/**
 * Router Loader Resolution
 *
 * Loader execution, memoization, and error handling utilities.
 */
import { track } from "../server/context";
import { isHandle } from "../handle.js";
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
import { getRequestContext } from "../server/request-context.js";
import { debugLog } from "./logging.js";
/**
 * Wrap a loader promise with error handling for deferred client-side resolution.
 * Catches errors and converts them to LoaderDataResult objects that include
 * error info and pre-rendered fallback UI when an error boundary is available.
 *
 * @param onError - Optional callback invoked when loader errors occur.
 *   This has a simplified signature for internal use - the caller (typically
 *   wrapLoaderPromise in router.ts) is responsible for bridging to the full
 *   OnErrorCallback with complete request context (request, url, env, etc.).
 */
export function wrapLoaderWithErrorHandling(promise, entry, segmentId, pathname, findNearestErrorBoundary, createErrorInfo, onError) {
    // Extract loader name from segmentId (format: "M1L0D0.loaderName")
    const loaderName = segmentId.split(".").pop() || "unknown";
    return Promise.resolve(promise)
        .then((data) => ({
        __loaderResult: true,
        ok: true,
        data,
    }))
        .catch((error) => {
        // Find nearest error boundary
        const fallback = findNearestErrorBoundary(entry);
        // Create error info
        const errorInfo = createErrorInfo(error, segmentId, "loader");
        // Invoke onError callback if provided
        onError?.(error, {
            segmentId,
            loaderName,
            handledByBoundary: !!fallback,
        });
        if (!fallback) {
            // No error boundary - return error result without fallback
            // Client will throw this error
            return {
                __loaderResult: true,
                ok: false,
                error: errorInfo,
                fallback: null,
            };
        }
        // Render fallback on server
        let renderedFallback;
        if (typeof fallback === "function") {
            // ErrorBoundaryHandler - call with error info
            const props = {
                error: errorInfo,
            };
            renderedFallback = fallback(props);
        }
        else {
            renderedFallback = fallback;
        }
        debugLog("loader", "loader error wrapped with boundary fallback", {
            segmentId,
            message: errorInfo.message,
        });
        return {
            __loaderResult: true,
            ok: false,
            error: errorInfo,
            fallback: renderedFallback,
        };
    });
}
/**
 * Detect cycles in the loader dependency graph using DFS from a given node.
 * Returns the cycle path (array of loader IDs forming the cycle) if one exists,
 * or null if no cycle is found.
 */
function detectLoaderCycle(from, to, dependsOn) {
    // If `to` can reach `from` via the dependency graph, adding the edge
    // from -> to creates a cycle. We search from `to` looking for `from`.
    const visited = new Set();
    const path = [from, to];
    function dfs(current) {
        if (current === from) {
            // Found a cycle: return the path leading back to `from`
            return path;
        }
        if (visited.has(current))
            return null;
        visited.add(current);
        const deps = dependsOn.get(current);
        if (!deps)
            return null;
        for (const dep of deps) {
            path.push(dep);
            const cycle = dfs(dep);
            if (cycle)
                return cycle;
            path.pop();
        }
        return null;
    }
    return dfs(to);
}
/**
 * Creates a memoizing loader executor with cycle detection.
 * Shared by setupLoaderAccess and setupLoaderAccessSilent; only the handle
 * branch differs between the two, so only the loader logic is extracted here.
 *
 * Returns a useLoader(loader, callerLoaderId) function that:
 * - Tracks dependency edges between loaders for cycle detection
 * - Throws immediately (synchronously inside an async fn) on circular deps
 * - Memoizes each loader's promise so it runs at most once per request
 */
function createLoaderExecutor(ctx, loaderPromises) {
    // Dependency graph: loaderId -> set of loader IDs it directly depends on.
    const dependsOn = new Map();
    // Loaders whose promises have not yet settled.
    // A dependency on a pending loader that closes a cycle means deadlock.
    const pendingLoaders = new Set();
    function useLoader(loader, callerLoaderId) {
        // Record the dependency edge and check for cycles before running
        if (callerLoaderId !== null) {
            let deps = dependsOn.get(callerLoaderId);
            if (!deps) {
                deps = new Set();
                dependsOn.set(callerLoaderId, deps);
            }
            // Only relevant when the target is still pending (would deadlock)
            if (pendingLoaders.has(loader.$$id)) {
                const cycle = detectLoaderCycle(callerLoaderId, loader.$$id, dependsOn);
                if (cycle) {
                    throw new Error(`Circular loader dependency detected: ${cycle.join(" -> ")}. ` +
                        `Loaders cannot depend on each other in a cycle. ` +
                        `Refactor to break the circular dependency.`);
                }
            }
            deps.add(loader.$$id);
        }
        // Return cached promise if already started
        if (loaderPromises.has(loader.$$id)) {
            return loaderPromises.get(loader.$$id);
        }
        // Get loader function - either from loader object or fetchable registry
        let loaderFn = loader.fn;
        if (!loaderFn) {
            const fetchable = getFetchableLoader(loader.$$id);
            if (fetchable) {
                loaderFn = fetchable.fn;
            }
        }
        if (!loaderFn) {
            throw new Error(`Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`);
        }
        pendingLoaders.add(loader.$$id);
        const currentLoaderId = loader.$$id;
        const loaderCtx = {
            params: ctx.params,
            request: ctx.request,
            searchParams: ctx.searchParams,
            pathname: ctx.pathname,
            url: ctx.url,
            env: ctx.env,
            var: ctx.var,
            get: ctx.get,
            use: (dep) => {
                return useLoader(dep, currentLoaderId);
            },
            method: "GET",
            body: undefined,
        };
        const doneLoader = track(`loader:${loader.$$id}`);
        const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
            pendingLoaders.delete(loader.$$id);
            doneLoader();
        });
        loaderPromises.set(loader.$$id, promise);
        return promise;
    }
    return useLoader;
}
/**
 * Set up the use() method on handler context to access loaders and handles.
 *
 * For loaders: Lazily runs loaders, memoizes results per request.
 * For handles: Returns a push function bound to the current segment.
 *
 * Includes cycle detection: tracks dependency edges between loaders and
 * throws on circular dependencies to prevent deadlocks.
 */
export function setupLoaderAccess(ctx, loaderPromises) {
    const getHandleStore = () => {
        return getRequestContext()?._handleStore;
    };
    const useLoader = createLoaderExecutor(ctx, loaderPromises);
    ctx.use = ((item) => {
        if (isHandle(item)) {
            const handle = item;
            const store = getHandleStore();
            const segmentId = ctx._currentSegmentId;
            if (!segmentId) {
                throw new Error(`Handle "${handle.$$id}" used outside of handler context. ` +
                    `Handles must be used within route/layout handlers.`);
            }
            return (dataOrFn) => {
                if (!store)
                    return;
                const valueOrPromise = typeof dataOrFn === "function"
                    ? dataOrFn()
                    : dataOrFn;
                store.push(handle.$$id, segmentId, valueOrPromise);
            };
        }
        return useLoader(item, null);
    });
}
/**
 * Set up ctx.use() for pre-rendering (build-time).
 * Handles push to HandleStore; loaders throw with a clear error.
 */
export function setupBuildUse(ctx) {
    // Get HandleStore from request context
    const getHandleStore = () => {
        return getRequestContext()?._handleStore;
    };
    ctx.use = ((item) => {
        // Handle case: return a push function bound to the current segment
        if (isHandle(item)) {
            const handle = item;
            const store = getHandleStore();
            const segmentId = ctx._currentSegmentId;
            if (!segmentId) {
                throw new Error(`Handle "${handle.$$id}" used outside of handler context. ` +
                    `Handles must be used within route/layout handlers.`);
            }
            return (dataOrFn) => {
                if (!store)
                    return;
                const valueOrPromise = typeof dataOrFn === "function"
                    ? dataOrFn()
                    : dataOrFn;
                store.push(handle.$$id, segmentId, valueOrPromise);
            };
        }
        // Loader case: not available during pre-rendering
        throw new Error("Loaders are not available during pre-rendering. " +
            "Use them on parent layouts with cache() for request-time data, " +
            "or use a passthrough prerender handler.");
    });
}
/**
 * Set up ctx.use() for proactive caching (silent mode).
 * Handles are silently ignored (no push to HandleStore).
 * Loaders work normally but with fresh memoization and cycle detection.
 *
 * This prevents duplicate handle data (breadcrumbs, meta) from being
 * pushed to the response stream during background proactive caching.
 */
export function setupLoaderAccessSilent(ctx, loaderPromises) {
    const useLoader = createLoaderExecutor(ctx, loaderPromises);
    ctx.use = ((item) => {
        if (isHandle(item)) {
            // Silent mode - return a no-op so handle data is not pushed during caching
            return (_dataOrFn) => { };
        }
        return useLoader(item, null);
    });
}
/**
 * Conditional execution based on revalidation
 * Evaluates revalidation logic lazily, then executes appropriate callback
 *
 * @param shouldRevalidate - Async function that determines if revalidation is needed
 * @param onRevalidate - Callback executed if revalidation returns true
 * @param onSkip - Callback executed if revalidation returns false
 * @returns Result from either onRevalidate or onSkip
 */
export async function revalidate(shouldRevalidate, onRevalidate, onSkip) {
    const needsRevalidation = await shouldRevalidate();
    return needsRevalidation ? await onRevalidate() : onSkip();
}
//# sourceMappingURL=loader-resolution.js.map