Browse Source

more performance fixes

master
Silberengel 1 month ago
parent
commit
a9cf35019c
  1. 4
      public/healthz.json
  2. 18
      src/lib/modules/comments/CommentThread.svelte
  3. 140
      src/lib/modules/discussions/DiscussionList.svelte
  4. 49
      src/lib/modules/profiles/ProfilePage.svelte
  5. 27
      src/lib/services/cache/event-cache.ts
  6. 73
      src/routes/highlights/+page.svelte
  7. 62
      src/routes/lists/+page.svelte
  8. 39
      src/routes/repos/+page.svelte
  9. 38
      src/routes/topics/[name]/+page.svelte

4
public/healthz.json

@ -2,7 +2,7 @@
"status": "ok", "status": "ok",
"service": "aitherboard", "service": "aitherboard",
"version": "0.2.0", "version": "0.2.0",
"buildTime": "2026-02-07T06:54:31.135Z", "buildTime": "2026-02-07T07:05:23.594Z",
"gitCommit": "unknown", "gitCommit": "unknown",
"timestamp": 1770447271135 "timestamp": 1770447923594
} }

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

@ -300,20 +300,18 @@
} }
// Load from cache first (fast - instant display) // Load from cache first (fast - instant display)
// Optimized: Batch all kinds into single cache query
try { try {
const { getRecentCachedEvents } = await import('../../services/cache/event-cache.js'); const { getRecentCachedEvents } = await import('../../services/cache/event-cache.js');
const cachedComments = await getRecentCachedEvents([KIND.COMMENT], 60 * 60 * 1000, config.feedLimit); // 1 hour cache // Batch all kinds into one call (optimized in getRecentCachedEvents to use single transaction)
const cachedKind1 = await getRecentCachedEvents([KIND.SHORT_TEXT_NOTE], 60 * 60 * 1000, config.feedLimit); const allCached = await getRecentCachedEvents(
const cachedYakBacks = await getRecentCachedEvents([KIND.VOICE_REPLY], 60 * 60 * 1000, config.feedLimit); [KIND.COMMENT, KIND.SHORT_TEXT_NOTE, KIND.VOICE_REPLY, KIND.ZAP_RECEIPT],
const cachedZaps = await getRecentCachedEvents([KIND.ZAP_RECEIPT], 60 * 60 * 1000, config.feedLimit); 60 * 60 * 1000,
config.feedLimit * 4 // Get more since we're filtering by thread
);
// Filter cached events to only those that reference this thread // Filter cached events to only those that reference this thread
const cachedReplies = [ const cachedReplies = allCached.filter(r => referencesRoot(r));
...cachedComments.filter(r => referencesRoot(r)),
...cachedKind1.filter(r => referencesRoot(r)),
...cachedYakBacks.filter(r => referencesRoot(r)),
...cachedZaps.filter(r => referencesRoot(r))
];
if (cachedReplies.length > 0 && isMounted) { if (cachedReplies.length > 0 && isMounted) {
// Process cached replies immediately // Process cached replies immediately

140
src/lib/modules/discussions/DiscussionList.svelte

@ -297,51 +297,29 @@
if (!isMounted) return; if (!isMounted) return;
// Fetch reactions with lowercase e // Optimized: Fetch reactions with both #e and #E in single call (most relays support both)
const reactionsFetchPromise1 = nostrClient.fetchEvents( // If a relay rejects #E, it will just return empty results for that filter
[{ kinds: [KIND.REACTION], '#e': threadIds, limit: config.feedLimit }], const reactionsFetchPromise = nostrClient.fetchEvents(
[
{ kinds: [KIND.REACTION], '#e': threadIds, limit: config.feedLimit },
{ kinds: [KIND.REACTION], '#E': threadIds, limit: config.feedLimit }
],
reactionRelays, reactionRelays,
{ {
useCache: 'relay-first', useCache: 'cache-first', // Changed from relay-first for better performance
cacheResults: true, cacheResults: true,
timeout: config.standardTimeout, timeout: config.standardTimeout,
onUpdate: handleReactionUpdate onUpdate: handleReactionUpdate
} }
); );
activeFetchPromises.add(reactionsFetchPromise1); activeFetchPromises.add(reactionsFetchPromise);
const reactionsWithLowerE = await reactionsFetchPromise1; const allReactions = await reactionsFetchPromise;
activeFetchPromises.delete(reactionsFetchPromise1); activeFetchPromises.delete(reactionsFetchPromise);
if (!isMounted) return; if (!isMounted) return;
// Try uppercase filter // Add all reactions to map (deduplication handled by Map)
let reactionsWithUpperE: NostrEvent[] = []; for (const r of allReactions) {
try {
const reactionsFetchPromise2 = nostrClient.fetchEvents(
[{ kinds: [KIND.REACTION], '#E': threadIds, limit: config.feedLimit }],
reactionRelays,
{
useCache: 'relay-first',
cacheResults: true,
timeout: config.standardTimeout,
onUpdate: handleReactionUpdate
}
);
activeFetchPromises.add(reactionsFetchPromise2);
reactionsWithUpperE = await reactionsFetchPromise2;
activeFetchPromises.delete(reactionsFetchPromise2);
if (!isMounted) return;
} catch (error) {
if (isMounted) {
console.log('[DiscussionList] Upper case #E filter rejected by relay (this is normal):', error);
}
}
// Combine reactions
for (const r of reactionsWithLowerE) {
allReactionsMap.set(r.id, r);
}
for (const r of reactionsWithUpperE) {
allReactionsMap.set(r.id, r); allReactionsMap.set(r.id, r);
} }
@ -352,65 +330,71 @@
updateVoteCountsMap(); updateVoteCountsMap();
voteCountsReady = true; voteCountsReady = true;
// Fetch zap receipts (for sorting) // Optimized: Fetch zaps and comments in parallel (they're independent)
if (!isMounted) return; if (!isMounted) return;
const zapFetchPromise = nostrClient.fetchEvents( const zapFetchPromise = nostrClient.fetchEvents(
[{ kinds: [KIND.ZAP_RECEIPT], '#e': threadIds, limit: config.feedLimit }], [{ kinds: [KIND.ZAP_RECEIPT], '#e': threadIds, limit: config.feedLimit }],
zapRelays, zapRelays,
{ useCache: 'relay-first', cacheResults: true, timeout: config.standardTimeout } { useCache: 'cache-first', cacheResults: true, timeout: config.standardTimeout }
); );
activeFetchPromises.add(zapFetchPromise);
const allZapReceipts = await zapFetchPromise;
activeFetchPromises.delete(zapFetchPromise);
if (!isMounted) return;
// Group zap receipts by thread ID (for sorting)
const newZapReceiptsMap = new Map<string, NostrEvent[]>();
for (const zapReceipt of allZapReceipts) {
const threadId = zapReceipt.tags.find((t: string[]) => t[0] === 'e')?.[1];
if (threadId && threadsMap.has(threadId)) {
if (!newZapReceiptsMap.has(threadId)) {
newZapReceiptsMap.set(threadId, []);
}
newZapReceiptsMap.get(threadId)!.push(zapReceipt);
}
}
zapReceiptsMap = newZapReceiptsMap;
// Batch-load comment counts for all threads
if (!isMounted) return;
const commentsFetchPromise = nostrClient.fetchEvents( const commentsFetchPromise = nostrClient.fetchEvents(
[{ kinds: [KIND.COMMENT], '#E': threadIds, '#K': ['11'], limit: config.feedLimit }], [{ kinds: [KIND.COMMENT], '#E': threadIds, '#K': ['11'], limit: config.feedLimit }],
commentRelays, commentRelays,
{ useCache: 'relay-first', cacheResults: true, timeout: config.standardTimeout, priority: 'low' } { useCache: 'cache-first', cacheResults: true, timeout: config.standardTimeout, priority: 'low' }
); );
// Track both promises for cleanup
activeFetchPromises.add(zapFetchPromise);
activeFetchPromises.add(commentsFetchPromise); activeFetchPromises.add(commentsFetchPromise);
const allComments = await commentsFetchPromise;
activeFetchPromises.delete(commentsFetchPromise);
if (!isMounted) return; try {
const [allZapReceipts, allComments] = await Promise.all([
zapFetchPromise,
commentsFetchPromise
]);
// Count comments per thread if (!isMounted) return;
const newCommentsMap = new Map<string, number>();
for (const comment of allComments) {
const threadId = comment.tags.find((t: string[]) => {
const tagName = t[0];
return (tagName === 'e' || tagName === 'E') && t[1];
})?.[1];
if (threadId && threadsMap.has(threadId)) { // Group zap receipts by thread ID (for sorting)
newCommentsMap.set(threadId, (newCommentsMap.get(threadId) || 0) + 1); const newZapReceiptsMap = new Map<string, NostrEvent[]>();
for (const zapReceipt of allZapReceipts) {
const threadId = zapReceipt.tags.find((t: string[]) => t[0] === 'e')?.[1];
if (threadId && threadsMap.has(threadId)) {
if (!newZapReceiptsMap.has(threadId)) {
newZapReceiptsMap.set(threadId, []);
}
newZapReceiptsMap.get(threadId)!.push(zapReceipt);
}
} }
} zapReceiptsMap = newZapReceiptsMap;
// Set count to 0 for threads with no comments if (!isMounted) return;
for (const threadId of threadIds) {
if (!newCommentsMap.has(threadId)) { // Count comments per thread
newCommentsMap.set(threadId, 0); const newCommentsMap = new Map<string, number>();
for (const comment of allComments) {
const threadId = comment.tags.find((t: string[]) => {
const tagName = t[0];
return (tagName === 'e' || tagName === 'E') && t[1];
})?.[1];
if (threadId && threadsMap.has(threadId)) {
newCommentsMap.set(threadId, (newCommentsMap.get(threadId) || 0) + 1);
}
}
// Set count to 0 for threads with no comments
for (const threadId of threadIds) {
if (!newCommentsMap.has(threadId)) {
newCommentsMap.set(threadId, 0);
}
} }
commentsMap = newCommentsMap;
} finally {
// Clean up both promises
activeFetchPromises.delete(zapFetchPromise);
activeFetchPromises.delete(commentsFetchPromise);
} }
commentsMap = newCommentsMap;
} }
} catch (error) { } catch (error) {
console.error('Error loading thread data:', error); console.error('Error loading thread data:', error);

49
src/lib/modules/profiles/ProfilePage.svelte

@ -330,14 +330,18 @@
} }
// Stream fresh data from relays (progressive enhancement) // Stream fresh data from relays (progressive enhancement)
// Fetch the actual bookmarked events in batches // Optimized: Fetch all bookmarked events in parallel batches
const batchSize = 100; const batchSize = 100;
const allBookmarkedEvents: NostrEvent[] = [];
const bookmarkedIdsArray = Array.from(bookmarkedIds); const bookmarkedIdsArray = Array.from(bookmarkedIds);
const batches: string[][] = [];
for (let i = 0; i < bookmarkedIdsArray.length; i += batchSize) { for (let i = 0; i < bookmarkedIdsArray.length; i += batchSize) {
const batch = bookmarkedIdsArray.slice(i, i + batchSize); batches.push(bookmarkedIdsArray.slice(i, i + batchSize));
const fetchPromise = nostrClient.fetchEvents( }
// Fetch all batches in parallel
const batchPromises = batches.map(batch =>
nostrClient.fetchEvents(
[{ ids: batch, limit: batch.length }], [{ ids: batch, limit: batch.length }],
profileRelays, profileRelays,
{ {
@ -355,19 +359,34 @@
loadingBookmarks = false; loadingBookmarks = false;
} }
} }
); )
activeFetchPromises.add(fetchPromise); );
const batchEvents = await fetchPromise;
activeFetchPromises.delete(fetchPromise); // Track all batch promises for cleanup
for (const promise of batchPromises) {
activeFetchPromises.add(promise);
}
try {
// Wait for all batches to complete
const allBatchResults = await Promise.all(batchPromises);
if (!isMounted) return; if (!isMounted) return;
allBookmarkedEvents.push(...batchEvents);
// Merge final results with existing bookmarks (onUpdate may have already updated some)
// This ensures we don't lose any intermediate updates from streaming
const bookmarkMap = new Map(bookmarks.map(b => [b.id, b]));
const allBookmarkedEvents = allBatchResults.flat();
for (const bookmark of allBookmarkedEvents) {
bookmarkMap.set(bookmark.id, bookmark);
}
bookmarks = Array.from(bookmarkMap.values()).sort((a, b) => b.created_at - a.created_at);
} finally {
// Clean up all batch promises
for (const promise of batchPromises) {
activeFetchPromises.delete(promise);
}
} }
if (!isMounted) return;
// Sort by created_at descending
bookmarks = allBookmarkedEvents.sort((a, b) => b.created_at - a.created_at);
} catch (error) { } catch (error) {
console.error('Error loading bookmarks:', error); console.error('Error loading bookmarks:', error);
if (isMounted) { if (isMounted) {
@ -1275,4 +1294,4 @@
max-width: 100%; max-width: 100%;
} }
} }
</style> </style>

