nav resilience after pool restart, thinner marker rings, drop dead marker popup

This commit is contained in:
2026-07-31 10:17:28 -04:00
parent d84f7e3911
commit ff61a4c5e9
6 changed files with 163 additions and 100 deletions

View File

@@ -7,11 +7,18 @@ import { pluginsApi } from '../api'
let enabledPromise = null
let enabledSet = null // resolved Set<string>, 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) {
enabledPromise = pluginsApi.enabled()
const fetched = pluginsApi.enabled()
.then(response => {
const names = response?.data?.data
// Only trust a real array; anything else -> fail open (null).
@@ -23,6 +30,19 @@ export function loadEnabledPlugins() {
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
}