Files
bitvid/js/services/trustBootstrap.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

204 lines
5.5 KiB
JavaScript

// js/services/trustBootstrap.js
import {
DEFAULT_TRUST_SEED_NPUBS,
FEATURE_TRUST_SEEDS,
} from "../constants.js";
import { ADMIN_SUPER_NPUB } from "../config.js";
import { accessControl } from "../accessControl.js";
import moderationService from "./moderationService.js";
import nostrService from "./nostrService.js";
import { devLogger, userLogger } from "../utils/logger.js";
const TRUST_SEED_READY_TIMEOUT_MS = 3500;
function normalizeNpub(value) {
return typeof value === "string" ? value.trim() : "";
}
function buildTrustedSeeds({ superAdmin, editors, fallbackSeeds }) {
const seeds = new Set();
const addSeed = (value) => {
const normalized = normalizeNpub(value);
if (normalized) {
seeds.add(normalized);
}
};
addSeed(superAdmin);
if (editors && typeof editors[Symbol.iterator] === "function") {
for (const value of editors) {
addSeed(value);
}
}
if (!seeds.size && fallbackSeeds && typeof fallbackSeeds[Symbol.iterator] === "function") {
for (const value of fallbackSeeds) {
addSeed(value);
}
}
return seeds;
}
function delay(ms = 0) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForRelaysReady({ timeoutMs = TRUST_SEED_READY_TIMEOUT_MS } = {}) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (
nostrService?.nostrClient?.pool &&
nostrService?.nostrClient?.relays &&
Array.isArray(nostrService.nostrClient.relays) &&
nostrService.nostrClient.relays.length > 0
) {
return true;
}
await delay(125);
}
return false;
}
async function waitForAccessControl({ timeoutMs = TRUST_SEED_READY_TIMEOUT_MS } = {}) {
if (!accessControl || typeof accessControl.waitForReady !== "function") {
return { ok: true, timedOut: false };
}
// Cached admin lists (hydrated synchronously from localStorage at
// construction) are enough to seed trust — don't stall boot behind the relay
// refresh. If the refresh later changes editors/whitelist/blacklist, the
// change listeners registered in bootstrapTrustedSeeds re-apply the seeds.
if (
typeof accessControl.isHydrated === "function" &&
accessControl.isHydrated()
) {
return { ok: true, timedOut: false, fromCache: true };
}
let timedOut = false;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
timedOut = true;
reject(new Error("accessControl ready check timed out"));
}, timeoutMs);
});
try {
await Promise.race([accessControl.waitForReady(), timeoutPromise]);
return { ok: true, timedOut: false };
} catch (error) {
if (timedOut) {
devLogger.warn(
"[bootstrap] access control hydration timed out; falling back to default trust seeds.",
error,
);
return { ok: true, timedOut: true };
}
throw error;
}
}
export async function bootstrapTrustedSeeds() {
devLogger.info("[trustBootstrap] bootstrapTrustedSeeds called");
if (
!(
FEATURE_TRUST_SEEDS &&
moderationService &&
typeof moderationService.setTrustedSeeds === "function"
)
) {
devLogger.warn("[trustBootstrap] Trust seeds feature disabled or service missing");
return;
}
const applySeeds = () => {
try {
const editors =
accessControl && typeof accessControl.getEditors === "function"
? accessControl.getEditors()
: [];
const seeds = buildTrustedSeeds({
superAdmin: ADMIN_SUPER_NPUB,
editors,
fallbackSeeds: DEFAULT_TRUST_SEED_NPUBS,
});
devLogger.info(`[trustBootstrap] Applying ${seeds.size} trusted seeds`, Array.from(seeds));
moderationService.setTrustedSeeds(seeds);
if (typeof moderationService.updateTrustedSeedOnlyStatus === "function") {
moderationService.updateTrustedSeedOnlyStatus();
}
if (
moderationService &&
typeof moderationService.recomputeAllSummaries === "function"
) {
moderationService.recomputeAllSummaries();
}
} catch (error) {
userLogger.warn("[bootstrap] Failed to apply trusted seeds", error);
}
};
let attempts = 0;
const hydrate = async () => {
attempts += 1;
try {
const result = await waitForAccessControl();
devLogger.info("[trustBootstrap] waitForAccessControl result", result);
applySeeds();
return result;
} catch (error) {
userLogger.warn(
"[bootstrap] Failed to hydrate admin lists for trusted seeds",
error,
);
return { ok: false, timedOut: false, error };
}
};
const hydrateResult = await hydrate();
const runAsyncRetry = async () => {
if (hydrateResult?.ok || attempts >= 2) {
return;
}
const relaysReady = await waitForRelaysReady();
if (relaysReady) {
await hydrate();
} else {
devLogger.warn(
"[bootstrap] Skipping trusted seed retry because relays were not ready in time.",
);
applySeeds();
}
};
runAsyncRetry().catch((error) => {
userLogger.warn("[bootstrap] Trusted seed retry failed", error);
});
if (!hydrateResult?.ok) {
applySeeds();
}
const applyOnChange = () => {
applySeeds();
};
if (accessControl && typeof accessControl.onWhitelistChange === "function") {
accessControl.onWhitelistChange(applyOnChange);
}
if (accessControl && typeof accessControl.onEditorsChange === "function") {
accessControl.onEditorsChange(applyOnChange);
}
if (accessControl && typeof accessControl.onBlacklistChange === "function") {
accessControl.onBlacklistChange(applyOnChange);
}
}