mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Ensure trusted seeds hydrate before app creation
This commit is contained in:
1 parent
192df53959
commit
9310929f0d
3 files changed
+147
-22
No files matched your search
Vendored
+98
-10
@@ -12,6 +12,9 @@ import nostrService from "./services/nostrService.js";
|
||||
import r2Service from "./services/r2Service.js";
|
||||
import { loadView } from "./viewManager.js";
|
||||
import hashtagPreferences from "./services/hashtagPreferencesService.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
|
||||
const TRUST_SEED_READY_TIMEOUT_MS = 3500;
|
||||
|
||||
function normalizeNpub(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
@@ -44,6 +47,53 @@ function buildTrustedSeeds({ superAdmin, editors, fallbackSeeds }) {
|
||||
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?.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.ensureReady !== "function") {
|
||||
return { ok: true, timedOut: false };
|
||||
}
|
||||
|
||||
let timedOut = false;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
timedOut = true;
|
||||
reject(new Error("accessControl ensureReady timed out"));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([accessControl.ensureReady(), 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;
|
||||
}
|
||||
}
|
||||
|
||||
async function bootstrapTrustedSeeds() {
|
||||
if (
|
||||
!(
|
||||
@@ -67,20 +117,49 @@ async function bootstrapTrustedSeeds() {
|
||||
fallbackSeeds: DEFAULT_TRUST_SEED_NPUBS,
|
||||
});
|
||||
moderationService.setTrustedSeeds(seeds);
|
||||
} catch {
|
||||
// Swallow bootstrap errors so login flow can continue; flag enables quick rollback.
|
||||
if (
|
||||
moderationService &&
|
||||
typeof moderationService.recomputeAllSummaries === "function"
|
||||
) {
|
||||
moderationService.recomputeAllSummaries();
|
||||
}
|
||||
} catch (error) {
|
||||
userLogger.warn("[bootstrap] Failed to apply trusted seeds", error);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (accessControl && typeof accessControl.ensureReady === "function") {
|
||||
await accessControl.ensureReady();
|
||||
let attempts = 0;
|
||||
const hydrate = async () => {
|
||||
attempts += 1;
|
||||
try {
|
||||
const result = await waitForAccessControl();
|
||||
applySeeds();
|
||||
return result;
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
"[bootstrap] Failed to hydrate admin lists for trusted seeds",
|
||||
error,
|
||||
);
|
||||
return { ok: false, timedOut: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
let hydrateResult = await hydrate();
|
||||
|
||||
if (!hydrateResult?.ok && attempts < 2) {
|
||||
const relaysReady = await waitForRelaysReady();
|
||||
if (relaysReady) {
|
||||
hydrateResult = await hydrate();
|
||||
} else {
|
||||
devLogger.warn(
|
||||
"[bootstrap] Skipping trusted seed retry because relays were not ready in time.",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Swallow bootstrap errors so login flow can continue; flag enables quick rollback.
|
||||
}
|
||||
|
||||
applySeeds();
|
||||
if (!hydrateResult?.ok) {
|
||||
applySeeds();
|
||||
}
|
||||
|
||||
const applyOnChange = () => {
|
||||
applySeeds();
|
||||
@@ -95,7 +174,7 @@ async function bootstrapTrustedSeeds() {
|
||||
}
|
||||
}
|
||||
|
||||
bootstrapTrustedSeeds();
|
||||
export const trustedSeedsReadyPromise = bootstrapTrustedSeeds();
|
||||
|
||||
function mergeServices(overrides = {}) {
|
||||
const merged = { nostrService, r2Service, hashtagPreferences };
|
||||
@@ -115,7 +194,16 @@ function mergeServices(overrides = {}) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function createApplication({ services, loadView: loadViewOverride } = {}) {
|
||||
export async function createApplication({ services, loadView: loadViewOverride } = {}) {
|
||||
try {
|
||||
await trustedSeedsReadyPromise;
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
"[bootstrap] Failed to await trusted seed hydration before creating application",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
return new Application({
|
||||
services: mergeServices(services),
|
||||
loadView: typeof loadViewOverride === "function" ? loadViewOverride : loadView,
|
||||
|
||||
+11
-11
@@ -3,7 +3,6 @@
|
||||
import { validateInstanceConfig } from "../config/validate-config.js";
|
||||
import { ASSET_VERSION } from "../config/asset-version.js";
|
||||
import "./bufferPolyfill.js";
|
||||
import Application from "./app.js";
|
||||
import { setApplication, setApplicationReady } from "./applicationContext.js";
|
||||
import nostrService from "./services/nostrService.js";
|
||||
import r2Service from "./services/r2Service.js";
|
||||
@@ -37,6 +36,7 @@ import { accessControl } from "./accessControl.js";
|
||||
import { nostrClient } from "./nostrClientFacade.js";
|
||||
import { userBlocks } from "./userBlocks.js";
|
||||
import { relayManager } from "./relayManager.js";
|
||||
import createApplication from "./bootstrap.js";
|
||||
|
||||
validateInstanceConfig();
|
||||
|
||||
@@ -354,17 +354,17 @@ function startApplication() {
|
||||
return applicationReadyPromise;
|
||||
}
|
||||
|
||||
application = new Application({
|
||||
services: {
|
||||
nostrService,
|
||||
r2Service,
|
||||
},
|
||||
loadView,
|
||||
});
|
||||
|
||||
setApplication(application);
|
||||
|
||||
const startupPromise = (async () => {
|
||||
application = await createApplication({
|
||||
services: {
|
||||
nostrService,
|
||||
r2Service,
|
||||
},
|
||||
loadView,
|
||||
});
|
||||
|
||||
setApplication(application);
|
||||
|
||||
await application.init();
|
||||
if (typeof application.start === "function") {
|
||||
await application.start();
|
||||
|
||||
@@ -87,6 +87,37 @@ test("trusted seeds contribute to trusted report counts", (t) => {
|
||||
assert.deepEqual(reporters.map((entry) => entry.pubkey), [seedHex]);
|
||||
});
|
||||
|
||||
test("trusted seed updates recompute guest report summaries", (t) => {
|
||||
withMockedNostrTools(t);
|
||||
|
||||
const eventId = "a".repeat(64);
|
||||
const reporterHex = "b".repeat(64);
|
||||
const reporterNpub = `npub${reporterHex}`;
|
||||
|
||||
const { service } = createModerationServiceHarness(t);
|
||||
|
||||
const report = createReportEvent({
|
||||
id: "c".repeat(64),
|
||||
reporter: reporterHex,
|
||||
eventId,
|
||||
createdAt: 1_700_000_999,
|
||||
type: "spam",
|
||||
});
|
||||
|
||||
service.ingestReportEvent(report);
|
||||
|
||||
const preSeedSummary = service.getTrustedReportSummary(eventId);
|
||||
assert.equal(preSeedSummary.totalTrusted, 0);
|
||||
assert.equal(preSeedSummary.types.spam?.trusted ?? 0, 0);
|
||||
|
||||
service.setTrustedSeeds([reporterNpub]);
|
||||
|
||||
const postSeedSummary = service.getTrustedReportSummary(eventId);
|
||||
assert.equal(postSeedSummary.totalTrusted, 1);
|
||||
assert.equal(postSeedSummary.types.spam.trusted, 1);
|
||||
assert.equal(service.trustedReportCount(eventId, "spam"), 1);
|
||||
});
|
||||
|
||||
test("moderator seeds populate trusted contacts", (t) => {
|
||||
withMockedNostrTools(t);
|
||||
|
||||
@@ -150,6 +181,7 @@ test("bootstrap seeds track editor roster and ignore whitelist-only changes", as
|
||||
};
|
||||
|
||||
const setTrustedSeedsCalls = [];
|
||||
const recomputeCalls = [];
|
||||
const moderationServiceMock = {
|
||||
setTrustedSeeds: (seeds) => {
|
||||
const snapshot = [];
|
||||
@@ -160,12 +192,16 @@ test("bootstrap seeds track editor roster and ignore whitelist-only changes", as
|
||||
}
|
||||
setTrustedSeedsCalls.push(snapshot);
|
||||
},
|
||||
recomputeAllSummaries: () => {
|
||||
recomputeCalls.push(true);
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.__bootstrapAccessControlMock = accessControlMock;
|
||||
globalThis.__bootstrapModerationServiceMock = moderationServiceMock;
|
||||
|
||||
await import("../../js/bootstrap.js");
|
||||
const bootstrapModule = await import("../../js/bootstrap.js");
|
||||
await bootstrapModule.trustedSeedsReadyPromise;
|
||||
|
||||
assert.equal(ensureReadyCalls, 1);
|
||||
assert.ok(
|
||||
@@ -174,6 +210,7 @@ test("bootstrap seeds track editor roster and ignore whitelist-only changes", as
|
||||
);
|
||||
|
||||
assert.equal(setTrustedSeedsCalls.length, 1);
|
||||
assert.equal(recomputeCalls.length, 1, "summaries should recompute after trusted seeds apply");
|
||||
const [initialSeeds] = setTrustedSeedsCalls;
|
||||
assert.ok(
|
||||
initialSeeds.includes(ADMIN_SUPER_NPUB),
|
||||
|
||||
Reference in new issue
Block a user