experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(lex): drop periodic sync poll, drive sync from visibility events

Lex was running a 5-minute setInterval that ran whether or not the home
window was visible, generating spammy '[atproto] Token refresh failed' and
'[lex] Failed to check repo rev' errors when the user wasn't actively in
Lex. The home tile is resident: true, so the script keeps running after
the window is hidden.

Replace the periodic loop with visibility-driven syncs:

- syncPoll() now runs once on tile init (if authenticated), once on
re-authentication, and on visibilitychange / window focus events
(debounced to one sync per minute).
- Drop startSyncPoll / stopSyncPoll helpers and the syncIntervalId state.
- Drop the 30-second updateSyncIndicator setInterval — syncPoll already
updates the indicator after each successful sync, which is when its
value changes.

User-driven sync paths (panel switches, refresh button) call syncPoll
directly and bypass the visibility debounce.

+31 -29
+31 -29
features/lex/home.js
··· 277 277 if (authExpiredUiShown) return; 278 278 authExpiredUiShown = true; 279 279 280 - console.warn('[lex] Auth session expired — stopping sync poll, prompting re-authentication'); 281 - stopSyncPoll(); 280 + console.warn('[lex] Auth session expired — prompting re-authentication'); 282 281 renderAuthExpiredUi(); 283 282 } 284 283 ··· 382 381 clearAuthExpired(); 383 382 removeAuthExpiredUi(); 384 383 385 - // Resume polling and refresh the active panel's data. 386 - startSyncPoll(); 384 + // Trigger one sync now that auth is restored. Future syncs are 385 + // visibility-driven (no periodic loop). 386 + void syncPoll(); 387 387 console.log('[lex] Re-authenticated as', currentSession.handle); 388 388 } catch (err) { 389 389 console.error('[lex] Re-authentication failed:', err); ··· 526 526 527 527 const CACHE_PREFIX = 'lex:records:'; 528 528 const REPO_REV_KEY = 'lex:repoRev'; 529 - const SYNC_POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes 530 529 531 - let syncIntervalId = null; 530 + // Minimum gap between automatic syncs triggered by visibility / focus. 531 + // User-initiated actions (panel switches, refresh button) are unaffected. 532 + const SYNC_DEBOUNCE_MS = 60 * 1000; // 1 minute 533 + 532 534 let lastSyncTime = 0; 533 535 534 536 /** ··· 647 649 648 650 /** 649 651 * Check repo rev and re-sync stale collections. 650 - * Called on the periodic poll interval. 652 + * 653 + * Called on user-visible triggers only: tile init while authenticated, 654 + * window becoming visible/focused, re-authentication. There is no periodic 655 + * background timer — see the architecture note above SYNC_DEBOUNCE_MS. 651 656 */ 652 657 async function syncPoll() { 653 658 if (!currentSession) return; ··· 676 681 } 677 682 678 683 /** 679 - * Start the periodic sync poll. 684 + * Run a sync now if it's been at least SYNC_DEBOUNCE_MS since the last one. 685 + * Used for visibility / focus triggers; user-driven refresh paths bypass 686 + * this and call `syncPoll` directly. 680 687 */ 681 - function startSyncPoll() { 682 - if (syncIntervalId) return; 683 - syncIntervalId = setInterval(syncPoll, SYNC_POLL_INTERVAL_MS); 684 - console.log('[lex] Started sync poll (every 5 min)'); 688 + function maybeSyncOnVisibility() { 689 + if (!currentSession) return; 690 + if (Date.now() - lastSyncTime < SYNC_DEBOUNCE_MS) return; 691 + void syncPoll(); 685 692 } 686 693 687 - /** 688 - * Stop the periodic sync poll. 689 - */ 690 - function stopSyncPoll() { 691 - if (syncIntervalId) { 692 - clearInterval(syncIntervalId); 693 - syncIntervalId = null; 694 - } 694 + let visibilityListenersBound = false; 695 + function ensureVisibilityListeners() { 696 + if (visibilityListenersBound) return; 697 + visibilityListenersBound = true; 698 + document.addEventListener('visibilitychange', () => { 699 + if (document.visibilityState === 'visible') maybeSyncOnVisibility(); 700 + }); 701 + window.addEventListener('focus', maybeSyncOnVisibility); 695 702 } 696 703 697 704 /** ··· 955 962 api.escape.onEscape(handleEscape); 956 963 } 957 964 958 - // Start sync poll if authenticated 965 + // Sync once on init if authenticated. Subsequent syncs are driven by 966 + // visibility / focus events — no periodic loop. 967 + ensureVisibilityListeners(); 959 968 if (currentSession) { 960 - startSyncPoll(); 969 + void syncPoll(); 961 970 } 962 - 963 - // Update "last synced" text every 30s 964 - setInterval(updateSyncIndicator, 30000); 965 971 } 966 972 967 973 // ============================================================================ ··· 1079 1085 // Logout button — call clearSession directly 1080 1086 logoutBtn.addEventListener('click', async () => { 1081 1087 await clearSession(); 1082 - stopSyncPoll(); 1083 1088 state.authenticated = false; 1084 1089 state.handle = null; 1085 1090 state.did = null; ··· 1285 1290 1286 1291 // Fetch collections directly 1287 1292 fetchCollections(); 1288 - 1289 - // Start periodic sync 1290 - startSyncPoll(); 1291 1293 } 1292 1294 1293 1295 async function fetchCollections() {