mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
1 parent
0c8a17e3cf
commit
122d806ad2
3 files changed
+87
-9
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user