import { isApiError } from "@/lib/api/client"; import { getAuthToken, isDemoPreviewSession } from "@/lib/auth/auth-client"; import { getPublicDemoMode } from "@/lib/env"; export function isLocalResourceId(id: string | null | undefined) { if (!id) return false; if (id.startsWith("doc-local-") || id.startsWith("paper-local-") || id.startsWith("job-local-") || id.startsWith("render-local-")) { return true; } return !/^(doc|paper|vjob|usr)_[a-z0-9]+$/i.test(id); } export function hasLiveSession() { return Boolean(getAuthToken()) && !isDemoPreviewSession(); } /** * Returns true when the frontend should use local mock data instead of calling the API. * * In "demo" mode: always use demo data. * In "live" mode: never use demo data. * In "auto" mode: use demo data only for local resource IDs (doc-local-xxx). */ export function shouldUseDemoData(resourceId?: string | null) { if (isDemoPreviewSession()) return true; const mode = getPublicDemoMode(); if (mode === "demo") return true; if (mode === "live") return false; // "auto" — use demo data only for local IDs (offline-created resources) if (resourceId && isLocalResourceId(resourceId)) return true; return false; } /** * Whether to silently fall back to demo data on API errors. * * Only returns true in explicit "demo" mode, or in "auto" mode when * no auth token exists (i.e., no backend connection has been established). * * When a real session exists (token present), errors must surface to the * user so they know something failed — not silently replaced with fakes. */ function shouldFallbackOnError(): boolean { if (isDemoPreviewSession()) return true; const mode = getPublicDemoMode(); if (mode === "demo") return true; if (mode === "live") return false; // "auto" — only fall back if there is no live session (no token = no backend) return !hasLiveSession(); } /** * Wraps an API call with a fallback. In production (live/auto with token), * errors are propagated to the UI so students see real error states. * Only in demo mode (or auto without a backend) are errors silently caught. */ export async function withDemoFallback( request: () => Promise, fallback: () => Promise | T, ): Promise { try { return await request(); } catch (error) { // Auth errors ALWAYS re-throw regardless of mode if (isApiError(error) && error.isAuthError) { throw error; } // In demo-only mode (no real backend), silently fall back if (shouldFallbackOnError()) { const isRecoverable = (isApiError(error) && (error.status === null || error.status >= 500)) || error instanceof TypeError; if (isRecoverable) { return fallback(); } } // In production: propagate the error so the UI shows a real error state throw error; } }