| 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); |
| }; |
| } |
|
|
| |
| |
| 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; |
| } |
| } |
|
|
| |
| |
| |
| |
| 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) { |
| |
| if (!(err instanceof DOMException && err.name === 'AbortError')) { |
| markBackendDown(); |
| } |
| throw err; |
| } |
| } |
|
|