From 1bb4471cc8ad6a287ecff945c8da4e88b78a29a3 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 28 May 2026 22:21:00 -0400 Subject: [PATCH] Fix: feed rerender silently dies when autosave restores myExtendedFollows as {} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adam reported the "All" tab in the template browser was empty even after the fingerprint dedup fix. Root cause: JSON cannot represent a Set, so when autosave serialises state and the user reloads, state.community.myExtendedFollows comes back as an empty object {}. buildRowsFromEvents calls trustTierForPubkey on every event, which calls state.community.myExtendedFollows?.has(...) — the ?. chain DOES NOT short-circuit on a non-null object that just doesn't have a `.has` method, so it throws TypeError. Inside the row builder loop the throw bubbles up through dedupByFingerprint and the rerender exits before any cards are drawn, leaving the wall empty with no console error the user would notice. Compounded by my recent additions writing autosave more often (after login, hydrate, etc.), which made the bad shape much more likely to be present on subsequent loads. Three defensive fixes (any one would have sufficed; all three are cheap): 1. restoreAutosave() re-initialises state.community.myExtendedFollows to a real Set after the autosave merge if the restored value isn't a Set. Same for myFollows → []. fetchHop2Follows refills myExtendedFollows on next login. 2. trustTierForPubkey() now checks `ext instanceof Set` before calling .has, and guards myFollows behind Array.isArray. Same idea — refuse to throw on a shape that wasn't a Set / Array. 3. The feed-tab rerender wrapper try/catches the inner render and console.warns instead of bubbling. Without this guard, a single throw anywhere in the row builder / dedup / sort / diff path silently kills the whole feed and the user has no way to see what went wrong. Tested via CDP: stuff autosave with state.community.myExtendedFollows: {}, reload, confirm state.community.myExtendedFollows is back to a Set and buildRowsFromEvents on a synthetic event succeeds (trust returned as 'unknown' since the test pubkey isn't followed). Pre- fix this test threw the TypeError shown above. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/index.html b/src/index.html index a1db5a7..ca677a8 100644 --- a/src/index.html +++ b/src/index.html @@ -18517,8 +18517,14 @@ const myPubkey = normalizePubkey(state.community.myPubkey); if (!myPubkey) return 'unknown'; if (normalized === myPubkey) return 'self'; - if (state.community.myFollows.includes(normalized)) return 'trusted'; - if (state.community.myExtendedFollows?.has(normalized)) return 'extended'; + const follows = Array.isArray(state.community.myFollows) ? state.community.myFollows : []; + if (follows.includes(normalized)) return 'trusted'; + // myExtendedFollows is a Set in memory but autosave serialises it as + // `{}`. Guard against the post-restore shape — otherwise this would + // throw inside the feed-row builder on every event and silently kill + // the whole rerender. + const ext = state.community.myExtendedFollows; + if (ext instanceof Set && ext.has(normalized)) return 'extended'; return 'unknown'; } @@ -20784,6 +20790,16 @@ wall.innerHTML = '
Loading…
'; const rerender = () => { + try { return rerenderInner(); } + catch (err) { + // Without this guard, a single throw inside the row builder / + // dedup / sort / diff path silently kills the whole feed — + // leaving the user staring at an empty wall with no clue + // anything went wrong. + console.warn('[feed] rerender threw — leaving wall as-is:', err); + } + }; + const rerenderInner = () => { const wallEl = $(`feedWall${tab}`); if (!wallEl) return; const typeFilter = _feedState[`filter${tab}`] || 'any'; @@ -24905,6 +24921,16 @@ }); normalizeDiscState(); normalizeJewelState(); + // JSON can't represent a Set — autosave round-trips Sets as `{}`. + // Re-init the community Sets so feed code (trustTierForPubkey, + // _blockedAuthors etc) gets the right shape after restore. + // fetchHop2Follows refills myExtendedFollows on next login. + if (!(state.community.myExtendedFollows instanceof Set)) { + state.community.myExtendedFollows = new Set(); + } + if (!Array.isArray(state.community.myFollows)) { + state.community.myFollows = []; + } if ((parsed.appMode || parsed.ui?.appMode) === 'designer') { loadCurrentDiscDesignLayers(); }