mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 04:42:57 +00:00
[channel] Fix slow/incomplete channel video grid: coalesce loads, bound relays, parity source
Three fixes for "the channel grid loads slowly and not all videos come in even when they show on other grids": 1. Race condition (primary "won't load" cause). loadUserVideos() bumps a shared currentVideoLoadToken and only renders if its token is still current. A burst of refresh triggers — e.g. the NIP-71 ingest flushing repeatedly right after a channel opens (RELAY STORM in the logs) — supersedes every in-flight load before it paints, so the grid stays on "Loading videos…". Add requestChannelVideoLoad(): while a load for the same channel is in flight, a refresh flags a single trailing rerun instead of starting a competing load; navigation still starts immediately. All three call sites routed through it. 2. Slowness from hung relays. The per-relay pool.list() fan-out had no timeout, so one relay that never sends EOSE stalled the merged render. Add raceListWithTimeout() (exported, pure) bounding each list() to 8s, resolving to [] on timeout/rejection. 3. Completeness/parity. The channel grid sourced only nostrClient.allEvents + a scoped relay fetch, which can diverge from the shared feed cache the other grids render from. getCachedChannelVideoEvents() now also unions the author's videos from nostrService.getActiveVideosByAuthors() (same videosMap-backed author index), so a video shown on another grid is present here by construction. Tests: tests/channel-relay-timeout.test.mjs (5). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
61e4b25de7
commit
a4549f967f
2 files changed
+174
-6
No files matched your search
@@ -0,0 +1,65 @@
|
||||
// Channel-grid relay fetch is bounded (raceListWithTimeout): a hung relay (one
|
||||
// that never sends EOSE) must resolve to "no events" instead of stalling the
|
||||
// whole grid render. A failing relay likewise contributes [] rather than
|
||||
// throwing, and a late rejection after the timeout can't surface as unhandled.
|
||||
//
|
||||
// test_integrity_note:
|
||||
// change_type: ["new_tests"]
|
||||
// scenarios:
|
||||
// - id: SCN-channel-relay-timeout
|
||||
// given: "a relay list() that resolves / hangs / rejects / returns non-array"
|
||||
// when: "raceListWithTimeout wraps it with a short timeout"
|
||||
// then: "success returns the array; hang/reject/garbage resolve to []"
|
||||
// observable_outcomes:
|
||||
// - "a fast success passes the array through unchanged"
|
||||
// - "a hang resolves to [] within the timeout bound (does not stall)"
|
||||
// - "a rejection resolves to [] (does not throw)"
|
||||
// determinism_controls:
|
||||
// - "explicit short timeoutMs; hang modeled as a never-resolving promise"
|
||||
// anti_cheat_rationale:
|
||||
// prevents: ["retry/sleep-based flake masking", "hard-coded return value"]
|
||||
// relaxation:
|
||||
// did_relax_any_assertion: false
|
||||
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import test from "node:test";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { raceListWithTimeout } from "../js/channelProfile.js";
|
||||
|
||||
test("passes a fast successful result through unchanged", async () => {
|
||||
const events = [{ id: "a" }, { id: "b" }];
|
||||
const result = await raceListWithTimeout(async () => events, 1000);
|
||||
assert.deepEqual(result, events);
|
||||
});
|
||||
|
||||
test("a hung relay resolves to [] within the timeout instead of stalling", async () => {
|
||||
const started = Date.now();
|
||||
// Never resolves — models a relay that never sends EOSE.
|
||||
const result = await raceListWithTimeout(() => new Promise(() => {}), 30);
|
||||
assert.deepEqual(result, []);
|
||||
assert.ok(Date.now() - started < 500, "must resolve promptly via the timeout");
|
||||
});
|
||||
|
||||
test("a rejecting relay resolves to [] rather than throwing", async () => {
|
||||
const result = await raceListWithTimeout(async () => {
|
||||
throw new Error("relay exploded");
|
||||
}, 1000);
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
test("a non-array result is normalized to []", async () => {
|
||||
const result = await raceListWithTimeout(async () => null, 1000);
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
test("a late rejection after the timeout wins does not throw (no unhandled)", async () => {
|
||||
// The list rejects AFTER the timeout has already resolved to []. The helper
|
||||
// must swallow it so it can't surface as an unhandled rejection.
|
||||
const result = await raceListWithTimeout(
|
||||
() => new Promise((_, reject) => setTimeout(() => reject(new Error("late")), 40)),
|
||||
10,
|
||||
);
|
||||
assert.deepEqual(result, []);
|
||||
// Give the late rejection a tick to (not) blow up.
|
||||
await new Promise((resolve) => setTimeout(resolve, 60));
|
||||
});
|
||||
Reference in new issue
Block a user