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;
  }
}