PURPOSE
Detects when a newer build of the game has been deployed and surfaces a restart prompt to the player. Polls /version.txt on a fixed interval and exposes the result through a hook plus two banner components — a hub-bottom banner that overlaps the LAUNCH button and a safe-route fullscreen modal overlay. Never interrupts gameplay, post-game stats, reveal, or level select.
OWNS
- Module-scoped polling state:
_updateAvailable,_listeners,_started. extractVersion(text)helper that parses thevX.Y.Ztoken out ofversion.txt._startPolling()boot routine that schedules the first check after 2 minutes and then everyCHECK_INTERVAL(60s)._notifyListeners()fan-out that pushes the latest_updateAvailableto every subscribed React setter.useUpdateAvailable()hook that subscribes a component to the shared flag and returns it.HubUpdateBannercomponent (slide-up bottom banner withmed-banner-bottomstyling, click anywhere reloads).UpdateBannercomponent (fullscreenMedPanelModaloverlay gated toSAFE_UPDATE_ROUTES).SAFE_UPDATE_ROUTESallowlist:/,/shop,/collection,/profile,/settings,/admin,/prologue.
READS FROM
BUILD_VERSIONfrom@starship-survivors/engine/core/config— used to deriveCURRENT_SHORT(the localvX.Y.Ztoken).MedPanel,MedPanelModalfrom./MedPanel— the visual shell for the fullscreen overlay variant.fetch('/version.txt?_vc=<ts>')withcache: 'no-store'— the deployed version manifest served from the site root.window.location.pathname— to gate the fullscreen overlay to safe routes.- React
useState,useEffect— local subscription wiring.
PUSHES TO
window.location.reload()on banner click and on the inline CLICK TO RESTART button (both banners). This is the only side effect that escapes the module.- Subscribed React setters in
_listenerswhenever_updateAvailableflips true.
DOES NOT
- Does not write to localStorage, sessionStorage, Supabase, or any store.
- Does not emit telemetry or analytics.
- Does not flip
_updateAvailableback to false once set — the flag latches until reload. - Does not poll on app boot — the first check is deferred 120s after the first hook mount.
- Does not start polling if
CURRENT_SHORTis empty (i.e.BUILD_VERSIONdoes not matchvX.Y.Z). - Does not show the fullscreen overlay on gameplay, reveal, stats, or level select routes.
- Does not provide a dismiss affordance — both banners are click-to-reload only.
- Does not retry on fetch error; network failures are swallowed and the next interval tick tries again.
Signals
- Detection signal: remote
vX.Y.Zfromversion.txtdiffers fromCURRENT_SHORTand_updateAvailableis still false. - State flip:
_updateAvailable = truethen_notifyListeners()broadcast. - UI signal in hub:
HubUpdateBannerreturns its slide-up banner element instead ofnull. - UI signal on safe routes:
UpdateBannerreturns theMedPanelModaloverlay instead ofnull. - Player action: click anywhere on either banner or the inline button triggers
window.location.reload(). - No-op signal:
useUpdateAvailable()returns false; both components rendernull.
Entry points
useUpdateAvailable()— exported hook; first caller triggers_startPolling()via itsuseEffect.HubUpdateBanner()— exported React component; mounted by the hub screen at the bottom of the LAUNCH column.UpdateBanner()— exported React component; mounted at the app shell level to render on any safe route.
Pattern notes
- Shared state lives at module scope, not in a Zustand store.
_updateAvailable,_listeners, and_startedare plain module variables so polling persists across mount/unmount of any single component and stays singleton across the app. - Polling is started lazily on the first
useUpdateAvailable()mount, not at module import time, so server-side or pre-mount imports do not kick off network traffic._startedis the idempotency guard. - The first poll is intentionally delayed 120s to avoid hammering the deploy moments after launch; subsequent polls are every 60s (
CHECK_INTERVAL). - The
?_vc=<Date.now()>cache buster pluscache: 'no-store'defeats any intermediate CDN or service-worker cache that would otherwise pinversion.txt. _updateAvailableis one-way latching. Once true it never resets, even if a later poll succeeds with the old version — this prevents a flapping banner.- Hub banner uses CSS classes from the medical-UI theme (
med-banner,med-banner-bottom,med-banner-accent,med-banner-title,med-btn,med-btn-primary); fullscreen overlay reusesMedPanelwith thedoorsvariant. - Safe-route gating uses exact path match with optional trailing slash, not prefix match — gameplay subpaths under those roots will still be protected.
- The fullscreen overlay has no
onScrimClickhandler, so it is non-dismissible by design; the only exit is reload. - Hub banner stops propagation on the inner button click so the outer div’s reload handler does not double-fire.