Files
bitvid/tests/feed-list-cache-gate.test.mjs
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

107 lines
3.5 KiB
JavaScript

// Cold-load "first impression" fix: the post-login feed gate can open as soon as
// blocks/subscriptions/hashtags hold valid per-pubkey CACHES (warm boot), instead
// of waiting for the full relay sync + decryption. These test the probe + waiter
// that implement the early release.
//
// test_integrity_note:
// change_type: ["new_tests"]
// scenarios:
// - id: SCN-feed-list-cache-gate
// given: "list services with/without valid per-pubkey caches"
// when: "createListCacheProbe / waitForListCaches run"
// then: "probe is true only when ALL THREE caches match the active pubkey; the waiter resolves 'cache' early or null at timeout"
// observable_outcomes:
// - "all three caches valid + matching pubkey -> probe true"
// - "any cache missing / stale pubkey / not loaded -> probe false"
// - "waiter resolves 'cache' once the probe flips true (caches hydrating late)"
// - "waiter resolves null at timeout when caches never hydrate (cold boot)"
// determinism_controls:
// - "plain state objects; short real timers (interval 5ms, timeout 40ms)"
// anti_cheat_rationale:
// prevents: ["hard-coded return value", "over-mocking internal logic"]
// relaxation:
// did_relax_any_assertion: false
import test from "node:test";
import { strict as assert } from "node:assert";
import {
createListCacheProbe,
waitForListCaches,
} from "../js/app/feedListCacheGate.js";
const PUB = "A".repeat(64); // exercise normalization (upper → lower)
function services({ blocks = true, subs = true, tags = true, pubkey = PUB } = {}) {
return {
userBlocks: { loaded: blocks, activePubkey: pubkey },
subscriptions: { loaded: subs, currentUserPubkey: pubkey },
hashtagPreferences: { loaded: tags, activePubkey: pubkey },
};
}
const normalize = (v) => (typeof v === "string" ? v.trim().toLowerCase() : "");
test("probe is true only when ALL three caches are loaded for the active pubkey", () => {
const ok = createListCacheProbe({
...services(),
normalizeHexPubkey: normalize,
activePubkey: PUB.toLowerCase(),
});
assert.equal(ok(), true);
for (const partial of [
services({ blocks: false }),
services({ subs: false }),
services({ tags: false }),
services({ pubkey: "b".repeat(64) }), // caches belong to another account
]) {
const probe = createListCacheProbe({
...partial,
normalizeHexPubkey: normalize,
activePubkey: PUB.toLowerCase(),
});
assert.equal(probe(), false);
}
});
test("probe is false with no active pubkey or missing services", () => {
assert.equal(
createListCacheProbe({ ...services(), normalizeHexPubkey: normalize, activePubkey: "" })(),
false,
);
assert.equal(
createListCacheProbe({ normalizeHexPubkey: normalize, activePubkey: PUB })(),
false,
);
});
test("waiter resolves 'cache' once caches hydrate (late flip)", async () => {
const state = { ready: false };
setTimeout(() => {
state.ready = true;
}, 15);
const result = await waitForListCaches(() => state.ready, {
timeoutMs: 500,
intervalMs: 5,
});
assert.equal(result, "cache");
});
test("waiter resolves null at timeout when caches never hydrate (cold boot)", async () => {
const result = await waitForListCaches(() => false, {
timeoutMs: 40,
intervalMs: 5,
});
assert.equal(result, null);
});
test("a throwing probe is treated as not-ready (never breaks the gate)", async () => {
const result = await waitForListCaches(
() => {
throw new Error("boom");
},
{ timeoutMs: 30, intervalMs: 5 },
);
assert.equal(result, null);
});