Fix: feed rerender silently dies when autosave restores myExtendedFollows as {}

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-05-28 22:21:00 -04:00
1 parent 95a7130255
commit 1bb4471cc8
1 file changed
+28 -2
+28 -2
View File
@@ -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 = '<div class="hint" style="grid-column:1/-1">Loading…</div>';
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();
}