mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +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]>
204 lines
5.5 KiB
JavaScript
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);
|
|
}
|
|
}
|