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. 84
      src/lib/modules/discussions/DiscussionList.svelte
  4. 41
      src/lib/modules/profiles/ProfilePage.svelte
  5. 21
      src/lib/services/cache/event-cache.ts
  6. 71
      src/routes/highlights/+page.svelte
  7. 54
      src/routes/lists/+page.svelte
  8. 39
      src/routes/repos/+page.svelte
  9. 36
      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

84
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,16 +330,28 @@
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 }
);
const commentsFetchPromise = nostrClient.fetchEvents(
[{ kinds: [KIND.COMMENT], '#E': threadIds, '#K': ['11'], limit: config.feedLimit }],
commentRelays,
{ useCache: 'cache-first', cacheResults: true, timeout: config.standardTimeout, priority: 'low' }
); );
// Track both promises for cleanup
activeFetchPromises.add(zapFetchPromise); activeFetchPromises.add(zapFetchPromise);
const allZapReceipts = await zapFetchPromise; activeFetchPromises.add(commentsFetchPromise);
activeFetchPromises.delete(zapFetchPromise);
try {
const [allZapReceipts, allComments] = await Promise.all([
zapFetchPromise,
commentsFetchPromise
]);
if (!isMounted) return; if (!isMounted) return;
@ -378,17 +368,6 @@
} }
zapReceiptsMap = newZapReceiptsMap; zapReceiptsMap = newZapReceiptsMap;
// Batch-load comment counts for all threads
if (!isMounted) return;
const commentsFetchPromise = nostrClient.fetchEvents(
[{ kinds: [KIND.COMMENT], '#E': threadIds, '#K': ['11'], limit: config.feedLimit }],
commentRelays,
{ useCache: 'relay-first', cacheResults: true, timeout: config.standardTimeout, priority: 'low' }
);
activeFetchPromises.add(commentsFetchPromise);
const allComments = await commentsFetchPromise;
activeFetchPromises.delete(commentsFetchPromise);
if (!isMounted) return; if (!isMounted) return;
// Count comments per thread // Count comments per thread
@ -411,6 +390,11 @@
} }
} }
commentsMap = newCommentsMap; commentsMap = newCommentsMap;
} finally {
// Clean up both promises
activeFetchPromises.delete(zapFetchPromise);
activeFetchPromises.delete(commentsFetchPromise);
}
} }
} catch (error) { } catch (error) {
console.error('Error loading thread data:', error); console.error('Error loading thread data:', error);

41
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);
if (!isMounted) return; // Track all batch promises for cleanup
allBookmarkedEvents.push(...batchEvents); 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;
// Sort by created_at descending // Merge final results with existing bookmarks (onUpdate may have already updated some)
bookmarks = allBookmarkedEvents.sort((a, b) => b.created_at - a.created_at); // 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);
}
}
} catch (error) { } catch (error) {
console.error('Error loading bookmarks:', error); console.error('Error loading bookmarks:', error);
if (isMounted) { if (isMounted) {

21
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 index = tx.store.index('kind'); const kindIndex = tx.store.index('kind');
const events = await index.getAll(kind);
// Get events for all kinds in parallel within single transaction
const kindPromises = kinds.map(async (kind) => {
try {
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; await tx.done;
// Filter by cache age and deduplicate // 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);

71
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; 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;
if (allItems.length === 0) {
allItems = []; allItems = [];
}
currentPage = 1; currentPage = 1;
try { try {
@ -81,16 +128,28 @@
// 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`);
// For highlights, we store the highlight event itself, mapped by source event ID // For highlights, we store the highlight event itself, mapped by source event ID
@ -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

54
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([
nostrClient.fetchEvents(
[{ kinds: [KIND.CONTACTS], authors: [currentPubkey], limit: 1 }], [{ kinds: [KIND.CONTACTS], authors: [currentPubkey], limit: 1 }],
relays, relays,
{ useCache: true, cacheResults: true } { useCache: 'cache-first', cacheResults: true }
); ),
nostrClient.fetchEvents(
// Fetch kind 30000 (follow_set) - parameterized replaceable, multiple per user with d-tags
const followSetEvents = await nostrClient.fetchEvents(
[{ kinds: [KIND.FOLLOW_SET], authors: [currentPubkey] }], [{ kinds: [KIND.FOLLOW_SET], authors: [currentPubkey] }],
relays, relays,
{ useCache: true, cacheResults: true } { useCache: 'cache-first', 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 = [];

36
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,
// Use t-tag events as the primary source caller: `topics/[name]/+page.svelte (t-tag)`,
// Content-based hashtag search is too inefficient (would require fetching all events) onUpdate: (newEvents) => {
// Users should use t-tags for proper topic organization // Merge with existing events as they stream in
const allEvents: NostrEvent[] = [...tTagEvents]; const eventMap = new Map(events.map(e => [e.id, e]));
for (const event of newEvents) {
// 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);
} }
events = Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at);
}
}
);
// Add new events // Final merge of any remaining events
for (const event of allEvents) { const eventMap = new Map(events.map(e => [e.id, e]));
for (const event of tTagEvents) {
eventMap.set(event.id, event); 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