File size: 2,844 Bytes
65d68f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | 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<T>(
request: () => Promise<T>,
fallback: () => Promise<T> | T,
): Promise<T> {
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;
}
}
|