Browse Source

speed up performance

master
Silberengel 1 month ago
parent
commit
d0c9666304
  1. 69
      src/lib/modules/comments/CommentThread.svelte
  2. 91
      src/lib/modules/reactions/FeedReactionButtons.svelte
  3. 108
      src/lib/services/nostr/auth-handler.ts
  4. 129
      src/lib/services/nostr/relay-manager.ts

69
src/lib/modules/comments/CommentThread.svelte

@ -238,13 +238,13 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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(

91
src/lib/modules/reactions/FeedReactionButtons.svelte

@ -32,8 +32,15 @@ @@ -32,8 +32,15 @@
let loadingReactions = $state(false);
let lastEventId = $state<string | null>(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 @@ @@ -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();
// Only run after mount and when event.id actually changes
if (!isMounted || !event.id || event.id === lastEventId || loadingReactions) {
return;
}
// 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();
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 @@ @@ -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 @@ @@ -190,12 +200,21 @@
async function filterDeletedReactions(reactions: NostrEvent[]): Promise<NostrEvent[]> {
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 @@ @@ -208,40 +227,24 @@
}))
});
// Build a set of deleted reaction event IDs (keyed by pubkey)
const deletedReactionIdsByPubkey = new Map<string, Set<string>>();
// Build a set of deleted reaction event IDs (more efficient - just a Set)
const deletedReactionIds = new Set<string>();
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;
});

108
src/lib/services/nostr/auth-handler.ts

@ -33,8 +33,14 @@ export async function authenticateWithNIP07(): Promise<string> { @@ -33,8 +33,14 @@ export async function authenticateWithNIP07(): Promise<string> {
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<void>((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<string> @@ -269,42 +275,72 @@ export async function authenticateAsAnonymous(password: string): Promise<string>
* Load user preferences (relay lists, mute list, blocked relays)
*/
async function loadUserPreferences(pubkey: string): Promise<void> {
// 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<void>((_, 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<NostrEvent[]>((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<NostrEvent[]>((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');
}
}

129
src/lib/services/nostr/relay-manager.ts

@ -9,6 +9,7 @@ import { config } from './config.js'; @@ -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 { @@ -21,67 +22,89 @@ class RelayManager {
* Load user relay preferences
*/
async loadUserPreferences(pubkey: string): Promise<void> {
// 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<NostrEvent[]>((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();

Loading…
Cancel
Save