perf(update-check): throttle + single-flight the build check (batch81)

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 16:14:50 +05:30
parent c1f01e796a
commit 667b4c69d5
+20 -7
View File
@@ -1002,7 +1002,7 @@
<body>
<script src="/icons.js?v=6"></script>
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
<script>window.__BUILD='2026-07-14-batch80';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
<script>window.__BUILD='2026-07-14-batch81';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
// Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load
// (emojis stay as plain Unicode). (#5)
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
@@ -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.