diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 833ce2d..14a4267 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -55,9 +55,11 @@ async function doRefreshAccessToken(): Promise { return false; } +const DEFAULT_TIMEOUT_MS = 30_000; + export async function api( path: string, - options: RequestInit = {} + options: RequestInit & { timeoutMs?: number } = {} ): Promise { const token = getToken(); const headers: Record = { @@ -68,31 +70,40 @@ export async function api( headers['Authorization'] = `Bearer ${token}`; } - let res = await fetch(`${API_BASE}${path}`, { ...options, headers }); + const { timeoutMs, ...fetchOptions } = options; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs ?? DEFAULT_TIMEOUT_MS); + const signal = options.signal ?? controller.signal; - // Try token refresh on 401 - if (res.status === 401 && token) { - const refreshed = await refreshAccessToken(); - if (refreshed) { - headers['Authorization'] = `Bearer ${getToken()}`; - res = await fetch(`${API_BASE}${path}`, { ...options, headers }); + try { + let res = await fetch(`${API_BASE}${path}`, { ...fetchOptions, headers, signal }); + + // Try token refresh on 401 + if (res.status === 401 && token) { + const refreshed = await refreshAccessToken(); + if (refreshed) { + headers['Authorization'] = `Bearer ${getToken()}`; + res = await fetch(`${API_BASE}${path}`, { ...fetchOptions, headers, signal }); + } } - } - if (res.status === 401) { - clearTokens(); - if (typeof window !== 'undefined') { - window.location.href = '/login'; + if (res.status === 401) { + clearTokens(); + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + throw new Error('Unauthorized'); } - throw new Error('Unauthorized'); + + if (res.status === 204) return undefined as T; + + if (!res.ok) { + const err = await res.json().catch(() => ({ detail: res.statusText })); + throw new Error(err.detail || `HTTP ${res.status}`); + } + + return res.json(); + } finally { + clearTimeout(timeout); } - - if (res.status === 204) return undefined as T; - - if (!res.ok) { - const err = await res.json().catch(() => ({ detail: res.statusText })); - throw new Error(err.detail || `HTTP ${res.status}`); - } - - return res.json(); } diff --git a/frontend/src/lib/components/SearchPalette.svelte b/frontend/src/lib/components/SearchPalette.svelte index 40ba56f..cf6f348 100644 --- a/frontend/src/lib/components/SearchPalette.svelte +++ b/frontend/src/lib/components/SearchPalette.svelte @@ -136,6 +136,7 @@ }); const flatResults = $derived(results); + const flatIndexMap = $derived(new Map(flatResults.map((item, idx) => [item, idx]))); async function openPalette() { open = true; @@ -239,7 +240,7 @@ {group.label} {#each group.items as item, i} - {@const flatIdx = flatResults.indexOf(item)} + {@const flatIdx = flatIndexMap.get(item) ?? -1}