From 667b4c69d5406a26dc0d01c9a2722cb41651d19f Mon Sep 17 00:00:00 2001 From: sravan Date: Tue, 14 Jul 2026 16:14:50 +0530 Subject: [PATCH] perf(update-check): throttle + single-flight the build check (batch81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check itself is cheap (one ~35-byte response, served from a variable the server reads once at startup), but it was hooked to BOTH `focus` and `visibilitychange` with no throttle — and a single alt-tab back into the app fires both, so every refocus cost two redundant requests. Now: one check per minute at most, never overlapping itself, and it stops checking entirely once a new build is known (the retry loop takes over). Co-Authored-By: Claude Opus 4.8 --- server/public/home.html | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/server/public/home.html b/server/public/home.html index e8d484d..877264a 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -1002,7 +1002,7 @@ - @@ -2789,16 +2789,27 @@ function wireUpdateBanner(){ // We poll the server's build marker and, as soon as it's SAFE, silently reload onto the new code. Safe = // not in a call, no live screen session, nothing half-typed, no dialog open. If the user is busy we simply // keep waiting and apply it the moment they're free (or when the window is next hidden). -let _newBuild=null, _autoRefreshTimer=null; -async function checkWebBuild(){ +let _newBuild=null, _autoRefreshTimer=null, _lastBuildCheck=0, _buildChecking=false; +// Throttled + single-flight. `focus` and `visibilitychange` both fire on a single alt-tab back into the +// app, so an unthrottled check fired TWICE every time the window was refocused. One check per minute is +// plenty (the payload is ~35 bytes and the server answers from memory), and it never overlaps itself. +const BUILD_CHECK_MIN_MS=60*1000; +async function checkWebBuild(force){ + if(_newBuild) return; // already know: the retry loop owns it now + const now=Date.now(); + if(_buildChecking) return; + if(!force && now-_lastBuildCheck < BUILD_CHECK_MIN_MS) return; + _buildChecking=true; _lastBuildCheck=now; try{ const r=await fetch('/api/build',{cache:'no-store'}); if(!r.ok) return; const d=await r.json(); if(d && d.build && window.__BUILD && d.build!==window.__BUILD){ - if(_newBuild!==d.build){ _newBuild=d.build; console.log('[build] new version on server:', d.build, '(running', window.__BUILD+')'); } + _newBuild=d.build; + console.log('[build] new version on server:', d.build, '(running', window.__BUILD+')'); scheduleAutoRefresh(); } }catch(_){} + finally{ _buildChecking=false; } } // Never yank the page out from under someone mid-task. function safeToRefresh(){ @@ -2830,10 +2841,12 @@ async function hardReloadApp(){ try{ if(window.caches && caches.keys){ const ks=await caches.keys(); await Promise.all(ks.map(k=>caches.delete(k).catch(()=>{}))); } }catch(_){} try{ location.replace(location.pathname+location.search); }catch(_){ location.reload(); } } -setInterval(checkWebBuild, 5*60*1000); // every 5 min -window.addEventListener('focus', checkWebBuild); // and whenever the user comes back to the app +// Cheap by design: one ~35-byte request, answered from a variable the server read at startup. The +// throttle above means refocusing the app repeatedly costs nothing. +setInterval(()=>checkWebBuild(true), 5*60*1000); // every 5 min (the interval IS the schedule) +window.addEventListener('focus', ()=>checkWebBuild()); // coming back to the app (throttled) document.addEventListener('visibilitychange', ()=>{ if(!document.hidden) checkWebBuild(); }); -setTimeout(checkWebBuild, 3000); // shortly after boot +setTimeout(()=>checkWebBuild(true), 3000); // shortly after boot // Deliberately NO banner and NO toast: a web build is an implementation detail. Surfacing it would make // users think about "web build vs app version". It just updates. // Small 'i' badge on the profile button when an update is pending; clicking opens Settings.