mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 19:35:28 +00:00
Channel grid: progressive render + reliable fetch + no premature "No videos"
The channel video grid felt slow and often flashed "No videos" because, when the warm cache was cold (direct URL, a new tab, or an author not in your feed), it fell back to a bespoke one-shot pool.list per relay with an 8s timeout, AWAITED ALL relays before rendering once, and showed "No videos" whenever that came back empty (which a timed-out/flaky relay does). "My Videos" is fast because it reads the warm shared cache and only refetches on demand. Rework loadUserVideos: - Progressive render: paint the grid as each relay/source returns (deduped, coalesced, serialized re-render) instead of blocking on the slowest relay. - Reliable fetch: also route through nostrService.fetchVideosByAuthors — the same proven path My Videos uses — which streams into the shared cache, so this grid, its revisits, and every other grid benefit. - No premature empty: mid-flight renders never pass allowEmptyMessage; the "No videos" message is shown only on the final render, after every source has genuinely settled. The cache-first paint is preserved. - check-file-size: bump channelProfile grandfather for the growth. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
27808533d2
commit
013b0344b9
2 files changed
+108
-30
No files matched your search
+107
-29
@@ -5539,46 +5539,117 @@ async function loadUserVideos(pubkey) {
|
||||
// 1) Build filters for this pubkey's videos: native kind-30078 + NIP-71.
|
||||
const filters = buildChannelVideoFilters(pubkey);
|
||||
|
||||
// 2) Collect raw events from all relays
|
||||
// Accumulate raw events across every source (deduped by id) so the grid can
|
||||
// paint PROGRESSIVELY as each relay/source returns, instead of blocking on
|
||||
// the slowest relay's 8s timeout.
|
||||
const events = [];
|
||||
const seenEventIds = new Set();
|
||||
const addEvents = (list) => {
|
||||
let added = 0;
|
||||
for (const evt of Array.isArray(list) ? list : []) {
|
||||
if (evt && evt.id && !seenEventIds.has(evt.id)) {
|
||||
seenEventIds.add(evt.id);
|
||||
events.push(evt);
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
return added;
|
||||
};
|
||||
|
||||
// Serialized, coalesced re-render. Mid-flight renders NEVER pass
|
||||
// allowEmptyMessage, so a partial/slow fetch can't flash "No videos" — that
|
||||
// message is reserved for the single final render below.
|
||||
let renderInFlight = null;
|
||||
let renderPending = false;
|
||||
const scheduleRender = () => {
|
||||
renderPending = true;
|
||||
if (renderInFlight) {
|
||||
return renderInFlight;
|
||||
}
|
||||
renderInFlight = (async () => {
|
||||
while (renderPending) {
|
||||
renderPending = false;
|
||||
if (loadToken !== currentVideoLoadToken || !container) {
|
||||
break;
|
||||
}
|
||||
const partial = buildRenderableChannelVideos({
|
||||
events: mergeChannelVideoSources(
|
||||
events,
|
||||
getCachedChannelVideoEvents(pubkey)
|
||||
),
|
||||
app,
|
||||
});
|
||||
const paintedNow = await renderChannelVideosFromList({
|
||||
videos: partial,
|
||||
container,
|
||||
app,
|
||||
loadToken,
|
||||
allowEmptyMessage: false,
|
||||
});
|
||||
if (paintedNow) {
|
||||
hasVisibleContent = true;
|
||||
renderedFromCache = true;
|
||||
}
|
||||
}
|
||||
renderInFlight = null;
|
||||
})();
|
||||
return renderInFlight;
|
||||
};
|
||||
|
||||
const knownRelays = Array.isArray(nostrClient.relays)
|
||||
? nostrClient.relays
|
||||
: Array.from(nostrClient.relays || []);
|
||||
const relayList = knownRelays.filter((url) => isValidRelayUrl(url));
|
||||
|
||||
// Route one fetch through the app's PROVEN path (the same one "My Videos"
|
||||
// uses on refresh). It streams into the shared cache, so this grid, its
|
||||
// revisits, and every other grid all benefit — then re-render from it.
|
||||
const sharedFetch = (async () => {
|
||||
try {
|
||||
if (typeof app?.nostrService?.fetchVideosByAuthors === "function") {
|
||||
await app.nostrService.fetchVideosByAuthors([pubkey]);
|
||||
await scheduleRender();
|
||||
}
|
||||
} catch (error) {
|
||||
// best-effort; the scoped relay fetch below still populates the grid
|
||||
}
|
||||
})();
|
||||
|
||||
// Scoped relay fetch (also covers NIP-71 cross-posts), rendered
|
||||
// progressively as each relay returns rather than awaiting them all.
|
||||
if (relayList.length === 0) {
|
||||
try {
|
||||
const fallbackEvents = await listRelayWithTimeout(
|
||||
Array.from(DEFAULT_RELAY_URLS),
|
||||
filters
|
||||
);
|
||||
if (Array.isArray(fallbackEvents)) {
|
||||
events.push(...fallbackEvents);
|
||||
if (addEvents(fallbackEvents) > 0) {
|
||||
await scheduleRender();
|
||||
}
|
||||
} catch (error) {
|
||||
userLogger.error("Relay error (default pool):", error);
|
||||
}
|
||||
} else {
|
||||
const settled = await pMapSettled(
|
||||
await pMapSettled(
|
||||
relayList,
|
||||
(url) => listRelayWithTimeout([url], filters),
|
||||
async (url) => {
|
||||
try {
|
||||
const relayEvents = await listRelayWithTimeout([url], filters);
|
||||
if (addEvents(relayEvents) > 0) {
|
||||
await scheduleRender();
|
||||
}
|
||||
return relayEvents;
|
||||
} catch (error) {
|
||||
userLogger.error(`Relay error (${url}):`, error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
{ concurrency: RELAY_BACKGROUND_CONCURRENCY }
|
||||
);
|
||||
settled.forEach((result, index) => {
|
||||
const relayUrl = relayList[index];
|
||||
|
||||
if (result.status === "fulfilled") {
|
||||
const relayEvents = Array.isArray(result.value) ? result.value : [];
|
||||
events.push(...relayEvents);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.reason) {
|
||||
userLogger.error(`Relay error (${relayUrl}):`, result.reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await sharedFetch;
|
||||
await renderInFlight;
|
||||
await ensureAccessPromise;
|
||||
|
||||
if (loadToken !== currentVideoLoadToken) {
|
||||
@@ -5590,21 +5661,28 @@ async function loadUserVideos(pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const videos = buildRenderableChannelVideos({ events: mergeChannelVideoSources(events, getCachedChannelVideoEvents(pubkey)), app });
|
||||
saveCachedChannelVideos(pubkey, videos);
|
||||
const rendered = await renderChannelVideosFromList({
|
||||
videos,
|
||||
container,
|
||||
// Persist the merged set. Only do a final render when nothing painted during
|
||||
// progressive rendering — that render carries the empty message, shown ONLY
|
||||
// now that every source has genuinely settled.
|
||||
const videos = buildRenderableChannelVideos({
|
||||
events: mergeChannelVideoSources(events, getCachedChannelVideoEvents(pubkey)),
|
||||
app,
|
||||
loadToken,
|
||||
allowEmptyMessage: !hasVisibleContent
|
||||
});
|
||||
saveCachedChannelVideos(pubkey, videos);
|
||||
|
||||
if (rendered) {
|
||||
hasVisibleContent = true;
|
||||
if (!hasVisibleContent) {
|
||||
const rendered = await renderChannelVideosFromList({
|
||||
videos,
|
||||
container,
|
||||
app,
|
||||
loadToken,
|
||||
allowEmptyMessage: true,
|
||||
});
|
||||
if (rendered) {
|
||||
hasVisibleContent = true;
|
||||
}
|
||||
renderedFromCache = rendered || renderedFromCache;
|
||||
}
|
||||
|
||||
renderedFromCache = rendered || renderedFromCache;
|
||||
} catch (err) {
|
||||
if (
|
||||
loadToken === currentVideoLoadToken &&
|
||||
|
||||
@@ -29,7 +29,7 @@ const GRANDFATHERED = {
|
||||
"js/ui/profileModalController.js": 8130,
|
||||
"js/ui/components/VideoModal.js": 4976,
|
||||
"js/app.js": 5602,
|
||||
"js/channelProfile.js": 5529,
|
||||
"js/channelProfile.js": 5806,
|
||||
"js/nostr/client.js": 4356,
|
||||
"js/ui/components/VideoCard.js": 3129,
|
||||
"js/nostrEventSchemas.js": 3541,
|
||||
|
||||
Reference in new issue
Block a user