{"version":3,"sources":["update-gate.mjs"],"sourcesContent":["// Kubicor — THE UPDATE GATE: A NEWER DEPLOY STOPS THE GAME UNTIL THE PAGE IS REFRESHED (the owner, Oct 11: \"disable the\n// game when an update is available, the user should have to refresh to continue playing\"; the owner's choice of how:\n// BLOCK AT ONCE, SAVE FIRST).\n//\n// WHY A GATE AND NOT THE TOAST. The page used to poll /version.json every 10 s and, on a new version, put up a persistent\n// \"Update available — click to refresh\" toast. A player could leave it there all evening, and an old page talking to new\n// functions and a new zone server is exactly the mix the shell-freshness work exists to prevent (tests/shell-freshness:\n// \"a mix of old and new is not a working program\"). An edit the new server refuses, a message the old page cannot read,\n// a catalogue that has moved under it: each is a bug report about a build nobody is running any more. So the toast is\n// gone and this takes its place: the moment a newer deploy is seen, the game stops and asks for one press of Refresh.\n//\n// ── WHAT HAPPENS, IN ORDER (block) ─────────────────────────────────────────────────────────────────────────────────\n//   1. AT ONCE: input stops (every key, press, wheel and touch outside the overlay is swallowed in the capture phase, the\n//      pointer lock is let go, the rest of the page goes inert), the game's frame loops are frozen (freeze), and the\n//      overlay goes up over everything, dialogs included: \"Kubicor has been updated\" / \"Refresh to keep playing.\", its\n//      one button reading \"Saving…\" and not yet pressable.\n//   2. SAVE FIRST: whatever is queued (mined and built cells, a pour, a scoop) goes out through the ordinary save path,\n//      app.js bdSettle, for at most FLUSH_MS. The cap is a promise race as well as bdSettle's own, so a save that never\n//      answers can hold the button for FLUSH_MS and no longer.\n//   3. THEN the zone socket and the presence heartbeat stop (disconnect), so nothing else happens on this page, and the\n//      button turns into Refresh, focused, so Enter (or Space) presses it. There is no close button, and Escape does\n//      nothing. Refresh reloads the same address (location.reload keeps the URL, the ?pos and everything else).\n//\n// ── WHEN IT FIRES, AND WHEN IT NEVER DOES (isNewerDeploy, check) ──────────────────────────────────────────────────\n//   ONLY ON A SUCCESSFULLY FETCHED, DIFFERENT VERSION. An offline tab, a 404, a 5xx, a Fastly timeout, an answer that is\n//   not JSON or carries no version: each is \"nothing known\", never \"a new version\". A blocked game is a much worse\n//   failure than a late one, so every doubt falls on the side of carrying on.\n//   NEVER AGAINST AN UNKNOWN BASELINE. The comparison is against the version THIS PAGE loaded with (app.js loadVersionHash).\n//   When that fetch failed at boot it fell back to 'dev', and a page that does not know its own version cannot tell an\n//   update from itself: the first version it does fetch becomes its baseline instead (the first-load race). Local\n//   serving says the same version.json it was loaded with, so a developer is never blocked by their own copy.\n//   NEVER IN CARD MODE. The card renderer (?card=1, card-render/) runs the game headless to take pictures; app.js does not\n//   install the gate there at all, and install refuses it too.\n//   WHEN: every POLL_MS, on the tab coming back into view or the window into focus (a laptop opened the morning after a\n//   deploy blocks before the first press, not a minute later), and on the browser saying it is online again. Checks\n//   never overlap, and none runs once blocked.\n//\n// ── ONLY A NEW CONTRACT BLOCKS (the owner's play review, Oct 23, s8: \"Give the server contract its own version and block\n// only when that changes. Otherwise load the new build at the next natural transition (landing, diving, a menu), back in\n// the same place\"; u8: \"A new version is ready. Your progress is saved.\" with Refresh now / After this) ─────────────────\n//   Every deploy stopped play, mid-play, for the owner too: a CSS tweak blocked as hard as a new edit protocol. The block\n//   exists for one case, an old page talking to new servers (above), and a deploy only makes that case when it changes\n//   what the servers do. So version.json carries a second hash, contractVersion (generate-version-hash.js): the servers'\n//   own code (functions/, zone-server/, minus their tests) and the rules, by what they do, not by their comments. Then:\n//   THE CONTRACT CHANGED (or either side does not say: an older version.json, a page that never learned its own): the block\n//     above, as it was, worded as u8 asks: \"A new version is ready\", \"Saving your progress…\" then \"Your progress is\n//     saved.\", and \"Refresh now\", which reloads in place (the page's `reload`: the same view, on foot where you stood).\n//   ONLY THE PAGE CHANGED: nothing stops. A notice says \"A new version is ready. Your progress is saved.\" with Refresh now\n//     (save, then reload in place) and After this (the notice goes; nothing else changes). Either way the new build is\n//     loaded at the next natural moment, which the page reports through transition(kind): a landing or a take-off done, a\n//     dive into or out of a system, a panel closed, or the tab coming back after RETURN_MS away. The save runs first.\n//   A later deploy that does change the contract still blocks, whatever the notice said.\n//\n// PURE BUT FOR WHAT IS HANDED IN: the fetch, the clock, the document and window, and the page's own hooks (flush,\n// freeze, disconnect, reload) all come in through install's options, so tests/update-gate.test.mjs drives the whole\n// flow in node with a stand-in document.\n\nexport const UPDATE_GATE = Object.freeze({\n  POLL_MS: 60000,        // how often a visible, playing page asks /version.json what is deployed\n  FLUSH_MS: 4000,        // the longest the save before the block may take (the owner: save first, then block)\n  FETCH_MS: 8000,        // a check that has not answered in this long has failed (and fails quietly: see above)\n  Z: 2147483640,         // over every layer the page has, the tooltip's 2147483000 included\n  NOTICE_Z: 2147483600,  // the soft notice: over the game and its panels, under the block\n  RETURN_MS: 60000,      // a tab away this long, coming back, is a natural moment to load the new build (transition)\n});\n\n// HAS THE SERVER CONTRACT CHANGED? Yes when both are known and differ, and ALSO when either is not known: a page or a\n// version.json from before contracts were hashed cannot say, and the block is what such a page has always done.\nexport function contractChanged(loaded, fetched) {\n  if (!knownVersion(loaded) || !knownVersion(fetched)) return true;\n  return String(fetched).trim() !== String(loaded).trim();\n}\n\n// A VERSION THE PAGE CAN COMPARE: a real deploy hash, not the boot fallback ('dev'), the placeholder before boot\n// ('LOADING'), or nothing.\nexport function knownVersion(v) {\n  return typeof v === 'string' && v.trim() !== '' && v !== 'dev' && v !== 'LOADING';\n}\n\n// IS `fetched` A NEWER DEPLOY THAN THE PAGE THAT IS RUNNING (`loaded`)? Only when both are real and they differ. Hashes\n// do not order, so \"newer\" means \"different from what this page is\": a rollback is an update too (the page in front of\n// the player is no longer what is deployed, which is the whole reason to stop).\nexport function isNewerDeploy(loaded, fetched) {\n  return knownVersion(loaded) && knownVersion(fetched) && String(fetched).trim() !== String(loaded).trim();\n}\n\n// ASK /version.json WHAT IS DEPLOYED: its version, or null for every kind of failure. cache: 'no-store' is the freshness\n// (the service worker never caches the file either: service-worker.js NEVER_CACHE_FILES); a unique ?t= URL would miss\n// the CDN for nothing.\nexport async function fetchDeployedVersion(fetchFn, opts) {\n  const d = await fetchDeployed(fetchFn, opts);\n  return d ? d.version : null;\n}\n// …and both of its hashes: { version, contract } (contract null when the file has none), or null for every failure.\nexport async function fetchDeployed(fetchFn, { timeoutMs = UPDATE_GATE.FETCH_MS, setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout } = {}) {\n  if (typeof fetchFn !== 'function') return null;\n  let timer = null, ctrl = null;\n  try {\n    ctrl = typeof AbortController === 'function' ? new AbortController() : null;\n    const timedOut = new Promise((resolve) => { timer = setTimeoutFn(() => { try { ctrl && ctrl.abort(); } catch (e) {} resolve(null); }, timeoutMs); });\n    const asked = (async () => {\n      const r = await fetchFn('/version.json', ctrl ? { cache: 'no-store', signal: ctrl.signal } : { cache: 'no-store' });\n      if (!r || !r.ok) return null;\n      const data = await r.json();\n      if (!(data && knownVersion(data.version))) return null;\n      return { version: String(data.version).trim(), contract: knownVersion(data.contractVersion) ? String(data.contractVersion).trim() : null };\n    })().catch(() => null);\n    return await Promise.race([asked, timedOut]);\n  } catch (e) {\n    return null;\n  } finally {\n    if (timer != null) try { clearTimeoutFn(timer); } catch (e) {}\n  }\n}\n\n// WHAT THE TWO PROMPTS SAY (u8: \"A new version is ready. Your progress is saved.\").\nexport const UPDATE_SAVED_SOFT = 'A new version is ready. Your progress is saved.';\nexport const UPDATE_SAVED_HARD = 'Your progress is saved. This update changes how the game talks to its servers, so it needs a refresh to keep playing.';\n\n// THE OVERLAY'S LOOK. Its own stylesheet, injected once, so it needs nothing of the page's (which may be the very thing\n// that changed). Opaque enough that the frozen frame behind reads as stopped, not as a game you could still play.\nconst CSS = `\n#update-gate { position: fixed; inset: 0; z-index: ${UPDATE_GATE.Z}; display: flex; align-items: center; justify-content: center;\n  padding: 16px; box-sizing: border-box; background: rgba(4, 5, 9, 0.88); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);\n  font-family: 'Space Mono', ui-monospace, monospace; color: #fff; pointer-events: auto; touch-action: none; }\n#update-gate .ug-card { max-width: 420px; width: 100%; box-sizing: border-box; padding: 26px 24px 22px; text-align: center;\n  background: #12131a; border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6); }\n#update-gate h2 { margin: 0 0 10px; font-size: 1.05rem; letter-spacing: 1px; font-weight: 700; }\n#update-gate p { margin: 0 0 20px; font-size: 0.86rem; color: #c9d1db; line-height: 1.5; }\n#update-gate button { font: inherit; font-size: 0.86rem; letter-spacing: 1px; min-width: 160px; min-height: 44px; padding: 10px 22px;\n  border-radius: 8px; border: 1px solid #8BC5E7; background: #8BC5E7; color: #05060a; font-weight: 700; cursor: pointer; }\n#update-gate button:disabled { background: transparent; color: #8BC5E7; cursor: progress; }\n#update-gate button:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }\n#update-notice { position: fixed; left: 50%; bottom: calc(var(--dock-h, 96px) + 14px); transform: translateX(-50%);\n  z-index: ${UPDATE_GATE.NOTICE_Z}; width: min(420px, calc(100vw - 32px)); box-sizing: border-box; padding: 14px 16px 12px;\n  background: #12131a; border: 1px solid rgba(139, 197, 231, 0.55); border-radius: 10px; box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);\n  font-family: 'Space Mono', ui-monospace, monospace; color: #fff; font-size: 0.82rem; line-height: 1.45; }\n#update-notice p { margin: 0 0 10px; }\n#update-notice .un-row { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }\n#update-notice button { font: inherit; font-size: 0.8rem; letter-spacing: 0.5px; min-height: 36px; padding: 6px 14px; border-radius: 7px;\n  border: 1px solid #8BC5E7; background: transparent; color: #8BC5E7; cursor: pointer; }\n#update-notice button.un-now { background: #8BC5E7; color: #05060a; font-weight: 700; }\n#update-notice button:disabled { cursor: progress; opacity: 0.7; }\n#update-notice button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }\n/* (and at the top while chat is open: it stood over the chat's line and Send, live test Oct 27) */\nbody.chat-open #update-notice { bottom: auto; top: calc(env(safe-area-inset-top, 0px) + 64px); }\n`;\n\n// THE GATE. install() arms it (and returns a handle; check() and block() are exposed for QA and the tests).\n//   loadedVersion   () => the version this page loaded with (app.js state.appVersion)\n//   loadedContract  () => the server contract this page loaded with (app.js state.contractVersion; see contractChanged)\n//   fetchFn         the page's fetch\n//   flush(ms)       the save before the block (app.js bdSettle); awaited, at most FLUSH_MS\n//   freeze()        stops the frame loops and the sound, at once\n//   disconnect()    stops the zone socket and the presence heartbeat, after the save\n//   reload()        reload the page in place (app.js reloadInPlace: the same view, on foot where the player stood)\n//   cardMode        true in the card renderer: nothing is installed\nexport function installUpdateGate(opts = {}) {\n  const {\n    loadedVersion = () => null, loadedContract = () => null, fetchFn = (typeof fetch === 'function' ? fetch : null),\n    flush = async () => {}, freeze = () => {}, disconnect = () => {}, reload = () => {},\n    doc = (typeof document !== 'undefined' ? document : null), win = (typeof window !== 'undefined' ? window : null),\n    cardMode = false, pollMs = UPDATE_GATE.POLL_MS, flushMs = UPDATE_GATE.FLUSH_MS, fetchMs = UPDATE_GATE.FETCH_MS, returnMs = UPDATE_GATE.RETURN_MS,\n    nowFn = () => Date.now(),\n    setIntervalFn = setInterval, clearIntervalFn = clearInterval, setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout,\n    log = (typeof console !== 'undefined' ? console : null),\n  } = opts;\n  const g = { installed: false, blocked: false, ready: false, checking: false, baseline: null, deployed: null, checks: 0,\n              overlay: null, button: null, timer: null, flushed: false, text: null,\n              pending: null, notice: null, reloading: false, hiddenAt: 0, baseContract: null, said: null };\n  // NOTHING IN CARD MODE (or with no page to cover): a handle that says so, and does nothing.\n  if (cardMode || !doc || !win) {\n    return { installed: false, blocked: false, ready: false, flushed: false, deployed: null, baseline: null, checks: 0, pending: null,\n             check: async () => false, block: async () => {}, refresh: () => {}, transition: async () => false, soft: () => {} };\n  }\n\n  // THE PAGE'S OWN VERSION, or, when it never learned it, the first one fetched (see \"never against an unknown baseline\").\n  function baseline() {\n    const v = loadedVersion();\n    if (knownVersion(v)) return String(v).trim();\n    return g.baseline;\n  }\n\n  async function check() {\n    if (g.blocked || g.checking) return false;\n    g.checking = true; g.checks++;\n    try {\n      const d = await fetchDeployed(fetchFn, { timeoutMs: fetchMs, setTimeoutFn, clearTimeoutFn });\n      if (!d || g.blocked) return false;                       // nothing known: carry on\n      const v = d.version;\n      const base = baseline();\n      if (!knownVersion(base)) { g.baseline = v; g.baseContract = d.contract; return false; }   // the page did not know itself: this is what it is\n      if (!isNewerDeploy(base, v)) return false;\n      g.deployed = v;\n      // ONLY A NEW CONTRACT BLOCKS (s8): against the page's own, or, for a page that took its baseline from a fetch, that one's.\n      const lc = loadedContract();\n      const mine = knownVersion(lc) ? String(lc).trim() : g.baseContract;\n      if (!contractChanged(mine, d.contract)) { soft(v); return true; }\n      await block(v);\n      return true;\n    } finally { g.checking = false; }\n  }\n\n  // INPUT STOPS HERE, in the capture phase of the window, which runs before any of the game's own listeners. Registered at\n  // install (the first capture listeners on the window, since app.js installs the gate early in boot), and idle until\n  // the gate is blocked. What is inside the overlay goes through, Tab stays on the one button, Escape is eaten.\n  const swallow = (e) => {\n    if (!g.blocked) return;\n    const inside = g.overlay && e.target && typeof g.overlay.contains === 'function' && g.overlay.contains(e.target);\n    if (e.type === 'keydown' || e.type === 'keyup' || e.type === 'keypress') {\n      const k = e.key;\n      if (k === 'Escape' || k === 'Esc') { stop(e); return; }\n      if (k === 'Tab') { stop(e); focusButton(); return; }\n      if ((k === 'Enter' || k === ' ' || k === 'Spacebar') && e.type === 'keydown') {\n        stop(e); if (g.ready) refresh(); return;                // Enter presses Refresh wherever the focus has got to\n      }\n      if (!inside) stop(e);\n      return;\n    }\n    // A press on the overlay's backdrop or card would take the focus off Refresh (Enter still works, through the\n    // keydown above, but the ring that says so would go): the press is let through, its focus change is not.\n    if ((e.type === 'mousedown' || e.type === 'pointerdown') && inside && e.target !== g.button) { try { e.preventDefault(); } catch (x) {} return; }\n    if (!inside) stop(e);\n  };\n  const stop = (e) => { try { e.preventDefault(); } catch (x) {} try { e.stopImmediatePropagation(); } catch (x) {} try { e.stopPropagation(); } catch (x) {} };\n  const INPUT = ['keydown', 'keyup', 'keypress', 'pointerdown', 'pointerup', 'pointermove', 'mousedown', 'mouseup', 'click', 'dblclick',\n                 'contextmenu', 'wheel', 'touchstart', 'touchmove', 'touchend'];\n  for (const t of INPUT) win.addEventListener(t, swallow, { capture: true, passive: false });\n\n  function focusButton() { try { g.button && g.button.focus({ preventScroll: true }); } catch (e) { try { g.button.focus(); } catch (x) {} } }\n\n  function refresh() {\n    if (!g.ready) return;\n    try { reload(); } catch (e) {}\n  }\n\n  function showOverlay() {\n    if (!doc.getElementById('update-gate-style')) {\n      const st = doc.createElement('style'); st.id = 'update-gate-style'; st.textContent = CSS;\n      (doc.head || doc.body).appendChild(st);\n    }\n    const o = doc.createElement('div');\n    o.id = 'update-gate';\n    o.setAttribute('role', 'alertdialog');\n    o.setAttribute('aria-modal', 'true');\n    o.setAttribute('aria-labelledby', 'update-gate-title');\n    o.setAttribute('aria-describedby', 'update-gate-text');\n    const card = doc.createElement('div'); card.className = 'ug-card';\n    // u8: say it is safe. While the save goes out the line says so, and once it is in, that it is.\n    const h = doc.createElement('h2'); h.id = 'update-gate-title'; h.textContent = 'A new version is ready';\n    const p = doc.createElement('p'); p.id = 'update-gate-text'; p.textContent = 'Saving your progress…';\n    g.text = p;\n    const b = doc.createElement('button'); b.type = 'button'; b.id = 'update-gate-refresh';\n    b.textContent = 'Saving…'; b.disabled = true; b.setAttribute('aria-busy', 'true');\n    b.addEventListener('click', (e) => { stop(e); refresh(); });\n    card.appendChild(h); card.appendChild(p); card.appendChild(b); o.appendChild(card);\n    // EVERYTHING ELSE GOES INERT: no focus, no clicks, nothing read out behind the dialog. Taken from the page's children\n    // as they are now; a panel appended later is under the overlay and behind the capture listener anyway.\n    const kids = Array.from(doc.body.children || []);\n    for (const el of kids) { if (el !== o && el.id !== 'update-gate-style') try { el.inert = true; el.setAttribute('aria-hidden', 'true'); } catch (e) {} }\n    doc.body.appendChild(o);\n    g.overlay = o; g.button = b;\n    try { b.focus(); } catch (e) {}\n  }\n\n  async function block(version) {\n    if (g.blocked) return;\n    g.blocked = true;\n    if (version) g.deployed = version;\n    if (g.timer != null) { try { clearIntervalFn(g.timer); } catch (e) {} g.timer = null; }\n    try { if (doc.pointerLockElement && doc.exitPointerLock) doc.exitPointerLock(); } catch (e) {}\n    try { const a = doc.activeElement; if (a && a !== doc.body && typeof a.blur === 'function') a.blur(); } catch (e) {}\n    try { freeze(); } catch (e) { log && log.warn && log.warn('update gate: freeze failed', e); }\n    try { showOverlay(); } catch (e) { log && log.warn && log.warn('update gate: overlay failed', e); }\n    if (log && log.info) log.info('🔄 update gate: version ' + (g.deployed || '?') + ' is deployed (this page is ' + (baseline() || '?') + ') — saving, then blocking');\n    // SAVE FIRST, capped twice over (bdSettle's own cap and this race), so the button is pressable within flushMs.\n    let t = null;\n    try {\n      await Promise.race([\n        Promise.resolve().then(() => flush(flushMs)).catch(() => {}),\n        new Promise((resolve) => { t = setTimeoutFn(resolve, flushMs + 250); }),\n      ]);\n    } finally { if (t != null) try { clearTimeoutFn(t); } catch (e) {} }\n    g.flushed = true;\n    try { disconnect(); } catch (e) { log && log.warn && log.warn('update gate: disconnect failed', e); }\n    g.ready = true;\n    hideNotice();\n    if (g.text) g.text.textContent = UPDATE_SAVED_HARD;\n    if (g.button) {\n      g.button.textContent = 'Refresh now'; g.button.disabled = false; g.button.removeAttribute('aria-busy');\n      focusButton();\n    }\n  }\n\n  // ── ONLY THE PAGE CHANGED (s8, u8): a notice, no block, and the new build at the next natural moment ──────────────────\n  function hideNotice() { try { if (g.notice && g.notice.parentNode && g.notice.parentNode.removeChild) g.notice.parentNode.removeChild(g.notice); } catch (e) {} g.notice = null; }\n  function soft(version) {\n    if (g.blocked) return;\n    g.pending = version || g.pending || 'pending';\n    if (g.said === g.pending) return;            // said once per version: After this means after this\n    g.said = g.pending;\n    hideNotice();\n    try {\n      if (!doc.getElementById('update-gate-style')) {\n        const st = doc.createElement('style'); st.id = 'update-gate-style'; st.textContent = CSS;\n        (doc.head || doc.body).appendChild(st);\n      }\n      const n = doc.createElement('div'); n.id = 'update-notice';\n      n.setAttribute('role', 'status'); n.setAttribute('aria-live', 'polite');\n      const p = doc.createElement('p'); p.id = 'update-notice-text'; p.textContent = UPDATE_SAVED_SOFT;\n      const row = doc.createElement('div'); row.className = 'un-row';\n      const later = doc.createElement('button'); later.type = 'button'; later.id = 'update-notice-later'; later.textContent = 'After this';\n      later.title = 'Keep playing: the new version loads at your next landing, dive or menu, right where you are';\n      const now = doc.createElement('button'); now.type = 'button'; now.id = 'update-notice-now'; now.className = 'un-now'; now.textContent = 'Refresh now';\n      later.addEventListener('click', (e) => { stop(e); hideNotice(); });\n      now.addEventListener('click', (e) => { stop(e); now.disabled = true; later.disabled = true; now.textContent = 'Saving…'; loadNew('now'); });\n      row.appendChild(later); row.appendChild(now); n.appendChild(p); n.appendChild(row);\n      doc.body.appendChild(n);\n      g.notice = n;\n    } catch (e) { log && log.warn && log.warn('update gate: notice failed', e); }\n    if (log && log.info) log.info('🔄 update gate: version ' + g.pending + ' is deployed, same server contract — loading it at the next natural moment');\n  }\n  // SAVE, THEN LOAD THE NEW BUILD, IN PLACE. Once; and never over a block, which has its own Refresh.\n  async function loadNew(kind) {\n    if (g.reloading || g.blocked) return false;\n    g.reloading = true;\n    let t = null;\n    try {\n      await Promise.race([\n        Promise.resolve().then(() => flush(flushMs)).catch(() => {}),\n        new Promise((resolve) => { t = setTimeoutFn(resolve, flushMs + 250); }),\n      ]);\n    } finally { if (t != null) try { clearTimeoutFn(t); } catch (e) {} }\n    if (g.blocked) return false;\n    if (log && log.info) log.info('🔄 update gate: loading version ' + g.pending + ' (' + kind + ')');\n    try { reload(); } catch (e) {}\n    return true;\n  }\n  // A NATURAL MOMENT, as the page reports it: if a new build is waiting, it is loaded now.\n  async function transition(kind) {\n    if (!g.pending || g.blocked || g.reloading) return false;\n    return loadNew(kind || 'transition');\n  }\n\n  // THE TRIGGERS. A visible tab and a focused window ask at once; going online asks too; the interval is the floor.\n  const soon = () => { if (!g.blocked) check().catch(() => {}); };\n  win.addEventListener('focus', soon);\n  win.addEventListener('online', soon);\n  doc.addEventListener('visibilitychange', () => {\n    if (doc.visibilityState !== 'visible') { g.hiddenAt = nowFn(); return; }\n    // …and coming back after a while away is a natural moment for a waiting build (s8)\n    if (g.pending && g.hiddenAt && nowFn() - g.hiddenAt >= returnMs) { g.hiddenAt = 0; transition('return'); return; }\n    g.hiddenAt = 0; soon();\n  });\n  g.timer = setIntervalFn(soon, pollMs);\n  g.installed = true;\n  return {\n    get installed() { return g.installed; }, get blocked() { return g.blocked; }, get ready() { return g.ready; },\n    get flushed() { return g.flushed; }, get deployed() { return g.deployed; }, get baseline() { return baseline(); },\n    get checks() { return g.checks; }, get pending() { return g.pending; }, get reloading() { return g.reloading; },\n    check: () => check(),\n    block: (v) => block(v),\n    refresh: () => refresh(),\n    soft: (v) => soft(v),\n    transition: (kind) => transition(kind),\n  };\n}\n"],"mappings":"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","names":[],"file":"update-gate.mjs"}