Browse Source

more performance optimization

bug-fixes
master
Silberengel 1 month ago
parent
commit
ff2e735881
  1. 331
      src/routes/highlights/+page.svelte
  2. 83
      src/routes/lists/+page.svelte

331
src/routes/highlights/+page.svelte

@ -101,7 +101,7 @@ @@ -101,7 +101,7 @@
const cachedHighlights = await getRecentCachedEvents([KIND.HIGHLIGHTED_ARTICLE], 60 * 60 * 1000, 100); // 1 hour cache
if (cachedHighlights.length > 0) {
// Process cached highlights immediately
// Process cached highlights immediately - show them right away
await processHighlightEvents(cachedHighlights);
loading = false; // Show cached content immediately
error = null;
@ -120,294 +120,79 @@ @@ -120,294 +120,79 @@
}
currentPage = 1;
try {
const relays = relayManager.getFeedReadRelays();
const profileRelays = relayManager.getProfileReadRelays();
const allRelaysForHighlights = [...new Set([...relays, ...profileRelays])];
// Fetch highlight events (kind 9802) - limit 100
const highlightFilter: any = { kinds: [KIND.HIGHLIGHTED_ARTICLE], limit: 100 };
// Stream fresh data from relays (progressive enhancement)
const highlightEvents = await nostrClient.fetchEvents(
[highlightFilter],
allRelaysForHighlights,
{
useCache: 'cache-first', // Already shown cache above, now stream updates
cacheResults: true,
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);
}
// For highlights, we store the highlight event itself, mapped by source event ID
const highlightBySourceEvent = new Map<string, { highlight: NostrEvent; authorPubkey: string }>();
const aTagHighlights = new Map<string, { highlight: NostrEvent; pubkey: string }>();
const highlightsWithoutRefs: { highlight: NostrEvent; authorPubkey: string }[] = [];
let highlightsWithETags = 0;
let highlightsWithATags = 0;
let highlightsWithNoRefs = 0;
// First pass: extract e-tags and collect a-tags
for (const highlight of highlightEvents) {
let hasRef = false;
// Extract e-tag (direct event reference)
const eTag = highlight.tags.find(t => t[0] === 'e' && t[1]);
if (eTag && eTag[1]) {
highlightBySourceEvent.set(eTag[1], { highlight, authorPubkey: highlight.pubkey });
highlightsWithETags++;
hasRef = true;
}
// Extract a-tag (addressable event: kind:pubkey:d-tag)
const aTag = highlight.tags.find(t => t[0] === 'a' && t[1]);
if (aTag && aTag[1]) {
aTagHighlights.set(aTag[1], { highlight, pubkey: highlight.pubkey });
if (!hasRef) highlightsWithATags++;
hasRef = true;
}
if (!hasRef) {
highlightsWithNoRefs++;
highlightsWithoutRefs.push({ highlight, authorPubkey: highlight.pubkey });
}
}
// Second pass: fetch events for a-tags in batches (grouped by kind+pubkey+d-tag)
if (aTagHighlights.size > 0) {
// Group a-tags by kind+pubkey+d-tag to create efficient filters
const aTagGroups = new Map<string, { aTags: string[]; pubkey: string; kind: number; dTag?: string }>();
for (const [aTag, info] of aTagHighlights.entries()) {
const aTagParts = aTag.split(':');
if (aTagParts.length >= 2) {
const kind = parseInt(aTagParts[0]);
const pubkey = aTagParts[1];
const dTag = aTagParts[2] || '';
const groupKey = `${kind}:${pubkey}:${dTag}`;
if (!aTagGroups.has(groupKey)) {
aTagGroups.set(groupKey, {
aTags: [],
pubkey: info.pubkey,
kind,
dTag: dTag || undefined
});
}
aTagGroups.get(groupKey)!.aTags.push(aTag);
}
}
// Create batched filters (one per group)
const aTagFilters: any[] = [];
const filterToATags = new Map<number, string[]>();
for (const [groupKey, group] of aTagGroups.entries()) {
const firstATag = group.aTags[0];
const aTagParts = firstATag.split(':');
if (aTagParts.length >= 2) {
const pubkey = aTagParts[1];
const filter: any = {
kinds: [group.kind],
authors: [pubkey],
limit: 100
};
if (group.dTag) {
filter['#d'] = [group.dTag];
}
const filterIndex = aTagFilters.length;
aTagFilters.push(filter);
filterToATags.set(filterIndex, group.aTags);
}
}
// Fetch all a-tag events in one batch
if (aTagFilters.length > 0) {
try {
const aTagEvents = await nostrClient.fetchEvents(
aTagFilters,
allRelaysForHighlights,
{
useCache: 'cache-first',
cacheResults: true,
timeout: config.standardTimeout
}
);
// Match a-tag events back to highlights
const eventToATag = new Map<string, string>();
for (let filterIndex = 0; filterIndex < aTagFilters.length; filterIndex++) {
const filter = aTagFilters[filterIndex];
const aTags = filterToATags.get(filterIndex) || [];
const kind = filter.kinds[0];
const pubkey = filter.authors[0];
const dTag = filter['#d']?.[0];
const matchingEvents = aTagEvents.filter(event =>
event.kind === kind &&
event.pubkey === pubkey &&
(!dTag || event.tags.find(t => t[0] === 'd' && t[1] === dTag))
);
for (const event of matchingEvents) {
for (const aTag of aTags) {
const aTagParts = aTag.split(':');
if (aTagParts.length >= 2) {
const aTagKind = parseInt(aTagParts[0]);
const aTagPubkey = aTagParts[1];
const aTagDTag = aTagParts[2] || '';
if (event.kind === aTagKind && event.pubkey === aTagPubkey) {
if (aTagDTag) {
const eventDTag = event.tags.find(t => t[0] === 'd' && t[1]);
if (eventDTag && eventDTag[1] === aTagDTag) {
eventToATag.set(event.id, aTag);
break;
}
} else {
eventToATag.set(event.id, aTag);
break;
}
}
}
}
}
}
// Map events to highlights
for (const [eventId, aTag] of eventToATag.entries()) {
const info = aTagHighlights.get(aTag);
if (info) {
highlightBySourceEvent.set(eventId, { highlight: info.highlight, authorPubkey: info.pubkey });
}
}
} catch (err) {
// Non-critical: a-tag resolution failed, continue with e-tags only
}
}
}
// Get source event IDs for highlights (to fetch them for sorting/display)
const highlightSourceEventIds = Array.from(highlightBySourceEvent.keys());
// Limit to maxTotalItems
const eventIds = highlightSourceEventIds.slice(0, maxTotalItems);
// Fetch the actual events - batch to avoid relay limits
const batchSize = 100;
const allFetchedEvents: NostrEvent[] = [];
// Start fetching fresh data in the background (non-blocking)
// This enhances the cached content progressively
const enhanceHighlights = async () => {
try {
const relays = relayManager.getFeedReadRelays();
const profileRelays = relayManager.getProfileReadRelays();
const allRelaysForHighlights = [...new Set([...relays, ...profileRelays])];
for (let i = 0; i < eventIds.length; i += batchSize) {
const batch = eventIds.slice(i, i + batchSize);
const filters = [{ ids: batch }];
// Fetch highlight events (kind 9802) - limit 100
const highlightFilter: any = { kinds: [KIND.HIGHLIGHTED_ARTICLE], limit: 100 };
const batchEvents = await nostrClient.fetchEvents(
filters,
relays,
// Stream fresh data from relays (progressive enhancement)
const highlightEvents = await nostrClient.fetchEvents(
[highlightFilter],
allRelaysForHighlights,
{
useCache: 'cache-first',
useCache: 'cache-first', // Already shown cache above, now stream updates
cacheResults: true,
timeout: config.mediumTimeout
timeout: config.standardTimeout,
onUpdate: async (newHighlights) => {
// Process new highlights as they stream in
if (newHighlights && newHighlights.length > 0) {
await processHighlightEvents(newHighlights);
}
}
}
);
allFetchedEvents.push(...batchEvents);
}
// Track which highlights we've already added (to avoid duplicates)
const addedHighlightIds = new Set<string>();
const items: HighlightItem[] = [];
// Create HighlightItem items
for (const event of allFetchedEvents) {
const highlightInfo = highlightBySourceEvent.get(event.id);
if (highlightInfo) {
// For highlights, use the highlight event itself, not the source event
if (!addedHighlightIds.has(highlightInfo.highlight.id)) {
items.push({
event: highlightInfo.highlight,
authorPubkey: highlightInfo.authorPubkey
});
addedHighlightIds.add(highlightInfo.highlight.id);
}
// Process final results (merge with any streaming updates)
if (highlightEvents && highlightEvents.length > 0) {
await processHighlightEvents(highlightEvents);
}
}
// Add ALL highlights with e-tag or a-tag references, even if source event wasn't found
for (const [sourceEventId, highlightInfo] of highlightBySourceEvent.entries()) {
if (!addedHighlightIds.has(highlightInfo.highlight.id)) {
items.push({
event: highlightInfo.highlight,
authorPubkey: highlightInfo.authorPubkey
});
addedHighlightIds.add(highlightInfo.highlight.id);
// Note: We skip the complex e-tag/a-tag resolution and source event fetching
// The highlights are already displayed from cache/streaming above
// This complex resolution can be done later if needed, but it's not blocking the UI
// Pre-fetch all profiles for event authors in one batch (non-blocking)
const uniquePubkeys = new Set<string>();
for (const item of allItems) {
uniquePubkeys.add(item.event.pubkey);
uniquePubkeys.add(item.authorPubkey);
}
}
// Add highlights without e-tag or a-tag references (URL-only highlights, etc.)
for (const highlightInfo of highlightsWithoutRefs) {
if (!addedHighlightIds.has(highlightInfo.highlight.id)) {
items.push({
event: highlightInfo.highlight,
authorPubkey: highlightInfo.authorPubkey
if (uniquePubkeys.size > 0) {
const profileRelays = relayManager.getProfileReadRelays();
const pubkeyArray = Array.from(uniquePubkeys);
nostrClient.fetchEvents(
[{ kinds: [KIND.METADATA], authors: pubkeyArray, limit: 1 }],
profileRelays,
{
useCache: 'cache-first',
cacheResults: true,
priority: 'low',
timeout: config.standardTimeout
}
).catch(() => {
// Non-critical: profile pre-fetch failed
});
addedHighlightIds.add(highlightInfo.highlight.id);
}
} catch (err) {
// Enhancement failed, but we already have cached content showing
}
};
// Sort by created_at (newest first) and limit to maxTotalItems
allItems = items.sort((a, b) => b.event.created_at - a.event.created_at).slice(0, maxTotalItems);
// Pre-fetch all profiles for event authors in one batch to avoid individual fetches
const uniquePubkeys = new Set<string>();
for (const item of allItems) {
uniquePubkeys.add(item.event.pubkey);
uniquePubkeys.add(item.authorPubkey);
}
if (uniquePubkeys.size > 0) {
const profileRelays = relayManager.getProfileReadRelays();
const pubkeyArray = Array.from(uniquePubkeys);
// Start enhancement in background (non-blocking)
enhanceHighlights().catch(() => {
// Enhancement failed, but cached content is already showing
});
nostrClient.fetchEvents(
[{ kinds: [KIND.METADATA], authors: pubkeyArray, limit: 1 }],
profileRelays,
{
useCache: 'cache-first',
cacheResults: true,
priority: 'low',
timeout: config.standardTimeout
}
).catch(() => {
// Non-critical: profile pre-fetch failed
});
}
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load highlights';
} finally {
loading = false;
hasLoadedOnce = true;
}
// Mark as loaded once we have cached content
hasLoadedOnce = true;
}
// Reset to page 1 when filter changes

83
src/routes/lists/+page.svelte

@ -55,10 +55,84 @@ @@ -55,10 +55,84 @@
return;
}
// Load from cache first (fast - instant display)
try {
const { getRecentCachedEvents } = await import('../../lib/services/cache/event-cache.js');
const cachedLists = await getRecentCachedEvents([KIND.CONTACTS, KIND.FOLLOW_SET], 60 * 60 * 1000, 10); // 1 hour cache
const userCachedLists = cachedLists.filter(e => e.pubkey === currentPubkey);
if (userCachedLists.length > 0) {
const allLists: ListInfo[] = [];
// Process cached contacts
const cachedContacts = userCachedLists.filter(e => e.kind === KIND.CONTACTS);
if (cachedContacts.length > 0) {
const contactsEvent = cachedContacts[0];
const pubkeys = contactsEvent.tags
.filter(tag => tag[0] === 'p' && tag[1])
.map(tag => tag[1]);
if (pubkeys.length > 0) {
allLists.push({
kind: KIND.CONTACTS,
name: 'Follows',
pubkeys,
event: contactsEvent
});
}
}
// Process cached follow_set events
const cachedFollowSets = userCachedLists.filter(e => e.kind === KIND.FOLLOW_SET);
for (const followSetEvent of cachedFollowSets) {
const dTag = followSetEvent.tags.find(tag => tag[0] === 'd' && tag[1])?.[1];
const pubkeys = followSetEvent.tags
.filter(tag => tag[0] === 'p' && tag[1])
.map(tag => tag[1]);
if (pubkeys.length > 0) {
allLists.push({
kind: KIND.FOLLOW_SET,
name: dTag || 'Follow Set',
dTag,
pubkeys,
event: followSetEvent
});
}
}
// Sort by kind (3 first, then 30000), then by name
allLists.sort((a, b) => {
if (a.kind !== b.kind) {
return a.kind - b.kind;
}
return a.name.localeCompare(b.name);
});
lists = allLists;
loading = false; // Show cached lists immediately
// Auto-select first list if available and load its cached events
if (lists.length > 0 && !selectedList) {
selectedList = lists[0];
// Load cached events for first list immediately (non-blocking)
loadListEvents(lists[0]).catch(() => {
// Failed to load cached events, will enhance from relays
});
}
} else {
loading = true; // Only show loading if no cache
}
} catch (error) {
// Cache error is non-critical, continue with relay fetch
loading = true;
}
// Enhance with fresh data from relays (non-blocking)
try {
const relays = getAllRelays();
// Optimized: Fetch both kinds in parallel in a single call
// Fetch both kinds in parallel
const [contactsEvents, followSetEvents] = await Promise.all([
nostrClient.fetchEvents(
[{ kinds: [KIND.CONTACTS], authors: [currentPubkey], limit: 1 }],
@ -119,13 +193,16 @@ @@ -119,13 +193,16 @@
lists = allLists;
// Auto-select first list if available
// Auto-select first list if available and not already selected
if (lists.length > 0 && !selectedList) {
selectedList = lists[0];
await loadListEvents(lists[0]);
} else if (selectedList && lists.length > 0) {
// Refresh events for currently selected list
await loadListEvents(selectedList);
}
} catch (error) {
// Failed to load lists
// Failed to load lists (but cached content may already be showing)
} finally {
loading = false;
}

Loading…
Cancel
Save