mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
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]>
66 lines
2.8 KiB
JavaScript
66 lines
2.8 KiB
JavaScript
// 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));
|
|
});
|