The dashboard and residential interest smoke tests were intermittently
failing with the page rendering empty/skeleton state. Root causes:
1. ui-store persisted currentPortId/Slug, but those are URL-derived state.
After login lands on /<first-port-by-name>/dashboard, localStorage holds
that port. Hard-navigating to /port-nimara/... rehydrated the store with
the stale id, and useQuery fired with the wrong port before
PortProvider's URL-sync useEffect could correct it. Drop both fields
from partialize — PortProvider re-derives them from the route every
navigation.
2. apiFetch's slug-to-port fallback fired N parallel /api/v1/admin/ports
calls when N components mounted simultaneously with an empty store.
Dedupe in-flight lookups so a stampede collapses into one round-trip.
Also tightened four flaky smoke tests that depended on a fixed 3s wait or
non-waiting isVisible({timeout}) — replaced with expect(...).toBeVisible
or expect.poll so they handle dev-mode JIT cold-start delays cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
'use client';
|
|
|
|
import { useUIStore } from '@/stores/ui-store';
|
|
|
|
export interface ApiFetchOptions extends Omit<RequestInit, 'body'> {
|
|
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<string, string>();
|
|
/** Dedupe in-flight admin/ports lookups so a stampede of parallel apiFetch
|
|
* calls (typical on dashboard mount) collapses into a single network round-
|
|
* trip instead of N. */
|
|
let inFlightPortsLookup: Promise<Array<{ id: string; slug: string }> | null> | null = null;
|
|
|
|
async function resolvePortIdFromSlug(slug: string): Promise<string | null> {
|
|
const cached = slugToIdCache.get(slug);
|
|
if (cached) return cached;
|
|
if (!inFlightPortsLookup) {
|
|
inFlightPortsLookup = (async () => {
|
|
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 }> };
|
|
return body.data ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
})().finally(() => {
|
|
inFlightPortsLookup = null;
|
|
});
|
|
}
|
|
const ports = await inFlightPortsLookup;
|
|
const port = ports?.find((p) => p.slug === slug);
|
|
if (!port) return null;
|
|
slugToIdCache.set(slug, port.id);
|
|
return port.id;
|
|
}
|
|
|
|
/**
|
|
* 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<T = unknown>(url: string, opts: ApiFetchOptions = {}): Promise<T> {
|
|
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<T>;
|
|
}
|