mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 21:02:28 +00:00
Harden Nostr startup performance
This commit is contained in:
1 parent
4525c1c657
commit
fe976b3ab7
17 files changed
+828
-93
No files matched your search
@@ -178,3 +178,37 @@ test("login reloads the open video comment thread", async () => {
|
||||
|
||||
assert.deepEqual(loadedVideos, [activeVideo]);
|
||||
});
|
||||
|
||||
test("duplicate login for the active identity joins the existing list sync", async () => {
|
||||
const coord = makeCoordinator({
|
||||
loadDirectMessages: async () => ({ ok: true }),
|
||||
refreshUnreadDmIndicator: async () => {},
|
||||
});
|
||||
let releaseBlocks;
|
||||
const blocksGate = new Promise((resolve) => {
|
||||
releaseBlocks = resolve;
|
||||
});
|
||||
let blockLoads = 0;
|
||||
coord.authService.loadBlocksForPubkey = async () => {
|
||||
blockLoads += 1;
|
||||
await blocksGate;
|
||||
return true;
|
||||
};
|
||||
const detail = {
|
||||
pubkey: ACTIVE_PUBKEY,
|
||||
identityChanged: false,
|
||||
postLogin: { profile: { name: "tester" } },
|
||||
postLoginPromise: Promise.resolve({ profile: { name: "tester" } }),
|
||||
};
|
||||
|
||||
await coord.handleAuthLogin(detail);
|
||||
const duplicate = coord.handleAuthLogin({ ...detail });
|
||||
for (let attempt = 0; attempt < 20 && blockLoads === 0; attempt += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
assert.equal(blockLoads, 1, "duplicate login must not start a second list sync");
|
||||
|
||||
releaseBlocks();
|
||||
await duplicate;
|
||||
assert.equal(coord._authLoginSync, null, "dedupe guard releases after list sync settles");
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
installRequestMonitor,
|
||||
isRequestMonitorEnabled,
|
||||
REQUEST_MONITOR_STORAGE_KEY,
|
||||
} from "../js/devReqMonitor.js";
|
||||
|
||||
function storage(value = null) {
|
||||
return { getItem: (key) => (key === REQUEST_MONITOR_STORAGE_KEY ? value : null) };
|
||||
}
|
||||
|
||||
function createScope({ hostname = "localhost", enabled = false } = {}) {
|
||||
class FakeWebSocket {
|
||||
send(data) {
|
||||
this.sent = data;
|
||||
return "sent";
|
||||
}
|
||||
}
|
||||
const intervals = [];
|
||||
return {
|
||||
location: { hostname },
|
||||
localStorage: storage(enabled ? "1" : null),
|
||||
WebSocket: FakeWebSocket,
|
||||
setInterval: (callback, delay) => {
|
||||
intervals.push({ callback, delay });
|
||||
return intervals.length;
|
||||
},
|
||||
clearInterval: () => {},
|
||||
console: { info: () => {}, warn: () => {} },
|
||||
intervals,
|
||||
};
|
||||
}
|
||||
|
||||
test("request monitor stays disabled on localhost until explicitly enabled", () => {
|
||||
const scope = createScope();
|
||||
const originalSend = scope.WebSocket.prototype.send;
|
||||
assert.equal(installRequestMonitor({ scope }), null);
|
||||
assert.equal(scope.WebSocket.prototype.send, originalSend);
|
||||
assert.equal(scope.intervals.length, 0);
|
||||
});
|
||||
|
||||
test("request monitor cannot be enabled on a public host", () => {
|
||||
assert.equal(
|
||||
isRequestMonitorEnabled({
|
||||
locationRef: { hostname: "unstable.bitvid.network" },
|
||||
storage: storage("1"),
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("explicit loopback opt-in patches REQ sends and can be stopped", () => {
|
||||
const scope = createScope({ enabled: true });
|
||||
const originalSend = scope.WebSocket.prototype.send;
|
||||
const monitor = installRequestMonitor({ scope });
|
||||
|
||||
assert.ok(monitor);
|
||||
assert.notEqual(scope.WebSocket.prototype.send, originalSend);
|
||||
assert.equal(scope.intervals.length, 1);
|
||||
assert.equal(scope.intervals[0].delay, 2000);
|
||||
|
||||
const socket = new scope.WebSocket();
|
||||
assert.equal(socket.send('["REQ","sub",{"kinds":[1]}]'), "sent");
|
||||
assert.equal(socket.sent, '["REQ","sub",{"kinds":[1]}]');
|
||||
|
||||
monitor.stop();
|
||||
assert.equal(scope.WebSocket.prototype.send, originalSend);
|
||||
assert.equal(scope.__bitvidReqMonitorInstalled, undefined);
|
||||
});
|
||||
@@ -20,6 +20,7 @@ function makeHarness({ whitelistOn = false, whitelist = [] } = {}) {
|
||||
const allEvents = new Map();
|
||||
let capturedFilters = null;
|
||||
let onEvent = null;
|
||||
let onEose = null;
|
||||
let closed = false;
|
||||
let whitelistListener = null;
|
||||
|
||||
@@ -27,6 +28,7 @@ function makeHarness({ whitelistOn = false, whitelist = [] } = {}) {
|
||||
subscribe(opts) {
|
||||
capturedFilters = opts.filters;
|
||||
onEvent = opts.onEvent;
|
||||
onEose = opts.onEose;
|
||||
return {
|
||||
close() {
|
||||
closed = true;
|
||||
@@ -81,6 +83,7 @@ function makeHarness({ whitelistOn = false, whitelist = [] } = {}) {
|
||||
return closed;
|
||||
},
|
||||
fireEvent: (e) => onEvent?.(e),
|
||||
triggerEose: () => onEose?.(),
|
||||
triggerWhitelistChange: () => whitelistListener?.(),
|
||||
triggerFeedReady: () => feedListeners.forEach((fn) => fn({})),
|
||||
nostrClient,
|
||||
@@ -263,6 +266,26 @@ test("startWhenFeedReady falls back to starting if the feed never emits", async
|
||||
svc.stop();
|
||||
});
|
||||
|
||||
test("initial NIP-71 sync keeps every event but emits one consolidated feed update", () => {
|
||||
const h = makeHarness();
|
||||
const svc = makeService(h, { feedReadyFallbackMs: 100000, initialSyncFallbackMs: 100000 });
|
||||
|
||||
svc.startWhenFeedReady();
|
||||
h.triggerFeedReady();
|
||||
h.fireEvent(foreignEvent({ id: "a", kind: 34235, tags: [["d", "ra"], ["title", "A"], ["imeta", "url https://e/a.mp4", "m video/mp4"]] }));
|
||||
h.fireEvent(foreignEvent({ id: "b", kind: 34235, tags: [["d", "rb"], ["title", "B"], ["imeta", "url https://e/b.mp4", "m video/mp4"]] }));
|
||||
svc.flush();
|
||||
|
||||
assert.ok(h.activeMap.has("ROOT:ra") && h.activeMap.has("ROOT:rb"), "all initial NIP-71 events are retained");
|
||||
assert.equal(h.emitted.filter((e) => e.name === "videos:updated").length, 0, "no partial-startup repaint");
|
||||
|
||||
h.triggerEose();
|
||||
const refreshes = h.emitted.filter((e) => e.name === "videos:updated");
|
||||
assert.equal(refreshes.length, 1, "one consolidated update at initial sync completion");
|
||||
assert.equal(refreshes[0].detail.reason, "nip71-ingest");
|
||||
svc.stop();
|
||||
});
|
||||
|
||||
test("coalesces refresh signals so an event burst can't storm the feed", () => {
|
||||
const h = makeHarness();
|
||||
const svc = makeService(h, { refreshThrottleMs: 10000 });
|
||||
|
||||
@@ -111,21 +111,36 @@ class FakeRelayPool {
|
||||
}
|
||||
|
||||
async function configureNostrTools() {
|
||||
const previousCanonical = globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
||||
const previousCanonicalDescriptor = Object.getOwnPropertyDescriptor(
|
||||
globalThis,
|
||||
"__BITVID_CANONICAL_NOSTR_TOOLS__",
|
||||
);
|
||||
const previousNostrTools = globalThis.NostrTools;
|
||||
const previousReady = globalThis.nostrToolsReady;
|
||||
|
||||
const nostrTools = await import("nostr-tools");
|
||||
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = nostrTools;
|
||||
// nostrToolsBootstrap deliberately exposes this value as read-only. Test
|
||||
// setup must replace its *configurable descriptor*, rather than assigning to
|
||||
// the property and failing after another test/import initialized bootstrap.
|
||||
Object.defineProperty(globalThis, "__BITVID_CANONICAL_NOSTR_TOOLS__", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: nostrTools,
|
||||
});
|
||||
globalThis.NostrTools = nostrTools;
|
||||
globalThis.nostrToolsReady = Promise.resolve({ ok: true, value: nostrTools });
|
||||
|
||||
const restore = () => {
|
||||
if (previousCanonical === undefined) {
|
||||
if (!previousCanonicalDescriptor) {
|
||||
delete globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
||||
} else {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = previousCanonical;
|
||||
Object.defineProperty(
|
||||
globalThis,
|
||||
"__BITVID_CANONICAL_NOSTR_TOOLS__",
|
||||
previousCanonicalDescriptor,
|
||||
);
|
||||
}
|
||||
|
||||
if (previousNostrTools === undefined) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
initPerformanceHarness,
|
||||
isLocalPerformanceHarnessEnabled,
|
||||
PERFORMANCE_HARNESS_STORAGE_KEY,
|
||||
} from "../js/performanceHarness.js";
|
||||
|
||||
function createStorage(value = null) {
|
||||
return { getItem: (key) => (key === PERFORMANCE_HARNESS_STORAGE_KEY ? value : null) };
|
||||
}
|
||||
|
||||
function createPerformance() {
|
||||
const marks = [];
|
||||
return {
|
||||
now: () => 100,
|
||||
mark: (name) => marks.push(name),
|
||||
getEntriesByType: (type) => {
|
||||
if (type === "resource") {
|
||||
return [{ transferSize: 12 }, { transferSize: 30 }];
|
||||
}
|
||||
if (type === "navigation") {
|
||||
return [{ domContentLoadedEventEnd: 12, loadEventEnd: 24 }];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
memory: { usedJSHeapSize: 10, totalJSHeapSize: 20, jsHeapSizeLimit: 30 },
|
||||
marks,
|
||||
};
|
||||
}
|
||||
|
||||
test("performance harness stays absent without explicit loopback opt-in", () => {
|
||||
const scope = {
|
||||
location: { hostname: "localhost" },
|
||||
localStorage: createStorage(),
|
||||
performance: createPerformance(),
|
||||
};
|
||||
|
||||
assert.equal(isLocalPerformanceHarnessEnabled({
|
||||
locationRef: scope.location,
|
||||
storage: scope.localStorage,
|
||||
}), false);
|
||||
assert.equal(initPerformanceHarness({ scope }), null);
|
||||
assert.equal(scope.__bitvidPerformance, undefined);
|
||||
});
|
||||
|
||||
test("performance harness cannot be enabled on a non-loopback host", () => {
|
||||
assert.equal(isLocalPerformanceHarnessEnabled({
|
||||
locationRef: { hostname: "unstable.bitvid.network" },
|
||||
storage: createStorage("1"),
|
||||
}), false);
|
||||
});
|
||||
|
||||
test("explicit loopback opt-in exposes bounded local measurements", () => {
|
||||
let observerCallback = null;
|
||||
class FakePerformanceObserver {
|
||||
constructor(callback) {
|
||||
observerCallback = callback;
|
||||
}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
}
|
||||
const scope = {
|
||||
location: { hostname: "127.0.0.1" },
|
||||
localStorage: createStorage("1"),
|
||||
performance: createPerformance(),
|
||||
PerformanceObserver: FakePerformanceObserver,
|
||||
};
|
||||
|
||||
const api = initPerformanceHarness({ scope });
|
||||
assert.equal(typeof api?.snapshot, "function");
|
||||
assert.equal(scope.__bitvidPerformance, api);
|
||||
|
||||
observerCallback({
|
||||
getEntries: () => [{ name: "longtask", startTime: 3, duration: 51 }],
|
||||
});
|
||||
api.mark("feed-refresh", { feed: "for-you" });
|
||||
api.setGauge("active-workers", 1);
|
||||
api.increment("active-workers");
|
||||
|
||||
const snapshot = api.snapshot();
|
||||
assert.equal(snapshot.longTasks.count, 1);
|
||||
assert.equal(snapshot.longTasks.totalDurationMs, 51);
|
||||
assert.equal(snapshot.resources.transferSize, 42);
|
||||
assert.equal(snapshot.gauges["active-workers"], 2);
|
||||
assert.equal(snapshot.memory.usedJsHeapSize, 10);
|
||||
assert.ok(snapshot.marks.some((entry) => entry.name === "feed-refresh"));
|
||||
|
||||
api.stop();
|
||||
assert.equal(scope.__bitvidPerformance, undefined);
|
||||
});
|
||||
@@ -9,6 +9,8 @@ describe("NostrService", () => {
|
||||
// Reset state
|
||||
nostrService.videosMap = new Map();
|
||||
nostrService.dmMessages = [];
|
||||
nostrService.dmMessageIndex = new Map();
|
||||
nostrService.dmActorPubkey = null;
|
||||
|
||||
// Mock the internal nostrClient
|
||||
mockClient = {
|
||||
@@ -145,4 +147,30 @@ describe("NostrService", () => {
|
||||
assert.equal(nostrService.dmMessageIndex.size, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadDirectMessages", () => {
|
||||
it("uses the actor NIP-17 relay list for the initial historical inbox fetch", async () => {
|
||||
const actor = "a".repeat(64);
|
||||
const dmRelay = "wss://dm-relay.example.com";
|
||||
mockClient.readRelays = ["wss://discovery.example.com"];
|
||||
mockClient.pool.list.mock.mockImplementation(async () => [
|
||||
{
|
||||
pubkey: actor,
|
||||
created_at: 10,
|
||||
tags: [["relay", dmRelay]],
|
||||
},
|
||||
]);
|
||||
mockClient.listDirectMessages = mock.fn(async () => []);
|
||||
|
||||
await nostrService.loadDirectMessages({ actorPubkey: actor, initialLoad: true });
|
||||
|
||||
assert.equal(mockClient.listDirectMessages.mock.callCount(), 1);
|
||||
const [, options] = mockClient.listDirectMessages.mock.calls[0].arguments;
|
||||
assert.deepEqual(
|
||||
options.relays,
|
||||
[dmRelay],
|
||||
"historical NIP-17 messages must use the same relay list as the live subscription",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -57,8 +57,17 @@ class MockTorrent extends EventEmitter {
|
||||
}
|
||||
|
||||
destroy(opts, cb) {
|
||||
this.destroyed = true;
|
||||
this.destroyedDuringWire = this.emittingWire === true;
|
||||
if (typeof cb === 'function') cb();
|
||||
}
|
||||
|
||||
emit(eventName, ...args) {
|
||||
if (eventName === 'wire') this.emittingWire = true;
|
||||
const result = super.emit(eventName, ...args);
|
||||
if (eventName === 'wire') this.emittingWire = false;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
describe("WebTorrent Regression Tests", () => {
|
||||
@@ -118,4 +127,18 @@ describe("WebTorrent Regression Tests", () => {
|
||||
assert.strictEqual(result.healthy, false, "Should be unhealthy");
|
||||
assert.strictEqual(result.reason, "timeout", "Reason should be 'timeout'");
|
||||
});
|
||||
|
||||
it("defers probe cleanup until after a successful wire callback returns", async () => {
|
||||
const client = new TorrentClient({ webTorrentClass: MockWebTorrent });
|
||||
const magnet = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567";
|
||||
const result = await client.probePeers(magnet, {
|
||||
timeoutMs: 1000,
|
||||
urlList: ["http://localhost:8080/video.mp4"],
|
||||
});
|
||||
const torrent = client.probeClient.torrents[0];
|
||||
|
||||
assert.equal(result.healthy, true);
|
||||
assert.equal(torrent.destroyed, true, "temporary probe torrent is cleaned up before its queue slot releases");
|
||||
assert.equal(torrent.destroyedDuringWire, false, "probe cleanup is not synchronous with the wire event");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user