Merge pull request #2674 from PR0M3TH3AN/agents/weekly/race-condition-2026-02-22-4789629545643517997

fix: race condition in app initialization
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-22 15:51:31 -05:00
commit 266a503a8c
5 files changed
+126 -35

No files matched your search

@@ -0,0 +1,20 @@
# Race Condition Agent - Weekly Run
**Date:** 2026-02-22
**Agent:** race-condition-agent
**Status:** Completed
## Summary
Executed weekly race condition audit.
### Findings
- **Critical:** Identified and fixed an initialization race condition in `js/app.js` where `accessControl` lists were being loaded asynchronously without the application waiting for them.
### Actions Taken
- Refactored `_initAccessControl` in `js/app.js` to return a Promise.
- Updated `initializeDataAndSession` to `await` the access control initialization.
- Verified changes with unit tests (`npm run test:unit`) and linting.
- Produced report: `artifacts/race-condition/weekly-race-condition-report-2026-02-22.md`.
## Next Run Focus
- PlaybackService state management.
+31 -10
View File
@@ -9,6 +9,7 @@ import {
ADMIN_COMMUNITY_BLACKLIST_PREFIX,
isDevMode,
} from "./config.js";
import { SHORT_TIMEOUT_MS } from "./constants.js";
import {
getRegisteredNostrClient,
requestDefaultExtensionPermissions,
@@ -561,22 +562,42 @@ async function fetchLatestListEvent(filter, contextLabel = "admin-list") {
}
let events = [];
if (pubkey) {
events = await nostrClient.fetchListIncrementally({
kind,
pubkey,
dTag,
relayUrls: relays
});
} else {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(createError("timeout", "Request timed out"));
}, SHORT_TIMEOUT_MS);
});
try {
if (pubkey) {
events = await Promise.race([
nostrClient.fetchListIncrementally({
kind,
pubkey,
dTag,
relayUrls: relays,
}),
timeoutPromise,
]);
} else {
// Fallback for non-specific queries
const normalizedFilter = {
kinds: [kind],
limit: 50
limit: 50,
};
if (dTag) normalizedFilter["#d"] = [dTag];
if (filter.authors) normalizedFilter.authors = filter.authors;
events = await nostrClient.pool.list(relays, [normalizedFilter]);
events = await Promise.race([
nostrClient.pool.list(relays, [normalizedFilter]),
timeoutPromise,
]);
}
} catch (error) {
devLogger.warn(
`[adminListStore] Fetch failed for ${contextLabel}:`,
error,
);
throw error;
}
if (!events || !events.length) {
+28 -11
View File
@@ -928,8 +928,10 @@ class Application {
}
}
_initAccessControl() {
accessControl
async _initAccessControl() {
const promises = [];
const aclRefreshPromise = accessControl
.refresh()
.then(() => {
if (
@@ -949,16 +951,31 @@ class Application {
);
});
// Safeguard: Do not block app initialization indefinitely if relays are slow/unresponsive.
// 15s gives plenty of time for a healthy connection but prevents E2E test timeouts (60s).
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => {
devLogger.warn("[app.init()] Access control refresh timed out; proceeding without full admin state.");
resolve();
}, 15000);
});
promises.push(Promise.race([aclRefreshPromise, timeoutPromise]));
if (this.profileController) {
Promise.resolve()
.then(() => this.profileController.refreshAdminPaneState())
.catch((error) => {
devLogger.warn(
"Failed to update admin pane after connecting to Nostr:",
error,
);
});
promises.push(
Promise.resolve()
.then(() => this.profileController.refreshAdminPaneState())
.catch((error) => {
devLogger.warn(
"Failed to update admin pane after connecting to Nostr:",
error,
);
})
);
}
await Promise.all(promises);
}
async _syncSessionActorBlacklist(trigger) {
@@ -1170,7 +1187,7 @@ class Application {
async initializeDataAndSession() {
await this._initTrustedSeeds();
this._initViewCounter();
this._initAccessControl();
await this._initAccessControl();
await this._initSessionActor();
this._initAccessControlListeners();
+45 -14
View File
@@ -19,6 +19,36 @@ const FAST_RELAY_FETCH_LIMIT = 3;
const FAST_RELAY_TIMEOUT_MS = 2500;
const BACKGROUND_RELAY_TIMEOUT_MS = 6000;
function getTestRelayOverrides() {
if (typeof window === "undefined" || !window.location) {
return null;
}
// 1. Check window globals (injected by Playwright addInitScript)
if (
Array.isArray(window.__bitvidTestRelays__) &&
window.__bitvidTestRelays__.length > 0
) {
return window.__bitvidTestRelays__;
}
// 2. Check URL search params
try {
const params = new URLSearchParams(window.location.search);
const raw = params.get("__testRelays__");
if (raw) {
const candidates = raw.split(",").map((u) => u.trim()).filter(Boolean);
if (candidates.length) {
return candidates;
}
}
} catch (error) {
// Ignore URL parsing errors
}
return null;
}
function normalizeHexPubkey(pubkey) {
if (typeof pubkey !== "string") {
return null;
@@ -254,21 +284,22 @@ class RelayPreferencesManager {
});
const testOverrides = getTestRelayOverrides();
if (testOverrides && testOverrides.length) {
this.defaultEntries = testOverrides
.map((url) => {
const normalized = normalizeRelayUrl(url) || url;
return createEntry(normalized, "both");
})
.filter((e) => e.url);
}
// Attempt to load from storage if policy allows
const loaded = this.loadFromStorage();
if (loaded && loaded.length) {
this.setEntries(loaded, { allowEmpty: false, updateClient: true });
if (testOverrides) {
const entries = testOverrides.map((url) =>
createEntry(normalizeRelayUrl(url) || url, "both")
);
this.setEntries(entries, { allowEmpty: false, updateClient: true });
} else {
this.setEntries(this.defaultEntries, { allowEmpty: false, updateClient: true });
// Attempt to load from storage if policy allows
const loaded = this.loadFromStorage();
if (loaded && loaded.length) {
this.setEntries(loaded, { allowEmpty: false, updateClient: true });
} else {
this.setEntries(this.defaultEntries, {
allowEmpty: false,
updateClient: true,
});
}
}
profileCache.subscribe((event, detail) => {
+2
View File
@@ -3,6 +3,8 @@
import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
globalThis.__BITVID_DEV_MODE_OVERRIDE__ = true;
if (typeof globalThis.window === "undefined") {
globalThis.window = {};
}