import { useEffect, useState } from 'react' import { api } from '../api/client.js' // Which shard surfaces the current viewer may reach, from // GET /public/shard/features. Admins configure this per feature (Admin → Shard // Visibility), so the nav can't be a static list any more. // // This is PRESENTATION only. The gate is server-side: a disabled feature 404s // and an out-of-rung one 403s whether or not the link is rendered. So while the // answer is still in flight we return `null` and callers show their default set // — better a link that briefly 403s than a nav that flickers in on every load. // // Cached module-level: the answer is per-viewer but stable for a session, and // every consumer would otherwise refetch it on mount. let cached = null let inFlight = null export function resetShardFeatures() { cached = null inFlight = null } export function useShardFeatures() { const [features, setFeatures] = useState(cached) useEffect(() => { if (cached) return undefined let alive = true inFlight = inFlight || api.shard .features() .then((data) => { cached = { level: data.level, set: new Set(data.features || []) } return cached }) .catch(() => { // A failed lookup must not blank the nav — fall back to "show // everything" and let the server do the gating. cached = null inFlight = null return null }) inFlight.then((result) => { if (alive) setFeatures(result) }) return () => { alive = false } }, []) return features } // Convenience: true when `name` is visible, or when we don't know yet. export function canSee(features, name) { return !features || features.set.has(name) }