mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-05 04:12:25 +00:00
client-reliability-2: make concurrent-init test hermetic and close feed transport
This commit is contained in:
1 parent
3968a97922
commit
181eae95d9
7 files changed
+141
-31
No files matched your search
@@ -1,4 +1,3 @@
|
||||
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
@@ -6,54 +5,97 @@ import { NostrClient } from "../../js/nostr/client.js";
|
||||
|
||||
// Mock global.window if missing
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
globalThis.window = {};
|
||||
}
|
||||
|
||||
function deferred() {
|
||||
let resolve;
|
||||
const promise = new Promise((done) => { resolve = done; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
test("NostrClient initialization race condition: concurrent init() calls", async (t) => {
|
||||
const client = new NostrClient();
|
||||
|
||||
// Mock ensurePool to simulate a slow initialization
|
||||
const relay = "wss://race.example.invalid";
|
||||
client.relays = [relay];
|
||||
const poolEntered = deferred();
|
||||
const releasePool = deferred();
|
||||
let poolReady = false;
|
||||
let subscription;
|
||||
let initPromiseA;
|
||||
const subscriptions = [];
|
||||
|
||||
// We mock connectionManager.ensurePool via the instance
|
||||
client.connectionManager.ensurePool = async () => {
|
||||
// Delay to simulate async work
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
// Teardown runs even if an assertion fails before init or subscription setup.
|
||||
t.after(async () => {
|
||||
releasePool.resolve();
|
||||
try {
|
||||
await initPromiseA;
|
||||
} finally {
|
||||
subscription?.unsub();
|
||||
client.resetRelayReconnectState();
|
||||
}
|
||||
});
|
||||
|
||||
// Create a fake pool
|
||||
const fakePool = {
|
||||
sub: () => ({ on: () => {}, unsub: () => {} }),
|
||||
get: async () => null,
|
||||
list: async () => [],
|
||||
ensureRelay: async () => {},
|
||||
};
|
||||
// Virtualize the real pool transport boundary; keep init, L1 subscription
|
||||
// management and the returned feed subscription's cleanup real.
|
||||
const fakePool = {
|
||||
sub: (relays, filters) => {
|
||||
assert.equal(poolReady, true, "Subscription must use the initialized pool");
|
||||
const record = { relays, filters, closed: 0 };
|
||||
subscriptions.push(record);
|
||||
return {
|
||||
on: () => {},
|
||||
unsub: () => { record.closed += 1; },
|
||||
};
|
||||
},
|
||||
get: async () => null,
|
||||
list: async () => [],
|
||||
ensureRelay: async () => {},
|
||||
};
|
||||
t.mock.method(client.connectionManager, "ensurePool", async () => {
|
||||
poolEntered.resolve();
|
||||
await releasePool.promise;
|
||||
client.connectionManager.pool = fakePool;
|
||||
poolReady = true;
|
||||
return fakePool;
|
||||
};
|
||||
});
|
||||
// An empty result means connection failure and starts real reconnect timers.
|
||||
// Model a successful connection at the manager boundary used by init/retry.
|
||||
t.mock.method(client.connectionManager, "connectToRelays", async () => [
|
||||
{ url: relay, success: true },
|
||||
]);
|
||||
t.mock.method(client, "restoreLocalData", async () => true);
|
||||
t.mock.method(client, "scheduleStoredRemoteSignerRestore", async () => {});
|
||||
|
||||
// Also mock connectToRelays to avoid network calls
|
||||
client.connectToRelays = async () => [];
|
||||
client.restoreLocalData = async () => true;
|
||||
|
||||
// ACT: Call init() (Caller A)
|
||||
const initPromiseA = client.init();
|
||||
|
||||
// ACT: Call init() again immediately (Caller B)
|
||||
initPromiseA = client.init();
|
||||
const initPromiseB = client.init();
|
||||
|
||||
// ASSERT: Caller B should receive a promise that resolves when initialization is complete.
|
||||
// In the buggy version, initPromiseB is undefined because it returns early.
|
||||
assert.ok(initPromiseB instanceof Promise, "Second init() call should return a Promise, not undefined");
|
||||
|
||||
await poolEntered.promise;
|
||||
let secondInitSettled = false;
|
||||
void initPromiseB.then(() => { secondInitSettled = true; });
|
||||
await Promise.resolve();
|
||||
assert.equal(secondInitSettled, false, "Second caller must wait for pool initialization");
|
||||
|
||||
releasePool.resolve();
|
||||
await initPromiseB;
|
||||
|
||||
assert.equal(poolReady, true, "Pool should be ready after awaiting second init() call");
|
||||
await initPromiseA;
|
||||
assert.equal(client.connectionManager.relayReconnectTimer, null, "Successful fixture initialization must not schedule relay retries");
|
||||
|
||||
// Verify we can use the client
|
||||
try {
|
||||
client.subscribeVideos(() => {});
|
||||
subscription = client.subscribeVideos(() => {});
|
||||
} catch (e) {
|
||||
assert.fail("Should be able to subscribe after awaiting init()");
|
||||
}
|
||||
assert.equal(subscriptions.length, 1, "Post-init feed subscription must reach the pool transport");
|
||||
assert.ok(subscriptions[0].relays.length > 0, "Feed must subscribe to a nonempty relay set");
|
||||
assert.ok(subscriptions[0].filters.some((filter) =>
|
||||
filter.kinds.includes(30078) && filter["#t"].includes("video")),
|
||||
"Feed must request video events");
|
||||
|
||||
subscription.unsub();
|
||||
assert.equal(subscriptions[0].closed, 1, "Feed cleanup must close its transport subscription");
|
||||
subscription.unsub();
|
||||
assert.equal(subscriptions[0].closed, 1, "Feed cleanup must be idempotent");
|
||||
});
|
||||
Reference in new issue
Block a user