mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Fix subscription feed hydration and infinite loading state
This commit is contained in:
1 parent
19cafc9f1a
commit
b07332395b
3 files changed
+158
-1
No files matched your search
@@ -118,6 +118,13 @@ export function createSubscriptionAuthorsSource({ service } = {}) {
|
||||
videos = await Promise.resolve(
|
||||
resolvedService.getActiveVideosByAuthors(authorList, options)
|
||||
);
|
||||
|
||||
if (!videos.length && typeof resolvedService.fetchVideosByAuthors === "function") {
|
||||
const fetched = await resolvedService.fetchVideosByAuthors(authorList, options);
|
||||
if (Array.isArray(fetched) && fetched.length > 0) {
|
||||
videos = fetched;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
videos = await Promise.resolve(
|
||||
resolvedService.getFilteredActiveVideos(options)
|
||||
|
||||
@@ -1159,6 +1159,144 @@ export class NostrService {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchVideosByAuthors(authors, options = {}) {
|
||||
const authorList = Array.isArray(authors) ? authors : [];
|
||||
if (!authorList.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const requestedLimit = Number(options?.limit);
|
||||
const resolvedLimit = this.nostrClient.clampVideoRequestLimit(
|
||||
requestedLimit,
|
||||
DEFAULT_VIDEO_REQUEST_LIMIT
|
||||
);
|
||||
|
||||
const filter = {
|
||||
kinds: [VIDEO_KIND],
|
||||
"#t": ["video"],
|
||||
authors: authorList,
|
||||
limit: resolvedLimit,
|
||||
};
|
||||
|
||||
const localAll = new Map();
|
||||
// Track invalid
|
||||
const invalidNotes = [];
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
this.nostrClient.relays.map(async (url) => {
|
||||
const events = await this.nostrClient.pool.list([url], [filter]);
|
||||
for (const evt of events) {
|
||||
if (evt && evt.id) {
|
||||
this.nostrClient.rawEvents.set(evt.id, evt);
|
||||
}
|
||||
const vid = convertEventToVideo(evt);
|
||||
if (vid.invalid) {
|
||||
invalidNotes.push({ id: vid.id, reason: vid.reason });
|
||||
} else {
|
||||
if (
|
||||
this.nostrClient &&
|
||||
typeof this.nostrClient.applyRootCreatedAt === "function"
|
||||
) {
|
||||
this.nostrClient.applyRootCreatedAt(vid);
|
||||
}
|
||||
const activeKey = getActiveKey(vid);
|
||||
if (vid.deleted) {
|
||||
this.recordTombstone(activeKey, vid.created_at);
|
||||
} else {
|
||||
this.applyTombstoneGuard(vid);
|
||||
}
|
||||
localAll.set(evt.id, vid);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Merge into allEvents
|
||||
for (const [id, vid] of localAll.entries()) {
|
||||
this.nostrClient.allEvents.set(id, vid);
|
||||
if (
|
||||
this.nostrClient &&
|
||||
typeof this.nostrClient.applyRootCreatedAt === "function"
|
||||
) {
|
||||
this.nostrClient.applyRootCreatedAt(vid);
|
||||
}
|
||||
}
|
||||
|
||||
// Update activeMap for affected keys
|
||||
for (const [id, video] of localAll.entries()) {
|
||||
if (video.deleted) continue;
|
||||
const activeKey = getActiveKey(video);
|
||||
const existing = this.nostrClient.activeMap.get(activeKey);
|
||||
|
||||
if (!existing || video.created_at > existing.created_at) {
|
||||
this.nostrClient.activeMap.set(activeKey, video);
|
||||
if (
|
||||
this.nostrClient &&
|
||||
typeof this.nostrClient.applyRootCreatedAt === "function"
|
||||
) {
|
||||
this.nostrClient.applyRootCreatedAt(video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-populate cache indexing
|
||||
this.ensureVideosMap();
|
||||
this.markAuthorIndexDirty();
|
||||
|
||||
// Collect the actual videos for the requested authors
|
||||
const collected = [];
|
||||
const seen = new Set();
|
||||
// We look at localAll to ensure we only return what we fetched (or fresh stuff)
|
||||
// Actually, we should probably return the "best" active videos for these authors
|
||||
// now that we've refreshed the cache.
|
||||
const index = this.ensureVideosByAuthorIndex();
|
||||
|
||||
for (const author of authorList) {
|
||||
const entries = index.get(author);
|
||||
if (Array.isArray(entries)) {
|
||||
for (const video of entries) {
|
||||
if (video && video.id && !seen.has(video.id)) {
|
||||
seen.add(video.id);
|
||||
collected.push(video);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = this.filterVideos(collected, options);
|
||||
|
||||
// Sort newest first
|
||||
filtered.sort((a, b) => {
|
||||
const createdA = Number(a?.created_at) || 0;
|
||||
const createdB = Number(b?.created_at) || 0;
|
||||
return createdB - createdA;
|
||||
});
|
||||
|
||||
const finalLimit = resolvedLimit || filtered.length;
|
||||
const limited = filtered.slice(0, finalLimit);
|
||||
|
||||
// Trigger metadata hydration
|
||||
await this.nostrClient.populateNip71MetadataForVideos(limited);
|
||||
limited.forEach((video) => {
|
||||
if (
|
||||
this.nostrClient &&
|
||||
typeof this.nostrClient.applyRootCreatedAt === "function"
|
||||
) {
|
||||
this.nostrClient.applyRootCreatedAt(video);
|
||||
}
|
||||
});
|
||||
|
||||
this.cacheVideos(limited);
|
||||
this.emit("videos:fetched", { videos: limited, context: "authors" });
|
||||
|
||||
return limited;
|
||||
} catch (err) {
|
||||
userLogger.error("[nostrService] fetchVideosByAuthors error:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async loadOlderVideos(lastTimestamp, {
|
||||
blacklistedEventIds,
|
||||
isAuthorBlocked,
|
||||
|
||||
+13
-1
@@ -420,7 +420,19 @@ class SubscriptionsManager {
|
||||
this.loadingPromise = loader;
|
||||
|
||||
try {
|
||||
await loader;
|
||||
// Race against a timeout so the UI doesn't hang indefinitely if relays stall.
|
||||
const timeoutMs = 6000;
|
||||
await Promise.race([
|
||||
loader,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("Timeout loading subscriptions")),
|
||||
timeoutMs
|
||||
)
|
||||
),
|
||||
]);
|
||||
} catch (error) {
|
||||
userLogger.warn("[SubscriptionsManager] ensureLoaded timed out or failed:", error);
|
||||
} finally {
|
||||
this.loadingPromise = null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user