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:
parent
9b2d6322be
commit
f2ff48b977
1 changed files with 35 additions and 13 deletions
48
main.js
48
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() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue