// Enabled-plugin gating for the router. Fetches the enabled plugin-name list // ONCE (cached promise) so a disabled backend plugin's frontend routes become // unreachable by direct URL. Fail-open: any fetch error treats every plugin as // enabled, so an API blip can never brick navigation. import { pluginsApi } from '../api' let enabledPromise = null let enabledSet = null // resolved Set, or null while pending / on failure // The router AWAITS this, so it must never outlast a navigation. An app-pool // restart leaves requests hanging (IIS queues them while the worker starts) and // axios has no timeout, so an unbounded wait here froze navigation outright - // and because the promise was cached, every later navigation awaited the same // hung request and stayed frozen after the backend recovered. const GATE_TIMEOUT_MS = 4000 // Kick off (or reuse) the single fetch. The endpoint is jwt-optional so this // works on unauthenticated kiosk routes (e.g. /tv) too. export function loadEnabledPlugins() { if (!enabledPromise) { const fetched = pluginsApi.enabled() .then(response => { const names = response?.data?.data // Only trust a real array; anything else -> fail open (null). enabledSet = Array.isArray(names) ? new Set(names) : null return enabledSet }) .catch(() => { // Fail open: leave enabledSet null so isPluginEnabled returns true. enabledSet = null return null }) // Bounded wait, fail-open on expiry. A timed-out or failed attempt drops the // cached promise so the NEXT navigation retries instead of inheriting a dead // one; a request that lands late still populates enabledSet. let timer const bounded = new Promise(resolve => { timer = setTimeout(() => resolve(null), GATE_TIMEOUT_MS) }) enabledPromise = Promise.race([fetched, bounded]).then(result => { clearTimeout(timer) if (result === null) enabledPromise = null return result }) } return enabledPromise } // True if the plugin is enabled OR the list is unknown (fail-open). A null // enabledSet means we never got a trustworthy answer, so allow everything. export function isPluginEnabled(name) { if (!name) return true if (enabledSet === null) return true return enabledSet.has(name) } // Test/hot-reload hook: forget the cached result so the next call refetches. export function resetEnabledPlugins() { enabledPromise = null enabledSet = null }