|
|
|
|
@ -220,8 +220,14 @@
@@ -220,8 +220,14 @@
|
|
|
|
|
/** |
|
|
|
|
* Fetches events from the Nostr network |
|
|
|
|
* |
|
|
|
|
* This function fetches index events and their referenced content events, |
|
|
|
|
* filters them according to NIP-62, and combines them for visualization. |
|
|
|
|
* This function orchestrates the fetching of events through multiple steps: |
|
|
|
|
* 1. Setup configuration and loading state |
|
|
|
|
* 2. Fetch non-publication events (kinds 1, 3, etc.) |
|
|
|
|
* 3. Fetch publication index events |
|
|
|
|
* 4. Extract and fetch content events |
|
|
|
|
* 5. Deduplicate and combine all events |
|
|
|
|
* 6. Fetch profiles for discovered pubkeys |
|
|
|
|
* 7. Apply display limits and finalize |
|
|
|
|
*/ |
|
|
|
|
async function fetchEvents() { |
|
|
|
|
// Prevent concurrent fetches |
|
|
|
|
@ -238,12 +244,47 @@
@@ -238,12 +244,47 @@
|
|
|
|
|
loading = true; |
|
|
|
|
error = null; |
|
|
|
|
|
|
|
|
|
// Get ALL event configurations (Phase 5: fetch all, display enabled) |
|
|
|
|
// Step 1: Setup configuration and loading state |
|
|
|
|
const { allConfigs, publicationConfigs, otherConfigs, kind0Config } = setupFetchConfiguration(); |
|
|
|
|
|
|
|
|
|
// Step 2: Fetch non-publication events |
|
|
|
|
const nonPublicationEvents = await fetchNonPublicationEvents(otherConfigs); |
|
|
|
|
|
|
|
|
|
// Step 3: Fetch publication index events |
|
|
|
|
const validIndexEvents = await fetchPublicationIndexEvents(publicationConfigs); |
|
|
|
|
|
|
|
|
|
// Step 4: Extract and fetch content events |
|
|
|
|
const contentEvents = await fetchContentEvents(validIndexEvents, publicationConfigs); |
|
|
|
|
|
|
|
|
|
// Step 5: Deduplicate and combine all events |
|
|
|
|
const combinedEvents = deduplicateAndCombineEvents(nonPublicationEvents, validIndexEvents, contentEvents); |
|
|
|
|
|
|
|
|
|
// Step 6: Fetch profiles for discovered pubkeys |
|
|
|
|
const eventsWithProfiles = await fetchProfilesForEvents(combinedEvents, kind0Config); |
|
|
|
|
|
|
|
|
|
// Step 7: Apply display limits and finalize |
|
|
|
|
finalizeEventFetch(eventsWithProfiles); |
|
|
|
|
|
|
|
|
|
} catch (e) { |
|
|
|
|
console.error("Error fetching events:", e); |
|
|
|
|
error = e instanceof Error ? e.message : String(e); |
|
|
|
|
} finally { |
|
|
|
|
loading = false; |
|
|
|
|
isFetching = false; |
|
|
|
|
debug("Loading set to false in fetchEvents"); |
|
|
|
|
debug("Final state check - loading:", loading, "events.length:", events.length, "allEvents.length:", allEvents.length); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Step 1: Setup configuration and loading state |
|
|
|
|
*/ |
|
|
|
|
function setupFetchConfiguration() { |
|
|
|
|
const config = get(visualizationConfig); |
|
|
|
|
const allConfigs = config.eventConfigs; |
|
|
|
|
|
|
|
|
|
debug("All event configs:", allConfigs); |
|
|
|
|
debug("Disabled kinds:", config.disabledKinds); |
|
|
|
|
debug("Enabled kinds:", allConfigs.filter(ec => ec.enabled !== false).map(ec => ec.kind)); |
|
|
|
|
|
|
|
|
|
// Set loading event kinds for display (show all being loaded) |
|
|
|
|
loadingEventKinds = allConfigs.map(ec => ({ |
|
|
|
|
@ -256,12 +297,18 @@
@@ -256,12 +297,18 @@
|
|
|
|
|
const publicationConfigs = allConfigs.filter(ec => publicationKinds.includes(ec.kind)); |
|
|
|
|
const otherConfigs = allConfigs.filter(ec => !publicationKinds.includes(ec.kind)); |
|
|
|
|
|
|
|
|
|
let allFetchedEvents: NDKEvent[] = []; |
|
|
|
|
|
|
|
|
|
// First, fetch non-publication events (like kind 1, 3, etc. but NOT kind 0) |
|
|
|
|
// We'll fetch kind 0 profiles after we know which pubkeys we need |
|
|
|
|
// Find kind 0 config for profile fetching |
|
|
|
|
const kind0Config = otherConfigs.find(c => c.kind === 0); |
|
|
|
|
|
|
|
|
|
return { allConfigs, publicationConfigs, otherConfigs, kind0Config }; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Step 2: Fetch non-publication events (kinds 1, 3, etc. but NOT kind 0) |
|
|
|
|
*/ |
|
|
|
|
async function fetchNonPublicationEvents(otherConfigs: any[]): Promise<NDKEvent[]> { |
|
|
|
|
const nonProfileConfigs = otherConfigs.filter(c => c.kind !== 0); |
|
|
|
|
let allFetchedEvents: NDKEvent[] = []; |
|
|
|
|
|
|
|
|
|
if (nonProfileConfigs.length > 0) { |
|
|
|
|
debug("Fetching non-publication events (excluding profiles):", nonProfileConfigs); |
|
|
|
|
@ -293,8 +340,13 @@
@@ -293,8 +340,13 @@
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Then handle publication events as before |
|
|
|
|
let validIndexEvents: Set<NDKEvent> = new Set(); |
|
|
|
|
return allFetchedEvents; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Step 3: Fetch publication index events |
|
|
|
|
*/ |
|
|
|
|
async function fetchPublicationIndexEvents(publicationConfigs: any[]): Promise<Set<NDKEvent>> { |
|
|
|
|
const shouldFetchIndex = publicationConfigs.some(ec => ec.kind === INDEX_EVENT_KIND); |
|
|
|
|
|
|
|
|
|
if (data.eventId) { |
|
|
|
|
@ -310,10 +362,10 @@
@@ -310,10 +362,10 @@
|
|
|
|
|
throw new Error(`Event ${data.eventId} is not a publication index (kind ${INDEX_EVENT_KIND})`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
validIndexEvents = new Set([event]); |
|
|
|
|
return new Set([event]); |
|
|
|
|
} else if (!shouldFetchIndex) { |
|
|
|
|
debug("Index events (30040) are disabled, skipping fetch"); |
|
|
|
|
validIndexEvents = new Set(); |
|
|
|
|
return new Set(); |
|
|
|
|
} else { |
|
|
|
|
// Original behavior: fetch all publications |
|
|
|
|
debug(`Fetching index events (kind ${INDEX_EVENT_KIND})`); |
|
|
|
|
@ -334,12 +386,57 @@
@@ -334,12 +386,57 @@
|
|
|
|
|
debug("Fetched index events:", indexEvents.size); |
|
|
|
|
|
|
|
|
|
// Filter valid index events according to NIP-62 |
|
|
|
|
validIndexEvents = filterValidIndexEvents(indexEvents); |
|
|
|
|
const validIndexEvents = filterValidIndexEvents(indexEvents); |
|
|
|
|
debug("Valid index events after filtering:", validIndexEvents.size); |
|
|
|
|
|
|
|
|
|
return validIndexEvents; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Step 3: Extract content event references from index events |
|
|
|
|
/** |
|
|
|
|
* Step 4: Extract and fetch content events |
|
|
|
|
*/ |
|
|
|
|
async function fetchContentEvents(validIndexEvents: Set<NDKEvent>, publicationConfigs: any[]): Promise<Set<NDKEvent>> { |
|
|
|
|
// Extract content event references from index events |
|
|
|
|
const contentReferences = extractContentReferences(validIndexEvents); |
|
|
|
|
debug("Content references to fetch:", contentReferences.size); |
|
|
|
|
|
|
|
|
|
// Fetch the referenced content events with author filter |
|
|
|
|
const enabledPublicationKinds = publicationConfigs.map(ec => ec.kind); |
|
|
|
|
const enabledContentKinds = CONTENT_EVENT_KINDS.filter(kind => enabledPublicationKinds.includes(kind)); |
|
|
|
|
debug(`Fetching content events (enabled kinds: ${enabledContentKinds.join(', ')})`); |
|
|
|
|
|
|
|
|
|
// Group by author to make more efficient queries |
|
|
|
|
const referencesByAuthor = groupContentReferencesByAuthor(contentReferences, enabledContentKinds); |
|
|
|
|
|
|
|
|
|
// Fetch events for each author |
|
|
|
|
const contentEventPromises = Array.from(referencesByAuthor.entries()).map( |
|
|
|
|
async ([author, refs]) => { |
|
|
|
|
const dTags = [...new Set(refs.map(r => r.dTag))]; // Dedupe d-tags |
|
|
|
|
return $ndkInstance.fetchEvents({ |
|
|
|
|
kinds: enabledContentKinds, // Only fetch enabled kinds |
|
|
|
|
authors: [author], |
|
|
|
|
"#d": dTags, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const contentEventSets = await Promise.all(contentEventPromises); |
|
|
|
|
|
|
|
|
|
// Deduplicate by keeping only the most recent version of each d-tag per author |
|
|
|
|
const eventsByCoordinate = deduplicateContentEvents(contentEventSets); |
|
|
|
|
const contentEvents = new Set(eventsByCoordinate.values()); |
|
|
|
|
debug("Fetched content events after deduplication:", contentEvents.size); |
|
|
|
|
|
|
|
|
|
return contentEvents; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Extract content event references from index events |
|
|
|
|
*/ |
|
|
|
|
function extractContentReferences(validIndexEvents: Set<NDKEvent>): Map<string, { kind: number; pubkey: string; dTag: string }> { |
|
|
|
|
const contentReferences = new Map<string, { kind: number; pubkey: string; dTag: string }>(); |
|
|
|
|
|
|
|
|
|
validIndexEvents.forEach((event) => { |
|
|
|
|
const aTags = event.getMatchingTags("a"); |
|
|
|
|
debug(`Event ${event.id} has ${aTags.length} a-tags`); |
|
|
|
|
@ -362,16 +459,19 @@
@@ -362,16 +459,19 @@
|
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
debug("Content references to fetch:", contentReferences.size); |
|
|
|
|
|
|
|
|
|
// Step 4: Fetch the referenced content events with author filter |
|
|
|
|
// Only fetch content kinds that are enabled |
|
|
|
|
const enabledPublicationKinds = publicationConfigs.map(ec => ec.kind); |
|
|
|
|
const enabledContentKinds = CONTENT_EVENT_KINDS.filter(kind => enabledPublicationKinds.includes(kind)); |
|
|
|
|
debug(`Fetching content events (enabled kinds: ${enabledContentKinds.join(', ')})`); |
|
|
|
|
return contentReferences; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Group by author to make more efficient queries |
|
|
|
|
/** |
|
|
|
|
* Group content references by author for efficient fetching |
|
|
|
|
*/ |
|
|
|
|
function groupContentReferencesByAuthor( |
|
|
|
|
contentReferences: Map<string, { kind: number; pubkey: string; dTag: string }>, |
|
|
|
|
enabledContentKinds: number[] |
|
|
|
|
): Map<string, Array<{ kind: number; dTag: string }>> { |
|
|
|
|
const referencesByAuthor = new Map<string, Array<{ kind: number; dTag: string }>>(); |
|
|
|
|
|
|
|
|
|
contentReferences.forEach(({ kind, pubkey, dTag }) => { |
|
|
|
|
// Only include references for enabled kinds |
|
|
|
|
if (enabledContentKinds.includes(kind)) { |
|
|
|
|
@ -382,24 +482,16 @@
@@ -382,24 +482,16 @@
|
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Fetch events for each author |
|
|
|
|
const contentEventPromises = Array.from(referencesByAuthor.entries()).map( |
|
|
|
|
async ([author, refs]) => { |
|
|
|
|
const dTags = [...new Set(refs.map(r => r.dTag))]; // Dedupe d-tags |
|
|
|
|
return $ndkInstance.fetchEvents({ |
|
|
|
|
kinds: enabledContentKinds, // Only fetch enabled kinds |
|
|
|
|
authors: [author], |
|
|
|
|
"#d": dTags, |
|
|
|
|
}); |
|
|
|
|
return referencesByAuthor; |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const contentEventSets = await Promise.all(contentEventPromises); |
|
|
|
|
|
|
|
|
|
// Deduplicate by keeping only the most recent version of each d-tag per author |
|
|
|
|
/** |
|
|
|
|
* Deduplicate content events by keeping only the most recent version |
|
|
|
|
*/ |
|
|
|
|
function deduplicateContentEvents(contentEventSets: Set<NDKEvent>[]): Map<string, NDKEvent> { |
|
|
|
|
const eventsByCoordinate = new Map<string, NDKEvent>(); |
|
|
|
|
|
|
|
|
|
contentEventSets.forEach((eventSet, idx) => { |
|
|
|
|
contentEventSets.forEach((eventSet) => { |
|
|
|
|
eventSet.forEach(event => { |
|
|
|
|
const dTag = event.tagValue("d"); |
|
|
|
|
const author = event.pubkey; |
|
|
|
|
@ -420,14 +512,21 @@
@@ -420,14 +512,21 @@
|
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const contentEvents = new Set(eventsByCoordinate.values()); |
|
|
|
|
debug("Fetched content events after deduplication:", contentEvents.size); |
|
|
|
|
return eventsByCoordinate; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Step 5: Combine all events (non-publication + publication events) |
|
|
|
|
/** |
|
|
|
|
* Step 5: Deduplicate and combine all events |
|
|
|
|
*/ |
|
|
|
|
function deduplicateAndCombineEvents( |
|
|
|
|
nonPublicationEvents: NDKEvent[], |
|
|
|
|
validIndexEvents: Set<NDKEvent>, |
|
|
|
|
contentEvents: Set<NDKEvent> |
|
|
|
|
): NDKEvent[] { |
|
|
|
|
// First, build coordinate map for replaceable events |
|
|
|
|
const coordinateMap = new Map<string, NDKEvent>(); |
|
|
|
|
const allEventsToProcess = [ |
|
|
|
|
...allFetchedEvents, // Non-publication events fetched earlier |
|
|
|
|
...nonPublicationEvents, // Non-publication events fetched earlier |
|
|
|
|
...Array.from(validIndexEvents), |
|
|
|
|
...Array.from(contentEvents) |
|
|
|
|
]; |
|
|
|
|
@ -490,13 +589,21 @@
@@ -490,13 +589,21 @@
|
|
|
|
|
|
|
|
|
|
baseEvents = [...allEvents]; // Store base events for tag expansion |
|
|
|
|
|
|
|
|
|
// Step 6: Extract all pubkeys and fetch profiles |
|
|
|
|
return allEvents; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Step 6: Fetch profiles for discovered pubkeys |
|
|
|
|
*/ |
|
|
|
|
async function fetchProfilesForEvents(combinedEvents: NDKEvent[], kind0Config: any): Promise<NDKEvent[]> { |
|
|
|
|
// Extract all pubkeys and fetch profiles |
|
|
|
|
debug("Extracting pubkeys from all events"); |
|
|
|
|
|
|
|
|
|
// Use the utility function to extract ALL pubkeys (authors + p tags + content) |
|
|
|
|
const allPubkeys = extractPubkeysFromEvents(allEvents); |
|
|
|
|
const allPubkeys = extractPubkeysFromEvents(combinedEvents); |
|
|
|
|
|
|
|
|
|
// Check if follow list is configured with limit > 0 |
|
|
|
|
const allConfigs = get(visualizationConfig).eventConfigs; |
|
|
|
|
const followListConfig = allConfigs.find(c => c.kind === 3); |
|
|
|
|
const shouldIncludeFollowPubkeys = followListConfig && followListConfig.limit > 0; |
|
|
|
|
|
|
|
|
|
@ -517,7 +624,7 @@
@@ -517,7 +624,7 @@
|
|
|
|
|
|
|
|
|
|
debug("Profile extraction complete:", { |
|
|
|
|
totalPubkeys: allPubkeys.size, |
|
|
|
|
fromEvents: allEvents.length, |
|
|
|
|
fromEvents: combinedEvents.length, |
|
|
|
|
fromFollowLists: followListEvents.length |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
@ -540,7 +647,7 @@
@@ -540,7 +647,7 @@
|
|
|
|
|
debug("Profile fetch complete, fetched", profileEvents.length, "profiles"); |
|
|
|
|
|
|
|
|
|
// Add profile events to allEvents |
|
|
|
|
allEvents = [...allEvents, ...profileEvents]; |
|
|
|
|
allEvents = [...combinedEvents, ...profileEvents]; |
|
|
|
|
|
|
|
|
|
// Update profile stats for display |
|
|
|
|
// Use the total number of pubkeys, not just newly fetched profiles |
|
|
|
|
@ -548,29 +655,29 @@
@@ -548,29 +655,29 @@
|
|
|
|
|
totalFetched: allPubkeys.size, |
|
|
|
|
displayLimit: kind0Config.limit |
|
|
|
|
}; |
|
|
|
|
} else { |
|
|
|
|
allEvents = [...combinedEvents]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Step 7: Apply display limits |
|
|
|
|
events = filterByDisplayLimits(allEvents, $visualizationConfig); |
|
|
|
|
return allEvents; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Step 8: Detect missing events |
|
|
|
|
const eventIds = new Set(allEvents.map(e => e.id)); |
|
|
|
|
/** |
|
|
|
|
* Step 7: Apply display limits and finalize |
|
|
|
|
*/ |
|
|
|
|
function finalizeEventFetch(eventsWithProfiles: NDKEvent[]) { |
|
|
|
|
// Apply display limits |
|
|
|
|
events = filterByDisplayLimits(eventsWithProfiles, $visualizationConfig); |
|
|
|
|
|
|
|
|
|
// Detect missing events |
|
|
|
|
const eventIds = new Set(eventsWithProfiles.map(e => e.id)); |
|
|
|
|
missingEventIds = detectMissingEvents(events, eventIds); |
|
|
|
|
|
|
|
|
|
debug("Total events fetched:", allEvents.length); |
|
|
|
|
debug("Total events fetched:", eventsWithProfiles.length); |
|
|
|
|
debug("Events displayed:", events.length); |
|
|
|
|
debug("Missing event IDs:", missingEventIds.size); |
|
|
|
|
debug("About to set loading to false"); |
|
|
|
|
debug("Current loading state:", loading); |
|
|
|
|
} catch (e) { |
|
|
|
|
console.error("Error fetching events:", e); |
|
|
|
|
error = e instanceof Error ? e.message : String(e); |
|
|
|
|
} finally { |
|
|
|
|
loading = false; |
|
|
|
|
isFetching = false; |
|
|
|
|
debug("Loading set to false in fetchEvents"); |
|
|
|
|
debug("Final state check - loading:", loading, "events.length:", events.length, "allEvents.length:", allEvents.length); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -793,7 +900,7 @@
@@ -793,7 +900,7 @@
|
|
|
|
|
|
|
|
|
|
// React to display limit and allowed kinds changes |
|
|
|
|
$effect(() => { |
|
|
|
|
debug("Effect triggered: allEvents.length =", allEvents.length, "allowedKinds =", $visualizationConfig.allowedKinds); |
|
|
|
|
debug("Effect triggered: allEvents.length =", allEvents.length, "enabledKinds =", $visualizationConfig.eventConfigs.filter(ec => ec.enabled !== false).map(ec => ec.kind)); |
|
|
|
|
if (allEvents.length > 0) { |
|
|
|
|
const newEvents = filterByDisplayLimits(allEvents, $visualizationConfig); |
|
|
|
|
|
|
|
|
|
|