Theseus: the startup update check no longer silently misses new releases

The startup check shared the main thread with snapshot parsing, tab restore
and add-on activation, under a 5 s abort timer started before the request.
Measured on an empty profile: 3.2 s for the manifest fetch, 1.6 s of it the
event loop being busy; a real profile went past 5 s, the abort won, the
failure was swallowed and nothing retried before the 6-hourly recheck. The
update only appeared after a manual "Check for updates".

- the startup check runs 8 s after the toolbar is ready and retries with
  backoff (30 s, 2 min, 10 min, 30 min) when it fails
- 20 s timeout via AbortSignal.timeout
- a failed installer download is retried on the next check instead of
  staying failed until the next release
- failures are logged
This commit is contained in:
Local Dev 2026-10-03 10:57:30 +02:00
parent 9b2d6322be
commit f2ff48b977

48
main.js
View file

@ -618,21 +618,27 @@ async function checkForUpdate() {
// newer release on the mirror shows the same one-click "Install & restart" // 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. // 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. // 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 { try {
const controller = new AbortController(); // 20 s, not 5: the startup check shares the main thread with snapshot
const to = setTimeout(() => controller.abort(), 5000); // parsing, tab restore and add-on activation. Measured 2026-10-03: 3.2 s
const r = await fetch(UPDATE_MANIFEST_URL, { signal: controller.signal, cache: "no-store" }); // for this fetch on an empty profile, 1.6 s of it the event loop being
clearTimeout(to); // busy — a real profile went past 5 s, the abort won, and (the failure
if (!r.ok) return; // 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 manifest = await r.json();
const rel = (manifest.releases || []).find((x) => x.id === "theseus-navigator"); 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())) { if (!versionIsNewer(rel.version, app.getVersion())) {
// Same version or older — nothing to offer. Clear any stale state so the // Same version or older — nothing to offer. Clear any stale state so the
// chip disappears after the user has updated + relaunched. // chip disappears after the user has updated + relaunched.
if (updateAvailable) { updateAvailable = null; updateDownloadState = "idle"; updateDownloadPath = null; emitUpdateAvailable(); } if (updateAvailable) { updateAvailable = null; updateDownloadState = "idle"; updateDownloadPath = null; emitUpdateAvailable(); }
return; return true;
} }
const files = rel.files || {}; const files = rel.files || {};
const setupFile = Object.keys(files).find((k) => /Setup/i.test(k)); 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; const versionChanged = !updateAvailable || updateAvailable.version !== nextAvailable.version;
updateAvailable = nextAvailable; updateAvailable = nextAvailable;
if (versionChanged) { if (versionChanged || updateDownloadState === "failed") {
// New candidate — reset any prior download state and kick off a fresh // New candidate — or the same one whose download failed (truncated,
// silent background fetch so the chip lands as "ready to install". // 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"; updateDownloadState = "idle";
updateDownloadPath = null; updateDownloadPath = null;
autoDownloadUpdate(); autoDownloadUpdate();
} }
emitUpdateAvailable(); 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 // Silent background pre-download of the update installer. The user never
// has to click Download — clicking the chip goes straight to Install. // has to click Download — clicking the chip goes straight to Install.
@ -1330,7 +1352,7 @@ function onChromeReady() {
emitUpdateAvailable(); emitUpdateAvailable();
setTimeout(loadOverlays, 250); setTimeout(loadOverlays, 250);
refreshSnapshotFromSia().catch(() => {}); refreshSnapshotFromSia().catch(() => {});
checkForUpdate().catch(() => {}); scheduleStartupUpdateCheck();
refreshRemoteHomeCards().catch(() => {}); refreshRemoteHomeCards().catch(() => {});
} }
function applyThrottle() { function applyThrottle() {