'use client'; import { useUIStore } from '@/stores/ui-store'; export interface ApiFetchOptions extends Omit { body?: unknown; } /** In-memory cache: slug -> id, populated lazily by `resolvePortIdFromSlug`. * Avoids re-fetching `/api/v1/admin/ports` on every request when the Zustand * store hasn't hydrated yet (fresh browser context, e2e tests, hard reload). */ const slugToIdCache = new Map(); async function resolvePortIdFromSlug(slug: string): Promise { const cached = slugToIdCache.get(slug); if (cached) return cached; try { const res = await fetch('/api/v1/admin/ports', { credentials: 'include' }); if (!res.ok) return null; const body = (await res.json()) as { data?: Array<{ id: string; slug: string }> }; const port = body.data?.find((p) => p.slug === slug); if (!port) return null; slugToIdCache.set(slug, port.id); return port.id; } catch { return null; } } /** * Client-side fetch wrapper that attaches the `X-Port-Id` header from the * UI store to every request. Used by all queryFn/mutationFn callbacks. * * Falls back to extracting the port slug from `window.location.pathname` and * resolving it via `/api/v1/admin/ports` when the Zustand store hasn't been * populated yet (fresh page load before `PortProvider`'s effect has fired). */ export async function apiFetch(url: string, opts: ApiFetchOptions = {}): Promise { let portId = useUIStore.getState().currentPortId; if (!portId && typeof window !== 'undefined') { const slug = window.location.pathname.split('/').filter(Boolean)[0]; if (slug && slug !== 'login' && slug !== 'portal' && slug !== 'api') { portId = await resolvePortIdFromSlug(slug); } } const headers = new Headers(opts.headers); if (portId) { headers.set('X-Port-Id', portId); } if (opts.body !== undefined && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } const res = await fetch(url, { ...opts, headers, credentials: 'include', body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, }); if (!res.ok) { const error = await res.json().catch(() => ({ error: res.statusText })); throw Object.assign(new Error(error.error ?? 'Request failed'), { status: res.status, code: error.code, details: error.details, }); } if (res.status === 204) return undefined as T; return res.json() as Promise; }