27
src/lib/services/cache/event-cache.ts vendored

@ -162,14 +162,25 @@ export async function getRecentCachedEvents(kinds: number[], maxAge: number = 15
const results: CachedEvent[] = []; const results: CachedEvent[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
// Get events for each kind // Optimized: Use single transaction for all kinds
for (const kind of kinds) { const tx = db.transaction('events', 'readonly');
const tx = db.transaction('events', 'readonly'); const kindIndex = tx.store.index('kind');
const index = tx.store.index('kind');
const events = await index.getAll(kind); // Get events for all kinds in parallel within single transaction
await tx.done; const kindPromises = kinds.map(async (kind) => {
try {
// Filter by cache age and deduplicate return await kindIndex.getAll(kind);
} catch (error) {
console.debug(`Error getting events for kind ${kind}:`, error);
return [];
}
});
const allKindResults = await Promise.all(kindPromises);
await tx.done;
// Flatten and filter by cache age and deduplicate
for (const events of allKindResults) {
for (const event of events) { for (const event of events) {
if (event.cached_at >= cutoffTime && !seen.has(event.id)) { if (event.cached_at >= cutoffTime && !seen.has(event.id)) {
seen.add(event.id); seen.add(event.id);

73
src/routes/highlights/+page.svelte

@ -67,10 +67,57 @@
// Computed: total pages // Computed: total pages
let totalPages = $derived.by(() => Math.ceil(filteredItems.length / itemsPerPage)); let totalPages = $derived.by(() => Math.ceil(filteredItems.length / itemsPerPage));
// Process highlight events and add them to allItems (for quick display from cache/streaming)
async function processHighlightEvents(highlightEvents: NostrEvent[]) {
if (!highlightEvents || highlightEvents.length === 0) return;
// Track existing highlight IDs to avoid duplicates
const existingIds = new Set(allItems.map(item => item.event.id));
// Add new highlights that aren't already in allItems
const newItems: HighlightItem[] = [];
for (const highlight of highlightEvents) {
if (!existingIds.has(highlight.id)) {
newItems.push({
event: highlight,
authorPubkey: highlight.pubkey
});
existingIds.add(highlight.id);
}
}
// Merge with existing items, sort by created_at (newest first), and limit
if (newItems.length > 0) {
allItems = [...allItems, ...newItems]
.sort((a, b) => b.event.created_at - a.event.created_at)
.slice(0, maxTotalItems);
}
}
async function loadHighlights() { async function loadHighlights() {
loading = true; // Load from cache first (fast - instant display)
try {
const { getRecentCachedEvents } = await import('../../lib/services/cache/event-cache.js');
const cachedHighlights = await getRecentCachedEvents([KIND.HIGHLIGHTED_ARTICLE], 60 * 60 * 1000, 100); // 1 hour cache
if (cachedHighlights.length > 0) {
// Process cached highlights immediately
await processHighlightEvents(cachedHighlights);
loading = false; // Show cached content immediately
error = null;
currentPage = 1;
} else {
loading = true; // Only show loading if no cache
}
} catch (error) {
console.debug('Error loading cached highlights:', error);
loading = true; // Show loading if cache check fails
}
error = null; error = null;
allItems = []; if (allItems.length === 0) {
allItems = [];
}
currentPage = 1; currentPage = 1;
try { try {
@ -81,15 +128,27 @@
// Fetch highlight events (kind 9802) - limit 100 // Fetch highlight events (kind 9802) - limit 100
const highlightFilter: any = { kinds: [KIND.HIGHLIGHTED_ARTICLE], limit: 100 }; const highlightFilter: any = { kinds: [KIND.HIGHLIGHTED_ARTICLE], limit: 100 };
// Stream fresh data from relays (progressive enhancement)
const highlightEvents = await nostrClient.fetchEvents( const highlightEvents = await nostrClient.fetchEvents(
[highlightFilter], [highlightFilter],
allRelaysForHighlights, allRelaysForHighlights,
{ {
useCache: true, useCache: 'cache-first', // Already shown cache above, now stream updates
cacheResults: true, cacheResults: true,
timeout: config.standardTimeout timeout: config.standardTimeout,
onUpdate: async (newHighlights) => {
// Process new highlights as they stream in
if (newHighlights && newHighlights.length > 0) {
await processHighlightEvents(newHighlights);
}
}
} }
); );
// Process final results (merge with any streaming updates)
if (highlightEvents && highlightEvents.length > 0) {
await processHighlightEvents(highlightEvents);
}
console.log(`[Highlights] Found ${highlightEvents.length} highlight events from ${allRelaysForHighlights.length} relays`); console.log(`[Highlights] Found ${highlightEvents.length} highlight events from ${allRelaysForHighlights.length} relays`);
@ -193,7 +252,7 @@
aTagFilters, aTagFilters,
allRelaysForHighlights, allRelaysForHighlights,
{ {
useCache: true, useCache: 'cache-first',
cacheResults: true, cacheResults: true,
timeout: config.standardTimeout timeout: config.standardTimeout
} }
@ -281,7 +340,7 @@
filters, filters,
relays, relays,
{ {
useCache: true, useCache: 'cache-first',
cacheResults: true, cacheResults: true,
timeout: config.mediumTimeout timeout: config.mediumTimeout
} }
@ -353,7 +412,7 @@
[{ kinds: [KIND.METADATA], authors: pubkeyArray, limit: 1 }], [{ kinds: [KIND.METADATA], authors: pubkeyArray, limit: 1 }],
profileRelays, profileRelays,
{ {
useCache: true, useCache: 'cache-first',
cacheResults: true, cacheResults: true,
priority: 'low', priority: 'low',
timeout: config.standardTimeout timeout: config.standardTimeout

62
src/routes/lists/+page.svelte

@ -58,19 +58,19 @@
try { try {
const relays = getAllRelays(); const relays = getAllRelays();
// Fetch kind 3 (contacts) - replaceable, one per user // Optimized: Fetch both kinds in parallel in a single call
const contactsEvents = await nostrClient.fetchEvents( const [contactsEvents, followSetEvents] = await Promise.all([
[{ kinds: [KIND.CONTACTS], authors: [currentPubkey], limit: 1 }], nostrClient.fetchEvents(
relays, [{ kinds: [KIND.CONTACTS], authors: [currentPubkey], limit: 1 }],
{ useCache: true, cacheResults: true } relays,
); { useCache: 'cache-first', cacheResults: true }
),
// Fetch kind 30000 (follow_set) - parameterized replaceable, multiple per user with d-tags nostrClient.fetchEvents(
const followSetEvents = await nostrClient.fetchEvents( [{ kinds: [KIND.FOLLOW_SET], authors: [currentPubkey] }],
[{ kinds: [KIND.FOLLOW_SET], authors: [currentPubkey] }], relays,
relays, { useCache: 'cache-first', cacheResults: true }
{ useCache: true, cacheResults: true } )
); ]);
const allLists: ListInfo[] = []; const allLists: ListInfo[] = [];
@ -137,7 +137,28 @@
return; return;
} }
loadingEvents = true; // Load from cache first (fast - instant display)
try {
const { getRecentCachedEvents } = await import('../../lib/services/cache/event-cache.js');
const feedKinds = getFeedKinds();
const cachedEvents = await getRecentCachedEvents(feedKinds, 60 * 60 * 1000, 100); // 1 hour cache
// Filter cached events to only those from list pubkeys
const listPubkeySet = new Set(list.pubkeys);
const filteredCached = cachedEvents.filter(e => listPubkeySet.has(e.pubkey));
if (filteredCached.length > 0) {
events = filteredCached.sort((a, b) => b.created_at - a.created_at);
loadingEvents = false; // Show cached content immediately
} else {
loadingEvents = true; // Only show loading if no cache
}
} catch (error) {
console.debug('Error loading cached list events:', error);
loadingEvents = true; // Show loading if cache check fails
}
// Stream fresh data from relays (progressive enhancement)
try { try {
const relays = getAllRelays(); const relays = getAllRelays();
const feedKinds = getFeedKinds(); // Get all kinds with showInFeed: true const feedKinds = getFeedKinds(); // Get all kinds with showInFeed: true
@ -147,7 +168,11 @@
// Function to merge new events into the view // Function to merge new events into the view
const mergeEvents = (newEvents: NostrEvent[]) => { const mergeEvents = (newEvents: NostrEvent[]) => {
for (const event of newEvents) { // Filter to only events from list pubkeys
const listPubkeySet = new Set(list.pubkeys);
const filtered = newEvents.filter(e => listPubkeySet.has(e.pubkey));
for (const event of filtered) {
eventsMap.set(event.id, event); eventsMap.set(event.id, event);
} }
// Convert map to array, sort by created_at descending (newest first) // Convert map to array, sort by created_at descending (newest first)
@ -156,8 +181,7 @@
}; };
// Fetch events from all pubkeys in the list, with showInFeed kinds // Fetch events from all pubkeys in the list, with showInFeed kinds
// useCache: true will return cached events immediately, then fetch from relays in background // Already shown cache above, now stream updates from relays
// onUpdate will be called as new events arrive from relays
const fetchedEvents = await nostrClient.fetchEvents( const fetchedEvents = await nostrClient.fetchEvents(
[{ [{
kinds: feedKinds, kinds: feedKinds,
@ -166,13 +190,13 @@
}], }],
relays, relays,
{ {
useCache: true, useCache: 'cache-first', // Already shown cache above, now stream updates
cacheResults: true, cacheResults: true,
onUpdate: mergeEvents // Update view as new events arrive from relays onUpdate: mergeEvents // Update view as new events arrive from relays
} }
); );
// Initial merge of cached events // Final merge of any remaining events
mergeEvents(fetchedEvents); mergeEvents(fetchedEvents);
} catch (error) { } catch (error) {
console.error('Error loading list events:', error); console.error('Error loading list events:', error);

39
src/routes/repos/+page.svelte

@ -40,8 +40,8 @@
async function loadCachedRepos() { async function loadCachedRepos() {
try { try {
// Load cached repos (within 15 minutes) // Load cached repos (within 1 hour - optimized for slow connections)
const cachedRepos = await getRecentCachedEvents([KIND.REPO_ANNOUNCEMENT], 15 * 60 * 1000, 100); const cachedRepos = await getRecentCachedEvents([KIND.REPO_ANNOUNCEMENT], 60 * 60 * 1000, 100);
if (cachedRepos.length > 0) { if (cachedRepos.length > 0) {
// For parameterized replaceable events, get the newest version of each (by pubkey + d tag) // For parameterized replaceable events, get the newest version of each (by pubkey + d tag)
@ -79,13 +79,40 @@
// Fetch repo announcement events // Fetch repo announcement events
const allRepos: NostrEvent[] = []; const allRepos: NostrEvent[] = [];
// Stream fresh data from relays (progressive enhancement)
const events = await nostrClient.fetchEvents( const events = await nostrClient.fetchEvents(
[{ kinds: [KIND.REPO_ANNOUNCEMENT], limit: 100 }], [{ kinds: [KIND.REPO_ANNOUNCEMENT], limit: 100 }],
relays, relays,
{ useCache: true, cacheResults: true } {
useCache: 'cache-first', // Already shown cache above, now stream updates
cacheResults: true,
onUpdate: (newRepos) => {
// Merge with existing repos as they stream in
const reposByKey = new Map<string, NostrEvent>();
// Add existing repos first
for (const repo of repos) {
const dTag = repo.tags.find(t => t[0] === 'd')?.[1] || '';
const key = `${repo.pubkey}:${dTag}`;
reposByKey.set(key, repo);
}
// Add/update with new repos
for (const event of newRepos) {
const dTag = event.tags.find(t => t[0] === 'd')?.[1] || '';
const key = `${event.pubkey}:${dTag}`;
const existing = reposByKey.get(key);
if (!existing || event.created_at > existing.created_at) {
reposByKey.set(key, event);
}
}
repos = Array.from(reposByKey.values()).sort((a, b) => b.created_at - a.created_at);
}
}
); );
// Merge with existing cached repos // Final merge of any remaining events
const reposByKey = new Map<string, NostrEvent>(); const reposByKey = new Map<string, NostrEvent>();
// Add existing cached repos first // Add existing cached repos first
@ -105,10 +132,8 @@
} }
} }
allRepos.push(...Array.from(reposByKey.values()));
// Sort by created_at descending // Sort by created_at descending
repos = allRepos.sort((a, b) => b.created_at - a.created_at); repos = Array.from(reposByKey.values()).sort((a, b) => b.created_at - a.created_at);
} catch (error) { } catch (error) {
console.error('Error loading repos:', error); console.error('Error loading repos:', error);
repos = []; repos = [];

38
src/routes/topics/[name]/+page.svelte

@ -62,8 +62,8 @@
if (!topicName || loadingEvents) return; if (!topicName || loadingEvents) return;
try { try {
// Load cached events for this topic (within 15 minutes) // Load cached events for this topic (within 1 hour - optimized for slow connections)
const cachedEvents = await getRecentCachedEvents([KIND.SHORT_TEXT_NOTE, KIND.DISCUSSION_THREAD], 15 * 60 * 1000, 100); const cachedEvents = await getRecentCachedEvents([KIND.SHORT_TEXT_NOTE, KIND.DISCUSSION_THREAD], 60 * 60 * 1000, 100);
if (cachedEvents.length > 0) { if (cachedEvents.length > 0) {
// Filter events that match the topic // Filter events that match the topic
@ -118,30 +118,30 @@
// Only fetch events with matching t-tag (most efficient - uses relay filtering) // Only fetch events with matching t-tag (most efficient - uses relay filtering)
// This avoids fetching all events and filtering client-side, saving bandwidth // This avoids fetching all events and filtering client-side, saving bandwidth
// Stream events as they arrive (progressive enhancement)
const tTagEvents = await nostrClient.fetchEvents( const tTagEvents = await nostrClient.fetchEvents(
[{ kinds: [KIND.SHORT_TEXT_NOTE, KIND.DISCUSSION_THREAD], '#t': [topicName], limit: config.feedLimit }], [{ kinds: [KIND.SHORT_TEXT_NOTE, KIND.DISCUSSION_THREAD], '#t': [topicName], limit: config.feedLimit }],
relays, relays,
{ useCache: true, cacheResults: true, caller: `topics/[name]/+page.svelte (t-tag)` } {
useCache: 'cache-first', // Already shown cache above, now stream updates
cacheResults: true,
caller: `topics/[name]/+page.svelte (t-tag)`,
onUpdate: (newEvents) => {
// Merge with existing events as they stream in
const eventMap = new Map(events.map(e => [e.id, e]));
for (const event of newEvents) {
eventMap.set(event.id, event);
}
events = Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at);
}
}
); );
// Use t-tag events as the primary source // Final merge of any remaining events
// Content-based hashtag search is too inefficient (would require fetching all events) const eventMap = new Map(events.map(e => [e.id, e]));
// Users should use t-tags for proper topic organization for (const event of tTagEvents) {
const allEvents: NostrEvent[] = [...tTagEvents];
// Merge with existing cached events and deduplicate
const eventMap = new Map<string, NostrEvent>();
// Add existing cached events first
for (const event of events) {
eventMap.set(event.id, event); eventMap.set(event.id, event);
} }
// Add new events
for (const event of allEvents) {
eventMap.set(event.id, event);
}
events = Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at); events = Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at);
} catch (error) { } catch (error) {
console.error('Error loading topic events:', error); console.error('Error loading topic events:', error);

Loading…
Cancel
Save