From d0c96663041dd4dfe8d3c79436728b74990e1722 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 5 Feb 2026 08:40:57 +0100 Subject: [PATCH] speed up performance --- src/lib/modules/comments/CommentThread.svelte | 69 +++++----- .../reactions/FeedReactionButtons.svelte | 93 +++++++------ src/lib/services/nostr/auth-handler.ts | 108 ++++++++++----- src/lib/services/nostr/relay-manager.ts | 129 +++++++++++------- 4 files changed, 234 insertions(+), 165 deletions(-) diff --git a/src/lib/modules/comments/CommentThread.svelte b/src/lib/modules/comments/CommentThread.svelte index 9f727c9..4d27ffb 100644 --- a/src/lib/modules/comments/CommentThread.svelte +++ b/src/lib/modules/comments/CommentThread.svelte @@ -238,13 +238,13 @@ const allRelays = relayManager.getProfileReadRelays(); const replyFilters: any[] = [ - { kinds: [KIND.COMMENT], '#e': [threadId] }, - { kinds: [KIND.COMMENT], '#E': [threadId] }, - { kinds: [KIND.COMMENT], '#a': [threadId] }, - { kinds: [KIND.COMMENT], '#A': [threadId] }, - { kinds: [KIND.SHORT_TEXT_NOTE], '#e': [threadId] }, - { kinds: [KIND.VOICE_REPLY], '#e': [threadId] }, - { kinds: [KIND.ZAP_RECEIPT], '#e': [threadId] } + { kinds: [KIND.COMMENT], '#e': [threadId], limit: 500 }, + { kinds: [KIND.COMMENT], '#E': [threadId], limit: 500 }, + { kinds: [KIND.COMMENT], '#a': [threadId], limit: 500 }, + { kinds: [KIND.COMMENT], '#A': [threadId], limit: 500 }, + { kinds: [KIND.SHORT_TEXT_NOTE], '#e': [threadId], limit: 500 }, + { kinds: [KIND.VOICE_REPLY], '#e': [threadId], limit: 500 }, + { kinds: [KIND.ZAP_RECEIPT], '#e': [threadId], limit: 500 } ]; // fetchEvents with useCache:true returns cached data immediately if available, @@ -319,13 +319,16 @@ nestedSubscriptionActive = true; + // Limit reply IDs to prevent massive subscriptions + const limitedReplyIds = Array.from(allReplyIds).slice(0, 100); + // Use a single subscription that covers all reply IDs const nestedFilters: any[] = [ - { kinds: [KIND.COMMENT], '#e': Array.from(allReplyIds) }, - { kinds: [KIND.COMMENT], '#E': Array.from(allReplyIds) }, - { kinds: [KIND.SHORT_TEXT_NOTE], '#e': Array.from(allReplyIds) }, - { kinds: [KIND.VOICE_REPLY], '#e': Array.from(allReplyIds) }, - { kinds: [KIND.ZAP_RECEIPT], '#e': Array.from(allReplyIds) } + { kinds: [KIND.COMMENT], '#e': limitedReplyIds, limit: 200 }, + { kinds: [KIND.COMMENT], '#E': limitedReplyIds, limit: 200 }, + { kinds: [KIND.SHORT_TEXT_NOTE], '#e': limitedReplyIds, limit: 200 }, + { kinds: [KIND.VOICE_REPLY], '#e': limitedReplyIds, limit: 200 }, + { kinds: [KIND.ZAP_RECEIPT], '#e': limitedReplyIds, limit: 200 } ]; nostrClient.fetchEvents( @@ -347,37 +350,41 @@ const allRelays = relayManager.getProfileReadRelays(); let hasNewReplies = true; let iterations = 0; - const maxIterations = 10; + const maxIterations = 3; // Reduced from 10 to prevent excessive fetching + const maxReplyIdsPerIteration = 100; // Limit number of reply IDs to check per iteration while (hasNewReplies && iterations < maxIterations) { iterations++; hasNewReplies = false; - const allReplyIds = new Set([ + const allReplyIds = Array.from(new Set([ ...comments.map(c => c.id), ...kind1Replies.map(r => r.id), ...yakBacks.map(y => y.id), ...zapReceipts.map(z => z.id) - ]); + ])); + + // Limit the number of reply IDs to prevent massive queries + const limitedReplyIds = allReplyIds.slice(0, maxReplyIdsPerIteration); - if (allReplyIds.size > 0) { + if (limitedReplyIds.length > 0) { const nestedFilters: any[] = [ // Fetch nested kind 1111 comments - check both e/E and a/A tags - { kinds: [KIND.COMMENT], '#e': Array.from(allReplyIds) }, - { kinds: [KIND.COMMENT], '#E': Array.from(allReplyIds) }, - { kinds: [KIND.COMMENT], '#a': Array.from(allReplyIds) }, - { kinds: [KIND.COMMENT], '#A': Array.from(allReplyIds) }, + { kinds: [KIND.COMMENT], '#e': limitedReplyIds, limit: 200 }, + { kinds: [KIND.COMMENT], '#E': limitedReplyIds, limit: 200 }, + { kinds: [KIND.COMMENT], '#a': limitedReplyIds, limit: 200 }, + { kinds: [KIND.COMMENT], '#A': limitedReplyIds, limit: 200 }, // Fetch nested kind 1 replies - { kinds: [KIND.SHORT_TEXT_NOTE], '#e': Array.from(allReplyIds) }, + { kinds: [KIND.SHORT_TEXT_NOTE], '#e': limitedReplyIds, limit: 200 }, // Fetch nested yak backs - { kinds: [KIND.VOICE_REPLY], '#e': Array.from(allReplyIds) }, + { kinds: [KIND.VOICE_REPLY], '#e': limitedReplyIds, limit: 200 }, // Fetch nested zap receipts - { kinds: [KIND.ZAP_RECEIPT], '#e': Array.from(allReplyIds) } + { kinds: [KIND.ZAP_RECEIPT], '#e': limitedReplyIds, limit: 200 } ]; const nestedReplies = await nostrClient.fetchEvents( nestedFilters, allRelays, - { useCache: true, cacheResults: true } + { useCache: true, cacheResults: true, timeout: 5000 } ); // Add new replies by type @@ -537,21 +544,21 @@ // Always fetch kind 1111 comments - check both e and E tags, and a and A tags replyFilters.push( - { kinds: [KIND.COMMENT], '#e': [threadId] }, // Lowercase e tag - { kinds: [KIND.COMMENT], '#E': [threadId] }, // Uppercase E tag (NIP-22) - { kinds: [KIND.COMMENT], '#a': [threadId] }, // Lowercase a tag (some clients use wrong tags) - { kinds: [KIND.COMMENT], '#A': [threadId] } // Uppercase A tag (NIP-22 for addressable events) + { kinds: [KIND.COMMENT], '#e': [threadId], limit: 500 }, // Lowercase e tag + { kinds: [KIND.COMMENT], '#E': [threadId], limit: 500 }, // Uppercase E tag (NIP-22) + { kinds: [KIND.COMMENT], '#a': [threadId], limit: 500 }, // Lowercase a tag (some clients use wrong tags) + { kinds: [KIND.COMMENT], '#A': [threadId], limit: 500 } // Uppercase A tag (NIP-22 for addressable events) ); // For kind 1 events, fetch kind 1 replies // Also fetch kind 1 replies for any event (some apps use kind 1 for everything) - replyFilters.push({ kinds: [KIND.SHORT_TEXT_NOTE], '#e': [threadId] }); + replyFilters.push({ kinds: [KIND.SHORT_TEXT_NOTE], '#e': [threadId], limit: 500 }); // Fetch yak backs (kind 1244) - voice replies - replyFilters.push({ kinds: [KIND.VOICE_REPLY], '#e': [threadId] }); + replyFilters.push({ kinds: [KIND.VOICE_REPLY], '#e': [threadId], limit: 500 }); // Fetch zap receipts (kind 9735) - replyFilters.push({ kinds: [KIND.ZAP_RECEIPT], '#e': [threadId] }); + replyFilters.push({ kinds: [KIND.ZAP_RECEIPT], '#e': [threadId], limit: 500 }); // Don't use cache when reloading after publishing - we want fresh data const allReplies = await nostrClient.fetchEvents( diff --git a/src/lib/modules/reactions/FeedReactionButtons.svelte b/src/lib/modules/reactions/FeedReactionButtons.svelte index 5543772..42e8b4b 100644 --- a/src/lib/modules/reactions/FeedReactionButtons.svelte +++ b/src/lib/modules/reactions/FeedReactionButtons.svelte @@ -32,8 +32,15 @@ let loadingReactions = $state(false); let lastEventId = $state(null); + let isMounted = $state(false); onMount(() => { + // Set lastEventId immediately to prevent $effect from running during mount + if (event.id) { + lastEventId = event.id; + } + isMounted = true; + nostrClient.initialize().then(async () => { if (event.id) { // Use pre-loaded reactions if available, otherwise fetch @@ -50,24 +57,27 @@ }); }); - // Reload reactions when event changes (but prevent duplicate loads) + // Reload reactions when event changes (but prevent duplicate loads and initial mount) $effect(() => { - if (event.id && event.id !== lastEventId && !loadingReactions) { - lastEventId = event.id; - // Clear previous reactions map when event changes - allReactionsMap.clear(); - - // Use pre-loaded reactions if available, otherwise fetch - if (preloadedReactions && preloadedReactions.length > 0) { - for (const r of preloadedReactions) { - allReactionsMap.set(r.id, r); - } - filterDeletedReactions(preloadedReactions).then(filtered => { - processReactions(filtered); - }); - } else { - loadReactions(); + // Only run after mount and when event.id actually changes + if (!isMounted || !event.id || event.id === lastEventId || loadingReactions) { + return; + } + + lastEventId = event.id; + // Clear previous reactions map when event changes + allReactionsMap.clear(); + + // Use pre-loaded reactions if available, otherwise fetch + if (preloadedReactions && preloadedReactions.length > 0) { + for (const r of preloadedReactions) { + allReactionsMap.set(r.id, r); } + filterDeletedReactions(preloadedReactions).then(filtered => { + processReactions(filtered); + }); + } else { + loadReactions(); } }); @@ -112,14 +122,14 @@ allReactionsMap.clear(); const reactionsWithLowerE = await nostrClient.fetchEvents( - [{ kinds: [KIND.REACTION], '#e': [event.id] }], + [{ kinds: [KIND.REACTION], '#e': [event.id], limit: 100 }], reactionRelays, - { useCache: true, cacheResults: true, onUpdate: handleReactionUpdate } + { useCache: true, cacheResults: true, onUpdate: handleReactionUpdate, timeout: 5000 } ); const reactionsWithUpperE = await nostrClient.fetchEvents( - [{ kinds: [KIND.REACTION], '#E': [event.id] }], + [{ kinds: [KIND.REACTION], '#E': [event.id], limit: 100 }], reactionRelays, - { useCache: true, cacheResults: true, onUpdate: handleReactionUpdate } + { useCache: true, cacheResults: true, onUpdate: handleReactionUpdate, timeout: 5000 } ); console.debug(`[FeedReactionButtons] Reactions fetched:`, { @@ -190,12 +200,21 @@ async function filterDeletedReactions(reactions: NostrEvent[]): Promise { if (reactions.length === 0) return reactions; - // Fetch deletion events (kind 5) to filter out deleted reactions + // Optimize: Instead of fetching all deletion events for all users, + // fetch deletion events that reference the specific reaction IDs we have + // This is much more efficient and limits memory usage const reactionRelays = relayManager.getProfileReadRelays(); + const reactionIds = reactions.map(r => r.id); + + // Limit to first 100 reactions to avoid massive queries + const limitedReactionIds = reactionIds.slice(0, 100); + + // Fetch deletion events that reference these specific reaction IDs + // This is much more efficient than fetching all deletion events from all users const deletionEvents = await nostrClient.fetchEvents( - [{ kinds: [KIND.EVENT_DELETION], authors: Array.from(new Set(reactions.map(r => r.pubkey))) }], + [{ kinds: [KIND.EVENT_DELETION], '#e': limitedReactionIds, limit: 100 }], reactionRelays, - { useCache: true } + { useCache: true, timeout: 5000 } ); console.debug(`[FeedReactionButtons] Deletion events fetched:`, { @@ -208,40 +227,24 @@ })) }); - // Build a set of deleted reaction event IDs (keyed by pubkey) - const deletedReactionIdsByPubkey = new Map>(); + // Build a set of deleted reaction event IDs (more efficient - just a Set) + const deletedReactionIds = new Set(); for (const deletionEvent of deletionEvents) { - const pubkey = deletionEvent.pubkey; - if (!deletedReactionIdsByPubkey.has(pubkey)) { - deletedReactionIdsByPubkey.set(pubkey, new Set()); - } // Kind 5 events have 'e' tags pointing to deleted events for (const tag of deletionEvent.tags) { if (tag[0] === 'e' && tag[1]) { - deletedReactionIdsByPubkey.get(pubkey)!.add(tag[1]); + deletedReactionIds.add(tag[1]); } } } console.debug(`[FeedReactionButtons] Deleted reaction IDs by pubkey:`, - Array.from(deletedReactionIdsByPubkey.entries()).map(([pubkey, ids]) => ({ - pubkey: pubkey.substring(0, 16) + '...', - deletedIds: Array.from(ids).map(id => id.substring(0, 16) + '...') - })) + Array.from(deletedReactionIds).slice(0, 10).map(id => id.substring(0, 16) + '...') ); - // Filter out deleted reactions + // Filter out deleted reactions - much simpler now const filtered = reactions.filter(reaction => { - const deletedIds = deletedReactionIdsByPubkey.get(reaction.pubkey); - const isDeleted = deletedIds && deletedIds.has(reaction.id); - if (isDeleted) { - console.debug(`[FeedReactionButtons] Filtering out deleted reaction:`, { - id: reaction.id.substring(0, 16) + '...', - pubkey: reaction.pubkey.substring(0, 16) + '...', - content: reaction.content, - fullEvent: reaction - }); - } + const isDeleted = deletedReactionIds.has(reaction.id); return !isDeleted; }); diff --git a/src/lib/services/nostr/auth-handler.ts b/src/lib/services/nostr/auth-handler.ts index 4282cd0..80c9060 100644 --- a/src/lib/services/nostr/auth-handler.ts +++ b/src/lib/services/nostr/auth-handler.ts @@ -33,8 +33,14 @@ export async function authenticateWithNIP07(): Promise { createdAt: Date.now() }, {}); // No metadata needed for NIP-07 - // Fetch user relay lists and mute list - await loadUserPreferences(pubkey); + // Fetch user relay lists and mute list in background with timeout + // Don't block login if relays are slow or unavailable + Promise.race([ + loadUserPreferences(pubkey), + new Promise((resolve) => setTimeout(resolve, 5000)) // 5 second timeout + ]).catch(() => { + // Silently fail - preference loading errors shouldn't break login + }); // Fetch and cache user's own profile (background-update if already cached) fetchProfile(pubkey).catch(() => { @@ -269,42 +275,72 @@ export async function authenticateAsAnonymous(password: string): Promise * Load user preferences (relay lists, mute list, blocked relays) */ async function loadUserPreferences(pubkey: string): Promise { - // Fetch relay lists and load into relay manager - await relayManager.loadUserPreferences(pubkey); - - // Fetch mute list (kind 10000) - const muteEvents = await nostrClient.fetchEvents( - [{ kinds: [KIND.MUTE_LIST], authors: [pubkey], limit: 1 }], - relayManager.getProfileReadRelays(), - { useCache: true, cacheResults: true } - ); - - if (muteEvents.length > 0) { - const mutedPubkeys = muteEvents[0].tags - .filter((t) => t[0] === 'p') - .map((t) => t[1]) - .filter(Boolean) as string[]; - muteList.clear(); - mutedPubkeys.forEach(pk => muteList.add(pk)); + try { + // Fetch relay lists and load into relay manager with timeout + await Promise.race([ + relayManager.loadUserPreferences(pubkey), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Relay list fetch timeout')), 5000) + ) + ]); + } catch (error) { + // If relay list fetch fails, continue with default relays + console.debug('[auth-handler] Failed to load user relay preferences, using defaults'); + } + + try { + // Fetch mute list (kind 10000) with timeout + const muteEvents = await Promise.race([ + nostrClient.fetchEvents( + [{ kinds: [KIND.MUTE_LIST], authors: [pubkey], limit: 1 }], + relayManager.getProfileReadRelays(), + { useCache: true, cacheResults: true, timeout: 5000 } + ), + new Promise((resolve) => + setTimeout(() => resolve([]), 5000) + ) + ]); + + if (muteEvents.length > 0) { + const mutedPubkeys = muteEvents[0].tags + .filter((t) => t[0] === 'p') + .map((t) => t[1]) + .filter(Boolean) as string[]; + muteList.clear(); + mutedPubkeys.forEach(pk => muteList.add(pk)); + } + } catch (error) { + // Silently fail - mute list fetch errors shouldn't break login + console.debug('[auth-handler] Failed to load mute list'); } - // Fetch blocked relays (kind 10006) - const blockedRelayEvents = await nostrClient.fetchEvents( - [{ kinds: [KIND.BLOCKED_RELAYS], authors: [pubkey], limit: 1 }], - relayManager.getProfileReadRelays(), - { useCache: true, cacheResults: true } - ); - - if (blockedRelayEvents.length > 0) { - const blocked = blockedRelayEvents[0].tags - .filter((t) => t[0] === 'relay') - .map((t) => t[1]) - .filter(Boolean) as string[]; - blockedRelays.clear(); - blocked.forEach(r => blockedRelays.add(r)); - - // Update relay manager with blocked relays - relayManager.updateBlockedRelays(blockedRelays); + try { + // Fetch blocked relays (kind 10006) with timeout + const blockedRelayEvents = await Promise.race([ + nostrClient.fetchEvents( + [{ kinds: [KIND.BLOCKED_RELAYS], authors: [pubkey], limit: 1 }], + relayManager.getProfileReadRelays(), + { useCache: true, cacheResults: true, timeout: 5000 } + ), + new Promise((resolve) => + setTimeout(() => resolve([]), 5000) + ) + ]); + + if (blockedRelayEvents.length > 0) { + const blocked = blockedRelayEvents[0].tags + .filter((t) => t[0] === 'relay') + .map((t) => t[1]) + .filter(Boolean) as string[]; + blockedRelays.clear(); + blocked.forEach(r => blockedRelays.add(r)); + + // Update relay manager with blocked relays + relayManager.updateBlockedRelays(blockedRelays); + } + } catch (error) { + // Silently fail - blocked relay fetch errors shouldn't break login + console.debug('[auth-handler] Failed to load blocked relays'); } } diff --git a/src/lib/services/nostr/relay-manager.ts b/src/lib/services/nostr/relay-manager.ts index 90afb12..1fe695c 100644 --- a/src/lib/services/nostr/relay-manager.ts +++ b/src/lib/services/nostr/relay-manager.ts @@ -9,6 +9,7 @@ import { config } from './config.js'; import { sessionManager } from '../auth/session-manager.js'; import { nostrClient } from './nostr-client.js'; import { KIND } from '../../types/kind-lookup.js'; +import type { NostrEvent } from '../../types/nostr.js'; class RelayManager { private userInbox: string[] = []; @@ -21,67 +22,89 @@ class RelayManager { * Load user relay preferences */ async loadUserPreferences(pubkey: string): Promise { - // Fetch relay lists (includes both kind 10002 and 10432) - const { inbox, outbox } = await fetchRelayLists(pubkey); - this.userInbox = inbox; - this.userOutbox = outbox; - - // Also fetch local relays separately to track read/write indicators - // Local relays are used as external cache - const relayList = [ - ...config.defaultRelays, - ...config.profileRelays - ]; - const localRelayEvents = await nostrClient.fetchEvents( - [{ kinds: [KIND.LOCAL_RELAYS], authors: [pubkey], limit: 1 }], - relayList, - { useCache: true, cacheResults: true } - ); - - const localRelaysRead: string[] = []; - const localRelaysWrite: string[] = []; - - for (const event of localRelayEvents) { - for (const tag of event.tags) { - if (tag[0] === 'r' && tag[1]) { - const url = tag[1].trim(); - if (!url) continue; - - const markers = tag.slice(2); - - // If no markers, relay is both read and write - if (markers.length === 0) { - if (!localRelaysRead.includes(url)) { - localRelaysRead.push(url); - } - if (!localRelaysWrite.includes(url)) { - localRelaysWrite.push(url); + try { + // Fetch relay lists (includes both kind 10002 and 10432) with timeout + const { inbox, outbox } = await Promise.race([ + fetchRelayLists(pubkey), + new Promise<{ inbox: string[]; outbox: string[] }>((resolve) => + setTimeout(() => resolve({ inbox: [], outbox: [] }), 5000) + ) + ]); + this.userInbox = inbox; + this.userOutbox = outbox; + } catch (error) { + // If relay list fetch fails, use empty lists (default relays will be used) + this.userInbox = []; + this.userOutbox = []; + } + + try { + // Also fetch local relays separately to track read/write indicators + // Local relays are used as external cache + const relayList = [ + ...config.defaultRelays, + ...config.profileRelays + ]; + const localRelayEvents = await Promise.race([ + nostrClient.fetchEvents( + [{ kinds: [KIND.LOCAL_RELAYS], authors: [pubkey], limit: 1 }], + relayList, + { useCache: true, cacheResults: true, timeout: 5000 } + ), + new Promise((resolve) => + setTimeout(() => resolve([]), 5000) + ) + ]); + + const localRelaysRead: string[] = []; + const localRelaysWrite: string[] = []; + + for (const event of localRelayEvents) { + for (const tag of event.tags) { + if (tag[0] === 'r' && tag[1]) { + const url = tag[1].trim(); + if (!url) continue; + + const markers = tag.slice(2); + + // If no markers, relay is both read and write + if (markers.length === 0) { + if (!localRelaysRead.includes(url)) { + localRelaysRead.push(url); + } + if (!localRelaysWrite.includes(url)) { + localRelaysWrite.push(url); + } + continue; } - continue; - } - // Check for explicit markers - const hasRead = markers.includes('read'); - const hasWrite = markers.includes('write'); + // Check for explicit markers + const hasRead = markers.includes('read'); + const hasWrite = markers.includes('write'); - // Determine read/write permissions - // If only 'read' marker: read=true, write=false - // If only 'write' marker: read=false, write=true - // If both or neither: both true (default behavior) - const read = hasRead || (!hasRead && !hasWrite); - const write = hasWrite || (!hasRead && !hasWrite); + // Determine read/write permissions + // If only 'read' marker: read=true, write=false + // If only 'write' marker: read=false, write=true + // If both or neither: both true (default behavior) + const read = hasRead || (!hasRead && !hasWrite); + const write = hasWrite || (!hasRead && !hasWrite); - if (read && !localRelaysRead.includes(url)) { - localRelaysRead.push(url); - } - if (write && !localRelaysWrite.includes(url)) { - localRelaysWrite.push(url); + if (read && !localRelaysRead.includes(url)) { + localRelaysRead.push(url); + } + if (write && !localRelaysWrite.includes(url)) { + localRelaysWrite.push(url); + } } } } + this.userLocalRelaysRead = localRelaysRead; + this.userLocalRelaysWrite = localRelaysWrite; + } catch (error) { + // If local relay fetch fails, use empty lists + this.userLocalRelaysRead = []; + this.userLocalRelaysWrite = []; } - this.userLocalRelaysRead = localRelaysRead; - this.userLocalRelaysWrite = localRelaysWrite; // Get blocked relays this.blockedRelays = getBlockedRelays();