File size: 2,464 Bytes
391c43e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
84
85
86
87
88
89
90
91
92
93
94
95
export type BackendStatus = {
  backendDown: boolean;
  authExpired: boolean;
};

type Listener = (status: BackendStatus) => void;

const state: BackendStatus = { backendDown: false, authExpired: false };
const listeners = new Set<Listener>();

function notify() {
  const snapshot = { ...state };
  listeners.forEach((l) => l(snapshot));
}

export function markBackendDown(): void {
  if (!state.backendDown) {
    state.backendDown = true;
    notify();
  }
}

export function markBackendUp(): void {
  if (state.backendDown) {
    state.backendDown = false;
    notify();
  }
}

export function markAuthExpired(): void {
  if (!state.authExpired) {
    state.authExpired = true;
    notify();
  }
}

export function clearAuthExpired(): void {
  if (state.authExpired) {
    state.authExpired = false;
    notify();
  }
}

export function getBackendStatus(): BackendStatus {
  return { ...state };
}

export function subscribeBackendStatus(listener: Listener): () => void {
  listeners.add(listener);
  return () => {
    listeners.delete(listener);
  };
}

// Auth-gated paths that return 401 when the session cookie is missing or expired.
// Matches middleware.ts.
function isAuthGatedPath(url: string): boolean {
  try {
    const path = new URL(url, typeof window !== 'undefined' ? window.location.origin : 'http://localhost').pathname;
    return path.startsWith('/api/w/') || path.startsWith('/api/admin/');
  } catch {
    return false;
  }
}

// Wraps fetch() for calls to our own Next.js API routes.
// Flips the "backend down" banner on when the server is unreachable (network
// error or 5xx) and off when a request succeeds. A 401 from an auth-gated
// route flips the "auth expired" banner on; a 2xx from one flips it off.
export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
  const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
  try {
    const res = await fetch(input, init);
    if (res.status >= 500) {
      markBackendDown();
    } else {
      markBackendUp();
    }
    if (isAuthGatedPath(url)) {
      if (res.status === 401) {
        markAuthExpired();
      } else if (res.ok) {
        clearAuthExpired();
      }
    }
    return res;
  } catch (err) {
    // Ignore user-initiated aborts — not a backend failure
    if (!(err instanceof DOMException && err.name === 'AbortError')) {
      markBackendDown();
    }
    throw err;
  }
}