perf(feed): don't block fresh load on remote admin-list fetch

loadVideos awaited ensureAccessControlReady -> waitForReady -> _performRefresh
-> loadAdminState, which awaits a remote admin-list fetch. On a cold load that
waits for relays to connect (~5s) plus the fetch, stalling the feed ~10s before
any video renders. The cached admin state IS hydrated synchronously at
construction, but with markLoaded:false, so waitForReady blocked on a full
remote refresh even when a usable cache existed ("refresh fixes it").

ensureAccessControlReady now blocks only on a first-ever cold load WITH whitelist
mode (where showing non-whitelisted content before the whitelist arrives would be
wrong). In every other case — admin state hydrated from the localStorage cache,
already loaded, or blacklist-only mode — it kicks the refresh off in the
background (ensureReady) and renders immediately; the reactive
onWhitelist/onBlacklistChange handlers re-filter the grid once fresh lists land.

Adds accessControl.isHydrated() and a branching test
(nostr-service-access-control-ready.test.mjs).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-12 14:31:38 -04:00
1 parent 0c8a17e3cf
commit 122d806ad2
3 files changed
+87 -9

No files matched your search

+8
View File
@@ -460,6 +460,14 @@ class AccessControl {
return this.whitelistEnabled;
}
// True once we have usable lists to filter with — either a completed refresh
// or admin state hydrated synchronously from the localStorage cache at
// construction. Callers use this to avoid blocking the feed on a remote
// refresh when cached lists already allow accurate canAccess() checks.
isHydrated() {
return this.hasLoaded === true || this._hydratedFromCache === true;
}
isSuperAdmin(npub) {
const normalized = normalizeNpub(npub);
return normalized ? normalized === ADMIN_SUPER_NPUB : false;
+9 -9
View File
@@ -1556,17 +1556,17 @@ export class NostrService {
}
async ensureAccessControlReady() {
if (!this.accessControl || typeof this.accessControl.waitForReady !== "function") {
return;
}
const ac = this.accessControl;
if (!ac) return;
// Block on the remote admin fetch only on a first-ever cold whitelist load;
// else the hydrated cache is accurate, so refresh in bg (handlers re-filter).
const hydrated = ac.isHydrated?.() ?? ac.hasLoaded === true;
const block = !hydrated && (ac.whitelistMode?.() ?? false);
const ready = block ? ac.waitForReady : ac.ensureReady || ac.waitForReady;
try {
await this.accessControl.waitForReady();
if (typeof ready === "function") await ready.call(ac);
} catch (error) {
userLogger.warn(
"[nostrService] Failed to ensure access control lists are ready:",
error
);
userLogger.warn("[nostrService] access control ready failed:", error);
}
}
@@ -0,0 +1,70 @@
// Verifies ensureAccessControlReady no longer blocks the feed on the remote
// admin-list fetch except on a first-ever cold load in whitelist mode.
//
// Scenario (SCN-access-control-non-blocking-feed):
// - admin state hydrated (cache or loaded) => background refresh (ensureReady)
// - cold + whitelist mode => block (waitForReady)
// - cold + blacklist-only mode => background refresh (ensureReady)
import assert from "node:assert/strict";
import test from "node:test";
import "./test-helpers/setup-localstorage.mjs";
import moderationService from "../js/services/moderationService.js";
import { NostrService } from "../js/services/nostrService.js";
// Stub background timers the service may schedule.
if (moderationService) {
moderationService.scheduleTrustedMuteSubscriptionRefresh = () => {};
moderationService.refreshTrustedMuteSubscriptions = async () => {};
}
if (typeof globalThis.window === "undefined") globalThis.window = {};
function makeAccessControl({ hydrated, whitelist }) {
const calls = { ensureReady: 0, waitForReady: 0 };
return {
calls,
isHydrated: () => hydrated,
whitelistMode: () => whitelist,
ensureReady: async () => {
calls.ensureReady += 1;
},
waitForReady: async () => {
calls.waitForReady += 1;
},
};
}
test("hydrated => background refresh, never blocks on waitForReady", async () => {
const service = new NostrService();
const ac = makeAccessControl({ hydrated: true, whitelist: true });
service.accessControl = ac;
await service.ensureAccessControlReady();
assert.equal(ac.calls.ensureReady, 1);
assert.equal(ac.calls.waitForReady, 0);
});
test("cold + whitelist mode => blocks on waitForReady (correctness)", async () => {
const service = new NostrService();
const ac = makeAccessControl({ hydrated: false, whitelist: true });
service.accessControl = ac;
await service.ensureAccessControlReady();
assert.equal(ac.calls.waitForReady, 1);
assert.equal(ac.calls.ensureReady, 0);
});
test("cold + blacklist-only mode => background refresh (no block)", async () => {
const service = new NostrService();
const ac = makeAccessControl({ hydrated: false, whitelist: false });
service.accessControl = ac;
await service.ensureAccessControlReady();
assert.equal(ac.calls.ensureReady, 1);
assert.equal(ac.calls.waitForReady, 0);
});
test("missing access control is a no-op", async () => {
const service = new NostrService();
service.accessControl = null;
await service.ensureAccessControlReady(); // must not throw
assert.ok(true);
});