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"
// 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() {