Leon4gr45/builder
0
1export type BackendStatus = {2 backendDown: boolean;3 authExpired: boolean;4};5 6type Listener = (status: BackendStatus) => void;7 8const state: BackendStatus = { backendDown: false, authExpired: false };9const listeners = new Set<Listener>();10 11function notify() {12 const snapshot = { ...state };13 listeners.forEach((l) => l(snapshot));14}15 16export function markBackendDown(): void {17 if (!state.backendDown) {18 state.backendDown = true;19 notify();20 }21}22 23export function markBackendUp(): void {24 if (state.backendDown) {25 state.backendDown = false;26 notify();27 }28}29 30export function markAuthExpired(): void {31 if (!state.authExpired) {32 state.authExpired = true;33 notify();34 }35}36 37export function clearAuthExpired(): void {38 if (state.authExpired) {39 state.authExpired = false;40 notify();41 }42}43 44export function getBackendStatus(): BackendStatus {45 return { ...state };46}47 48export function subscribeBackendStatus(listener: Listener): () => void {49 listeners.add(listener);50 return () => {51 listeners.delete(listener);52 };53}54 55// Auth-gated paths that return 401 when the session cookie is missing or expired.56// Matches middleware.ts.57function isAuthGatedPath(url: string): boolean {58 try {59 const path = new URL(url, typeof window !== 'undefined' ? window.location.origin : 'http://localhost').pathname;60 return path.startsWith('/api/w/') || path.startsWith('/api/admin/');61 } catch {62 return false;63 }64}65 66// Wraps fetch() for calls to our own Next.js API routes.67// Flips the "backend down" banner on when the server is unreachable (network68// error or 5xx) and off when a request succeeds. A 401 from an auth-gated69// route flips the "auth expired" banner on; a 2xx from one flips it off.70export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {71 const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;72 try {73 const res = await fetch(input, init);74 if (res.status >= 500) {75 markBackendDown();76 } else {77 markBackendUp();78 }79 if (isAuthGatedPath(url)) {80 if (res.status === 401) {81 markAuthExpired();82 } else if (res.ok) {83 clearAuthExpired();84 }85 }86 return res;87 } catch (err) {88 // Ignore user-initiated aborts — not a backend failure89 if (!(err instanceof DOMException && err.name === 'AbortError')) {90 markBackendDown();91 }92 throw err;93 }94}95 