Merge pull request #1189 from PR0M3TH3AN/fix-subscription-spinner-hang-134888602482071504

Fix infinite subscription loading spinner
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-11 19:11:39 -05:00
commit 0bcee151cb
4 files changed
+56 -21

No files matched your search

+7
View File
@@ -66,6 +66,7 @@ export function createSubscriptionAuthorsSource({ service } = {}) {
const resolvedService = resolveService(service, nostrService);
return async function subscriptionAuthorsSource(context = {}) {
context?.log?.("[subscriptions-source] START");
const runtimeAuthors = toArray(
context?.runtime?.subscriptionAuthors || context?.runtime?.authors
).map(normalizeAuthor);
@@ -88,6 +89,8 @@ export function createSubscriptionAuthorsSource({ service } = {}) {
[...runtimeAuthors, ...configAuthors, ...hookAuthors].filter(Boolean)
);
context?.log?.(`[subscriptions-source] Resolved ${authors.size} authors.`);
if (!authors.size) {
return [];
}
@@ -115,12 +118,16 @@ export function createSubscriptionAuthorsSource({ service } = {}) {
let videos = [];
try {
if (hasTargetedLookup) {
context?.log?.("[subscriptions-source] Calling getActiveVideosByAuthors...");
videos = await Promise.resolve(
resolvedService.getActiveVideosByAuthors(authorList, options)
);
context?.log?.(`[subscriptions-source] getActiveVideosByAuthors returned ${videos.length} videos.`);
if (!videos.length && typeof resolvedService.fetchVideosByAuthors === "function") {
context?.log?.("[subscriptions-source] Cache empty. Calling fetchVideosByAuthors...");
const fetched = await resolvedService.fetchVideosByAuthors(authorList, options);
context?.log?.(`[subscriptions-source] fetchVideosByAuthors returned ${fetched ? fetched.length : 0} videos.`);
if (Array.isArray(fetched) && fetched.length > 0) {
videos = fetched;
}
+1
View File
@@ -581,6 +581,7 @@ export function shimLegacySimplePoolMethods(pool) {
: 7000;
timer = setTimeout(() => {
devLogger.warn(`[toolkit] pool.list timed out after ${timeoutMs}ms.`);
timer = null;
try {
subscription.unsub?.();
+40 -21
View File
@@ -157,6 +157,17 @@ function extractSnapshotPreview(message) {
return sanitizeSnapshotPreview(selected || "");
}
function getActiveKey(video) {
if (video.videoRootId) {
return `ROOT:${video.videoRootId}`;
}
const dTag = video.tags?.find((t) => Array.isArray(t) && t[0] === "d");
if (dTag && typeof dTag[1] === "string") {
return `${video.pubkey}:${dTag[1]}`;
}
return `LEGACY:${video.id}`;
}
function resolveDirectMessageRemotePubkey(message, actorPubkey = "") {
const normalizedActor = normalizeHexPubkey(actorPubkey);
@@ -1165,10 +1176,11 @@ export class NostrService {
return [];
}
this.log(`[nostrService] fetchVideosByAuthors START. Authors: ${authorList.length}`);
const requestedLimit = Number(options?.limit);
const resolvedLimit = this.nostrClient.clampVideoRequestLimit(
requestedLimit,
DEFAULT_VIDEO_REQUEST_LIMIT
requestedLimit
);
const filter = {
@@ -1183,31 +1195,38 @@ export class NostrService {
const invalidNotes = [];
try {
this.log(`[nostrService] Querying ${this.nostrClient.relays.length} relays...`);
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);
try {
this.log(`[nostrService] Querying relay: ${url}`);
const events = await this.nostrClient.pool.list([url], [filter]);
this.log(`[nostrService] Relay ${url} returned ${events.length} events.`);
for (const evt of events) {
if (evt && evt.id) {
this.nostrClient.rawEvents.set(evt.id, evt);
}
const activeKey = getActiveKey(vid);
if (vid.deleted) {
this.recordTombstone(activeKey, vid.created_at);
const vid = convertEventToVideo(evt);
if (vid.invalid) {
invalidNotes.push({ id: vid.id, reason: vid.reason });
} else {
this.applyTombstoneGuard(vid);
if (
this.nostrClient &&
typeof this.nostrClient.applyRootCreatedAt === "function"
) {
this.nostrClient.applyRootCreatedAt(vid);
}
const activeKey = getActiveKey(vid);
if (vid.deleted) {
this.nostrClient.recordTombstone(activeKey, vid.created_at);
} else {
this.nostrClient.applyTombstoneGuard(vid);
}
localAll.set(evt.id, vid);
}
localAll.set(evt.id, vid);
}
} catch (relayErr) {
this.log(`[nostrService] Relay ${url} failed:`, relayErr);
}
})
);
+8
View File
@@ -398,17 +398,20 @@ class SubscriptionsManager {
}
async ensureLoaded(actorHex) {
devLogger.log("[SubscriptionsManager] ensureLoaded start", actorHex);
const normalizedActor = normalizeHexPubkey(actorHex) || actorHex;
if (!normalizedActor) {
return;
}
if (this.loaded && this.currentUserPubkey === normalizedActor) {
devLogger.log("[SubscriptionsManager] ensureLoaded already loaded");
return;
}
if (this.loadingPromise) {
try {
devLogger.log("[SubscriptionsManager] ensureLoaded awaiting existing promise");
await this.loadingPromise;
} catch (error) {
throw error;
@@ -431,6 +434,7 @@ class SubscriptionsManager {
)
),
]);
devLogger.log("[SubscriptionsManager] ensureLoaded success");
} catch (error) {
userLogger.warn("[SubscriptionsManager] ensureLoaded timed out or failed:", error);
} finally {
@@ -870,7 +874,9 @@ class SubscriptionsManager {
if (typeof nostrService?.awaitInitialLoad === "function") {
try {
devLogger.log("[SubscriptionsManager] awaiting nostrService initial load...");
await nostrService.awaitInitialLoad();
devLogger.log("[SubscriptionsManager] nostrService initial load done.");
} catch (error) {
devLogger.warn(
"[SubscriptionsManager] Failed to await nostrService initial load:",
@@ -906,7 +912,9 @@ class SubscriptionsManager {
try {
this.isRunningFeed = true;
devLogger.log("[SubscriptionsManager] Calling engine.run('subscriptions')...");
const result = await engine.run("subscriptions", runOptions);
devLogger.log("[SubscriptionsManager] engine.run complete. Items:", result?.items?.length);
const videos = Array.isArray(result?.items)
? result.items.map((item) => item?.video).filter(Boolean)