mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
107 lines
3.5 KiB
JavaScript
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);
|
|
});
|