mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 10:52:27 +00:00
fix(feed): fall back to default relays in subscribeVideos on cold start
subscribeVideos called pool.sub(getHealthyRelays(this.relays)) with no empty-relay fallback, unlike the fetch paths in the same file. On a cold first load the 5s relay connect probe can time out, marking every relay unreachable; init() only warns and schedules a background reconnect (it does not throw), so boot proceeds and the feed subscribes to ZERO relays. The background reconnect later succeeds but nothing re-issues the subscription, so the feed stays empty until the user refreshes (when the SWR cache renders) — the reported "notes don't load on first visit / need a refresh" symptom. Fall back to DEFAULT_RELAY_URLS when no relays are healthy, mirroring the established pattern. Because the fallback bypasses the unreachable cooldown, the subscription retries those relays directly and self-heals instead of staying dead until refresh. Adds tests/nostr/subscribeVideosRelayFallback.test.mjs: empty healthy set => default relays (never zero), populated healthy set => uses those relays. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
8850eb962a
commit
818fe16254
2 files changed
+91
-1
No files matched your search
+11
-1
@@ -3321,7 +3321,17 @@ export class NostrClient {
|
||||
|
||||
devLogger.log("[subscribeVideos] Subscribing with filter:", filter);
|
||||
|
||||
const sub = this.pool.sub(this.getHealthyRelays(this.relays), [filter]);
|
||||
// Mirror the empty-relay fallback used by the fetch paths above: if no relays
|
||||
// are currently healthy (e.g. the initial 5s connect probe timed out on a
|
||||
// cold first load), fall back to the default relay set instead of subscribing
|
||||
// to ZERO relays. A zero-relay subscription silently never delivers events
|
||||
// and never self-heals when relays reconnect in the background, which forces
|
||||
// the user to refresh the page before the feed loads anything.
|
||||
const healthyRelays = sanitizeRelayList(this.getHealthyRelays(this.relays));
|
||||
const relaysToUse = healthyRelays.length
|
||||
? healthyRelays
|
||||
: Array.from(DEFAULT_RELAY_URLS);
|
||||
const sub = this.pool.sub(relaysToUse, [filter]);
|
||||
|
||||
// BUFFERING STATE
|
||||
// We collect events here instead of processing them instantly to avoid
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Regression test for the first-load feed race.
|
||||
//
|
||||
// Scenario (SCN-subscribe-videos-empty-relay-fallback):
|
||||
// Given the initial relay connect probe timed out on a cold first load, so no
|
||||
// relays are currently marked healthy,
|
||||
// When the feed subscription is created via subscribeVideos,
|
||||
// Then it must subscribe to the default relay set (not an empty relay list),
|
||||
// because a zero-relay subscription silently never delivers events and never
|
||||
// self-heals when relays reconnect — forcing the user to refresh the page.
|
||||
//
|
||||
// Observable outcome asserted at the boundary: the relay list passed to
|
||||
// pool.sub().
|
||||
|
||||
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { NostrClient } from "../../js/nostr/client.js";
|
||||
|
||||
if (!globalThis.WebSocket) {
|
||||
globalThis.WebSocket = class MockWebSocket {};
|
||||
}
|
||||
|
||||
describe("subscribeVideos relay fallback", () => {
|
||||
let client;
|
||||
let mockPool;
|
||||
let subRelayArgs;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new NostrClient();
|
||||
subRelayArgs = null;
|
||||
mockPool = {
|
||||
sub: mock.fn((relays) => {
|
||||
subRelayArgs = relays;
|
||||
return { on: mock.fn(), unsub: mock.fn() };
|
||||
}),
|
||||
};
|
||||
client.pool = mockPool;
|
||||
client.isInitialized = true;
|
||||
client.relays = ["wss://relay.example.com", "wss://relay2.example.com"];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
it("falls back to the default relay set when no relays are healthy", () => {
|
||||
// Simulate the cold-start race: the connect probe timed out, so every relay
|
||||
// is currently filtered out as unhealthy.
|
||||
client.getHealthyRelays = () => [];
|
||||
|
||||
client.subscribeVideos(() => {});
|
||||
|
||||
assert.equal(mockPool.sub.mock.callCount(), 1);
|
||||
assert.ok(Array.isArray(subRelayArgs), "pool.sub must receive a relay array");
|
||||
assert.ok(
|
||||
subRelayArgs.length > 0,
|
||||
"must never subscribe to ZERO relays when healthy relays are empty",
|
||||
);
|
||||
for (const url of subRelayArgs) {
|
||||
assert.match(
|
||||
url,
|
||||
/^wss?:\/\//,
|
||||
`fallback relay should be a websocket URL, got ${url}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the healthy relays when they are available", () => {
|
||||
const healthy = ["wss://relay.example.com"];
|
||||
client.getHealthyRelays = () => healthy;
|
||||
|
||||
client.subscribeVideos(() => {});
|
||||
|
||||
assert.equal(mockPool.sub.mock.callCount(), 1);
|
||||
assert.deepEqual(
|
||||
subRelayArgs,
|
||||
healthy,
|
||||
"should subscribe using the healthy relays, not the default fallback",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user