mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
channel: union live fetch with known events so the grid never shrinks
loadUserVideos rendered only the live relay fetch, and the render REPLACES the grid container. So when a relay holding a creator's video transiently failed or EOSE'd late, that video disappeared from the wall even though it was already known in memory/cache — the "channel doesn't pull in all of a creator's videos" bug (#20, symptom 1). Union the live events with the already-known author events (mergeChannelVideoSources, live wins on id collision) before building the renderable list, so the rendered set is a superset and can't shrink on a transient relay gap. Existing dedupe-by-root + access/delete filtering still decide precedence downstream, so newer versions and deletions still win. Helper + tests live in channelProfileVideos.js (channelProfile.js is at its size cap); the call site stays net-zero. Streaming/first-relay-wins render is still the separate 17d UX follow-up. Regression: tests/channel-video-merge.test.mjs. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
2c62b959fd
commit
f05ba5edf9
4 files changed
+90
-3
No files matched your search
@@ -1,7 +1,7 @@
|
||||
// js/channelProfile.js
|
||||
|
||||
import { nostrClient } from "./nostrClientFacade.js";
|
||||
import { convertChannelEvent, buildChannelVideoFilters, loadCachedChannelVideos, saveCachedChannelVideos } from "./channelProfileVideos.js";
|
||||
import { convertChannelEvent, buildChannelVideoFilters, loadCachedChannelVideos, saveCachedChannelVideos, mergeChannelVideoSources } from "./channelProfileVideos.js";
|
||||
import { dedupeVideos } from "./utils/videoDeduper.js";
|
||||
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
||||
import { subscriptions } from "./subscriptions.js";
|
||||
@@ -5440,7 +5440,7 @@ async function loadUserVideos(pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const videos = buildRenderableChannelVideos({ events, app });
|
||||
const videos = buildRenderableChannelVideos({ events: mergeChannelVideoSources(events, getCachedChannelVideoEvents(pubkey)), app });
|
||||
saveCachedChannelVideos(pubkey, videos);
|
||||
const rendered = await renderChannelVideosFromList({
|
||||
videos,
|
||||
|
||||
@@ -28,6 +28,33 @@ export function convertChannelEvent(entry) {
|
||||
return sharedConvertEventToVideo(entry);
|
||||
}
|
||||
|
||||
// Reconcile a fresh live fetch with already-known events (the in-memory video
|
||||
// store + persisted cache) so a transiently-failing relay can't SHRINK the grid.
|
||||
// The channel render replaces the container with the live set, so rendering only
|
||||
// what this load fetched drops any video whose sole relay timed out — the "some
|
||||
// of a creator's videos are missing" bug. Union the two by event id (live wins on
|
||||
// collision since it's the freshest copy); the caller's dedupe-by-root + access
|
||||
// checks still handle versioning, deletes, and precedence downstream.
|
||||
export function mergeChannelVideoSources(liveEvents, knownEvents) {
|
||||
const byId = new Map();
|
||||
const extras = [];
|
||||
const add = (list) => {
|
||||
if (!Array.isArray(list)) return;
|
||||
for (const entry of list) {
|
||||
if (!entry || typeof entry !== "object") continue;
|
||||
const id = typeof entry.id === "string" ? entry.id : "";
|
||||
if (!id) {
|
||||
extras.push(entry);
|
||||
continue;
|
||||
}
|
||||
if (!byId.has(id)) byId.set(id, entry);
|
||||
}
|
||||
};
|
||||
add(liveEvents); // live first → wins on id collision
|
||||
add(knownEvents);
|
||||
return [...byId.values(), ...extras];
|
||||
}
|
||||
|
||||
// ---- Per-channel persisted cache (instant cold-load paint) -------------------
|
||||
// Mirrors the main feed's optimistic cache: on a cold hard-refresh of a profile,
|
||||
// render the last-seen videos from localStorage BEFORE relays connect, then the
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Regression (#20): the channel grid render replaces the container with the live
|
||||
// fetch, so a transiently-failing relay produced a smaller set and previously-
|
||||
// known videos vanished ("some of a creator's videos are missing"). The live
|
||||
// fetch must be unioned with already-known events so the grid never shrinks.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { mergeChannelVideoSources } from "../js/channelProfileVideos.js";
|
||||
|
||||
const ev = (id, extra = {}) => ({ id, kind: 30078, ...extra });
|
||||
|
||||
test("union is a superset of both live and known events", () => {
|
||||
const live = [ev("a"), ev("b")];
|
||||
const known = [ev("b"), ev("c"), ev("d")];
|
||||
|
||||
const merged = mergeChannelVideoSources(live, known);
|
||||
const ids = merged.map((e) => e.id).sort();
|
||||
|
||||
assert.deepEqual(ids, ["a", "b", "c", "d"], "all unique ids from both sources");
|
||||
});
|
||||
|
||||
test("a known video missing from the live fetch still appears (the bug)", () => {
|
||||
// The live fetch this load only returned one video; the other is known in
|
||||
// memory (e.g. from a prior load / the main feed). It must not disappear.
|
||||
const live = [ev("only-live")];
|
||||
const known = [ev("only-live"), ev("dropped-by-slow-relay")];
|
||||
|
||||
const merged = mergeChannelVideoSources(live, known);
|
||||
const ids = merged.map((e) => e.id).sort();
|
||||
|
||||
assert.deepEqual(ids, ["dropped-by-slow-relay", "only-live"]);
|
||||
});
|
||||
|
||||
test("live wins on id collision (freshest copy)", () => {
|
||||
const live = [ev("x", { created_at: 200, source: "live" })];
|
||||
const known = [ev("x", { created_at: 100, source: "cache" })];
|
||||
|
||||
const merged = mergeChannelVideoSources(live, known);
|
||||
assert.equal(merged.length, 1);
|
||||
assert.equal(merged[0].source, "live", "live copy must win the dedupe");
|
||||
});
|
||||
|
||||
test("preserves id-less entries and tolerates non-array / junk input", () => {
|
||||
const live = [ev("a"), { kind: 30078 /* no id */ }, null, "junk"];
|
||||
const merged = mergeChannelVideoSources(live, undefined);
|
||||
const withId = merged.filter((e) => e && e.id === "a");
|
||||
const withoutId = merged.filter((e) => e && typeof e === "object" && !e.id);
|
||||
|
||||
assert.equal(withId.length, 1);
|
||||
assert.equal(withoutId.length, 1, "id-less event kept, not dropped");
|
||||
assert.deepEqual(mergeChannelVideoSources(null, null), []);
|
||||
});
|
||||
@@ -267,7 +267,15 @@ un-hides when WebTorrent flips green). The real gaps:
|
||||
### 20. Feed / grid loading reliability — some videos missing (BUG)
|
||||
Two related intermittent symptoms; likely the same root cause (a one-shot,
|
||||
all-relays-settle fetch that drops slow-relay results — see 17d).
|
||||
- [ ] **Profile / Channel page sometimes doesn't pull in all of a creator's videos.**
|
||||
- [x] **Profile / Channel page sometimes doesn't pull in all of a creator's
|
||||
videos.** Root cause: `loadUserVideos` rendered ONLY the live fetch, which
|
||||
REPLACES the grid — so when a relay holding a video transiently failed this
|
||||
load, that video vanished even though it was known in memory/cache. Fixed by
|
||||
unioning the live events with already-known events
|
||||
(`mergeChannelVideoSources`, live-wins-on-id) before render, so the grid is a
|
||||
superset and never shrinks; existing dedupe-by-root + access checks still
|
||||
handle versioning/deletes. Regression: `tests/channel-video-merge.test.mjs`.
|
||||
(Streaming/first-relay-wins UX is still the 17d follow-up.)
|
||||
- [ ] **Paginating between video-grid pages sometimes doesn't pull in all videos.**
|
||||
- [ ] Audit the channel/search/grid fetch for: relays that EOSE late being dropped,
|
||||
pagination cursor gaps, dedup discarding valid items, and cache-vs-live
|
||||
|
||||
Reference in new issue
Block a user