diff --git a/main.js b/main.js index 9632e433..f897bac8 100644 --- a/main.js +++ b/main.js @@ -618,21 +618,27 @@ async function checkForUpdate() { // newer release on the mirror shows the same one-click "Install & restart" // chip as a real install, and that installer targets the REAL install dir. // 2026-09-15 a test run reinstalled the user's Theseus that way. - if (process.env.THESEUS_NO_UPDATE_CHECK) return; + // Returns true when the manifest was read (whatever it said), false when + // the check itself failed — the startup scheduler retries on false. + if (process.env.THESEUS_NO_UPDATE_CHECK) return true; + const t0 = Date.now(); try { - const controller = new AbortController(); - const to = setTimeout(() => controller.abort(), 5000); - const r = await fetch(UPDATE_MANIFEST_URL, { signal: controller.signal, cache: "no-store" }); - clearTimeout(to); - if (!r.ok) return; + // 20 s, not 5: the startup check shares the main thread with snapshot + // parsing, tab restore and add-on activation. Measured 2026-10-03: 3.2 s + // for this fetch on an empty profile, 1.6 s of it the event loop being + // busy — a real profile went past 5 s, the abort won, and (the failure + // being silent with no retry) the update only appeared after a manual + // "Check for updates". + const r = await fetch(UPDATE_MANIFEST_URL, { signal: AbortSignal.timeout(20000), cache: "no-store" }); + if (!r.ok) { console.warn(`[update] manifest HTTP ${r.status}`); return false; } const manifest = await r.json(); const rel = (manifest.releases || []).find((x) => x.id === "theseus-navigator"); - if (!rel || !rel.version) return; + if (!rel || !rel.version) return true; if (!versionIsNewer(rel.version, app.getVersion())) { // Same version or older — nothing to offer. Clear any stale state so the // chip disappears after the user has updated + relaunched. if (updateAvailable) { updateAvailable = null; updateDownloadState = "idle"; updateDownloadPath = null; emitUpdateAvailable(); } - return; + return true; } const files = rel.files || {}; const setupFile = Object.keys(files).find((k) => /Setup/i.test(k)); @@ -647,15 +653,31 @@ async function checkForUpdate() { }; const versionChanged = !updateAvailable || updateAvailable.version !== nextAvailable.version; updateAvailable = nextAvailable; - if (versionChanged) { - // New candidate — reset any prior download state and kick off a fresh - // silent background fetch so the chip lands as "ready to install". + if (versionChanged || updateDownloadState === "failed") { + // New candidate — or the same one whose download failed (truncated, + // hash mismatch, network drop), which used to stay failed until the + // next release. Reset and kick off a fresh silent background fetch so + // the chip lands as "ready to install". updateDownloadState = "idle"; updateDownloadPath = null; autoDownloadUpdate(); } emitUpdateAvailable(); - } catch { /* offline / manifest unreachable — silent */ } + return true; + } catch (e) { + console.warn(`[update] check failed after ${Date.now() - t0} ms: ${e?.name || ""} ${e?.cause?.code || e?.message || e}`); + return false; + } +} +// Startup check: wait for the boot rush to pass, then retry with backoff on +// failure — a flaky network at launch must not mean "no update until the +// 6-hourly recheck". +const UPDATE_STARTUP_DELAYS_MS = [8_000, 30_000, 2 * 60_000, 10 * 60_000, 30 * 60_000]; +function scheduleStartupUpdateCheck(attempt = 0) { + if (attempt >= UPDATE_STARTUP_DELAYS_MS.length) return; + setTimeout(() => { + checkForUpdate().then((ok) => { if (!ok) scheduleStartupUpdateCheck(attempt + 1); }, () => scheduleStartupUpdateCheck(attempt + 1)); + }, UPDATE_STARTUP_DELAYS_MS[attempt]); } // Silent background pre-download of the update installer. The user never // has to click Download — clicking the chip goes straight to Install. @@ -1330,7 +1352,7 @@ function onChromeReady() { emitUpdateAvailable(); setTimeout(loadOverlays, 250); refreshSnapshotFromSia().catch(() => {}); - checkForUpdate().catch(() => {}); + scheduleStartupUpdateCheck(); refreshRemoteHomeCards().catch(() => {}); } function applyThrottle() {