Files
bitvid/js/app/feedListCacheGate.js
T
thePR0M3TH3ANandClaude Opus 4.8 b8e6921ac0 [perf] Stale-while-revalidate cold boot: don't serialize behind admin lists + list sync
Cold refresh stalled on "Fetching moderation filters…" and then (logged in)
"Fetching subscriptions…" before the grid could render. Boot was SERIAL behind
relay fetches even though every stage already had a local cache:

1. _initAccessControl awaited the full admin-list relay refresh (up to 15s)
   before auto-login — but accessControl hydrates whitelist/blacklist from
   localStorage at construction, and isHydrated() existed for exactly this case
   (unused). Now: when cached lists are present (every visit after the first),
   boot proceeds immediately and the refresh continues in the background — the
   existing whitelist/blacklist change listeners re-filter the grids when it
   lands. First-ever visits still wait (a whitelist-mode instance must never
   render unfiltered). The admin-pane refresh no longer blocks boot at all.

2. bootstrapTrustedSeeds awaited the same relay refresh (up to 3.5s more).
   waitForAccessControl now fast-paths on isHydrated(): cached editors seed
   trust immediately, and its onEditors/Whitelist/BlacklistChange listeners
   already re-apply seeds when the real refresh lands.

3. The post-login feed gate held the grid for the FULL list sync + NIP-07
   decryption (up to 8s). New js/app/feedListCacheGate.js: a probe that checks
   all three list services (blocks/subscriptions/hashtags) hold a valid cache
   for the active pubkey, raced into the gate — warm boots open as soon as the
   caches hydrate (first ticks) and the background sync re-filters on arrival.
   Cold boots (no caches) behave exactly as before.

Net effect: warm cold-refresh renders from IndexedDB-cached events with cached
filters/lists near-instantly; relay truth streams in behind it. No moderation
semantics changed — every stage that lacks a cache still waits.

Tests: tests/feed-list-cache-gate.test.mjs (5 — all-three-caches probe incl.
stale-pubkey/missing-service, late-hydration early release, cold-boot timeout,
throwing probe). Access-control + switch suites green. Lint + build green
(coordinator compacted to stay under its size cap).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 15:32:34 -04:00

80 lines
2.6 KiB
JavaScript

// Cache-aware early release for the post-login feed gate. The login flow holds
// the video grid until blocks/subscriptions/hashtag lists settle (so the feed
// never flashes unfiltered content), time-boxed at several seconds. But on every
// warm boot those services hydrate valid per-pubkey CACHES almost immediately —
// filtering with cached lists is accurate, and the relay sync keeps running in
// the background (its change events re-filter when fresh data lands). This probe
// lets the gate open as soon as all three caches are usable instead of waiting
// for full relay sync + NIP-07 decryption ("Fetching subscriptions…" stall).
// Builds a () => boolean probe: true when all three list services hold a valid
// cache for the active pubkey. Mirrors the hasValid*Cache checks used by the
// login list-sync bookkeeping.
export function createListCacheProbe({
userBlocks,
subscriptions,
hashtagPreferences,
normalizeHexPubkey,
activePubkey,
} = {}) {
const normalize =
typeof normalizeHexPubkey === "function"
? normalizeHexPubkey
: (value) => (typeof value === "string" ? value.trim().toLowerCase() : "");
const target = normalize(activePubkey);
return () => {
if (!target) {
return false;
}
const blocksReady =
Boolean(userBlocks) &&
userBlocks.loaded === true &&
normalize(userBlocks.activePubkey) === target;
const subscriptionsReady =
Boolean(subscriptions) &&
subscriptions.loaded === true &&
normalize(subscriptions.currentUserPubkey) === target;
const hashtagsReady =
Boolean(hashtagPreferences) &&
hashtagPreferences.loaded === true &&
normalize(hashtagPreferences.activePubkey) === target;
return blocksReady && subscriptionsReady && hashtagsReady;
};
}
// Resolves "cache" as soon as the probe passes, or null at the timeout. Polls on
// a short interval — the caches hydrate within the first ticks on warm boots.
export function waitForListCaches(
probe,
{ timeoutMs = 8000, intervalMs = 150 } = {},
) {
return new Promise((resolve) => {
if (typeof probe !== "function") {
resolve(null);
return;
}
const startedAt = Date.now();
const check = () => {
let ready = false;
try {
ready = probe() === true;
} catch (error) {
ready = false;
}
if (ready) {
resolve("cache");
return;
}
if (Date.now() - startedAt >= timeoutMs) {
resolve(null);
return;
}
setTimeout(check, intervalMs);
};
check();
});
}
export default waitForListCaches;