Reload Block

Dev-only mechanism that intercepts page reloads in the playground so HMR doesn’t blow away in-progress state without consent. Implemented in screens/playground/PlaygroundUpdateGuard.tsx.

How it works

  • On mount (DEV builds only), patches Location.prototype.reload with a configurable descriptor whose replacement value checks an internal _blocked flag.
  • While blocked, calls to window.location.reload() are suppressed and a pg:reload-blocked CustomEvent is dispatched on window instead of triggering navigation.
  • The original reload is captured via Location.prototype.reload and .bind(window.location) before override, then stored at module scope so HMR re-renders of the component don’t lose it.
  • Install is idempotent — installReloadBlock early-returns true when _blocked is already set. Wrapped in try/catch to handle browsers that refuse the override.
  • Uninstall restores the captured original via the same Object.defineProperty path; module-level state means the patch survives component HMR.

UI

  • A fixed-position amber badge (#f59e0b, top-right, z-index: 999999) reading ↻ Update available — click to reload appears only when a reload has been blocked.
  • Clicking the badge calls removeReloadBlock() then the captured _originalReload, performing the real navigation.
  • Badge is rendered conditionally on pending && import.meta.env.DEV — never visible in production builds.

Vite HMR hook

  • Belt-and-suspenders: subscribes to import.meta.hot.on('vite:beforeFullReload', ...) and sets pending there as well, so the badge appears even on HMR-initiated full reloads that don’t route through location.reload().

Event contract

NameTargetPayloadEmitted when
pg:reload-blockedwindownonelocation.reload() is called while _blocked is true

Listeners (the guard component itself) set pending = true, which renders the badge.

Production behavior

useEffect returns early when !import.meta.env.DEV, and the badge render is gated on the same flag. No patch installs and no UI appears in production.