Browse Source

30040 e-books

master
Silberengel 1 month ago
parent
commit
13e136d89a
  1. 7
      src/lib/components/content/MarkdownRenderer.svelte
  2. 168
      src/lib/modules/events/EventView.svelte
  3. 11
      src/lib/services/nostr/config.ts
  4. 195
      src/lib/services/nostr/event-index-loader.ts

7
src/lib/components/content/MarkdownRenderer.svelte

@ -465,11 +465,13 @@
const isKind30040 = $derived(event?.kind === 30040); const isKind30040 = $derived(event?.kind === 30040);
// Load highlights for event // Load highlights for event
// Highlights are loaded for all content events (kind 30041, kind 1, etc.)
// but NOT for kind 30040 index events (which have no content)
async function loadHighlights(abortSignal: AbortSignal) { async function loadHighlights(abortSignal: AbortSignal) {
if (!event || !content) return; if (!event || !content) return;
// Skip highlights for kind 30040 index events (they have no content) // Skip highlights for kind 30040 index events (they have no content)
// Highlights should be loaded for the indexed sections instead // Highlights should be loaded for the indexed sections (kind 30041) instead
if (isKind30040 && !content.trim()) { if (isKind30040 && !content.trim()) {
if (!abortSignal.aborted) { if (!abortSignal.aborted) {
highlightsLoaded = true; highlightsLoaded = true;
@ -486,7 +488,8 @@
} }
try { try {
// Load highlights for this specific event (which could be a section event) // Load highlights for this specific event (kind 30041, kind 1, etc.)
// Highlights will be displayed with user badges and links to the highlight events
const dTag = event.tags.find(t => t[0] === 'd' && t[1])?.[1]; const dTag = event.tags.find(t => t[0] === 'd' && t[1])?.[1];
const eventHighlights = await getHighlightsForEvent( const eventHighlights = await getHighlightsForEvent(
event.id, event.id,

168
src/lib/modules/events/EventView.svelte

@ -63,6 +63,7 @@
let totalItemCount = $derived(countTotalItems(eventIndexItems)); let totalItemCount = $derived(countTotalItems(eventIndexItems));
// Check if event is metadata-only (no content to display) // Check if event is metadata-only (no content to display)
// Note: kind 30040 (Publication Index) is excluded because it should render its children
let isMetadataOnly = $derived(rootEvent ? ( let isMetadataOnly = $derived(rootEvent ? (
rootEvent.kind === KIND.METADATA || rootEvent.kind === KIND.METADATA ||
rootEvent.kind === KIND.RELAY_LIST || rootEvent.kind === KIND.RELAY_LIST ||
@ -70,7 +71,7 @@
rootEvent.kind === KIND.BLOCKED_RELAYS || rootEvent.kind === KIND.BLOCKED_RELAYS ||
rootEvent.kind === KIND.LOCAL_RELAYS || rootEvent.kind === KIND.LOCAL_RELAYS ||
rootEvent.kind === KIND.USER_STATUS || rootEvent.kind === KIND.USER_STATUS ||
(rootEvent.content === '' && rootEvent.tags.length > 0) (rootEvent.content === '' && rootEvent.tags.length > 0 && rootEvent.kind !== 30040)
) : false); ) : false);
onMount(async () => { onMount(async () => {
@ -130,6 +131,15 @@
eventIndexItems = result.items; eventIndexItems = result.items;
missingEvents = result.missingEvents; missingEvents = result.missingEvents;
console.log(`Loaded ${result.items.length} events from index hierarchy`); console.log(`Loaded ${result.items.length} events from index hierarchy`);
// Debug: log items with children
const itemsWithChildren = result.items.filter(item => item.children && item.children.length > 0);
console.log(`[EventView] Items with children: ${itemsWithChildren.length}`, itemsWithChildren.map(item => ({
id: item.event.id,
kind: item.event.kind,
level: item.level,
childrenCount: item.children?.length || 0,
title: item.event.tags.find(t => t[0] === 'title')?.[1]
})));
if (result.missingEvents.length > 0) { if (result.missingEvents.length > 0) {
console.warn(`[EventView] ${result.missingEvents.length} events are missing from the index hierarchy`); console.warn(`[EventView] ${result.missingEvents.length} events are missing from the index hierarchy`);
} }
@ -148,14 +158,114 @@
<p class="text-fog-text dark:text-fog-dark-text error-message">Error: {error}</p> <p class="text-fog-text dark:text-fog-dark-text error-message">Error: {error}</p>
{:else if rootEvent} {:else if rootEvent}
<article class="event-view"> <article class="event-view">
<!-- Display title prominently for kind 30040 (book index) --> <!-- Display title and metadata for kind 30040 (book index) -->
{#if rootEvent.kind === 30040} {#if rootEvent.kind === 30040}
{@const titleTag = rootEvent.tags.find(t => t[0] === 'title')} {@const titleTag = rootEvent.tags.find(t => t[0] === 'title')}
{@const authorTag = rootEvent.tags.find(t => t[0] === 'author')}
{@const typeTag = rootEvent.tags.find(t => t[0] === 'type')}
{@const versionTag = rootEvent.tags.find(t => t[0] === 'version')}
{@const publishedOnTag = rootEvent.tags.find(t => t[0] === 'published_on')}
{@const publishedByTag = rootEvent.tags.find(t => t[0] === 'published_by')}
{@const imageTag = rootEvent.tags.find(t => t[0] === 'image')}
{@const summaryTag = rootEvent.tags.find(t => t[0] === 'summary')}
{@const sourceTag = rootEvent.tags.find(t => t[0] === 'source')}
{@const iTags = rootEvent.tags.filter(t => t[0] === 'i')}
{@const tTags = rootEvent.tags.filter(t => t[0] === 't')}
{@const pTag = rootEvent.tags.find(t => t[0] === 'p')}
{@const ETag = rootEvent.tags.find(t => t[0] === 'E')}
{#if titleTag && titleTag[1]} {#if titleTag && titleTag[1]}
<h1 class="book-title text-3xl font-bold mb-6 text-fog-text dark:text-fog-dark-text"> <h1 class="book-title text-3xl font-bold mb-6 text-fog-text dark:text-fog-dark-text">
{titleTag[1]} {titleTag[1]}
</h1> </h1>
{/if} {/if}
<!-- Publication metadata -->
<div class="publication-metadata mb-6 space-y-2">
{#if authorTag && authorTag[1]}
<p class="text-fog-text dark:text-fog-dark-text">
<span class="font-semibold">Author:</span> {authorTag[1]}
</p>
{/if}
{#if typeTag && typeTag[1]}
<p class="text-fog-text dark:text-fog-dark-text">
<span class="font-semibold">Type:</span> {typeTag[1]}
</p>
{/if}
{#if versionTag && versionTag[1]}
<p class="text-fog-text dark:text-fog-dark-text">
<span class="font-semibold">Version:</span> {versionTag[1]}
</p>
{/if}
{#if publishedOnTag && publishedOnTag[1]}
<p class="text-fog-text dark:text-fog-dark-text">
<span class="font-semibold">Published:</span> {publishedOnTag[1]}
{#if publishedByTag && publishedByTag[1]}
<span> by {publishedByTag[1]}</span>
{/if}
</p>
{:else if publishedByTag && publishedByTag[1]}
<p class="text-fog-text dark:text-fog-dark-text">
<span class="font-semibold">Published by:</span> {publishedByTag[1]}
</p>
{/if}
{#if iTags.length > 0}
{@const plural = iTags.length > 1 ? 's' : ''}
<p class="text-fog-text dark:text-fog-dark-text">
<span class="font-semibold">Identifier{plural}:</span>
{#each iTags as iTag, index}
{iTag[1]}{#if index < iTags.length - 1}, {/if}
{/each}
</p>
{/if}
{#if tTags.length > 0}
{@const plural = tTags.length > 1 ? 's' : ''}
<p class="text-fog-text dark:text-fog-dark-text">
<span class="font-semibold">Topic{plural}:</span>
{#each tTags as tTag}
<span class="topic-tag">{tTag[1]}</span>
{/each}
</p>
{/if}
{#if sourceTag && sourceTag[1]}
<p class="text-fog-text dark:text-fog-dark-text">
<span class="font-semibold">Source:</span>
<a href={sourceTag[1]} target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline">
{sourceTag[1]}
</a>
</p>
{/if}
{#if summaryTag && summaryTag[1]}
<p class="text-fog-text dark:text-fog-dark-text mt-4">
{summaryTag[1]}
</p>
{/if}
{#if imageTag && imageTag[1]}
<div class="publication-cover-image mt-4">
<img src={imageTag[1]} alt="Cover image for {titleTag?.[1] || 'publication'}" class="max-w-md rounded-lg shadow-lg" />
</div>
{/if}
{#if pTag && pTag[1]}
<p class="text-fog-text dark:text-fog-dark-text mt-4 text-sm italic">
<span class="font-semibold">Original author:</span>
<a href="/profile/{pTag[1]}" class="text-blue-600 dark:text-blue-400 hover:underline">
{pTag[1]}
</a>
{#if ETag && ETag[1]}
<span> (derivative work)</span>
{/if}
</p>
{/if}
</div>
{/if} {/if}
<!-- Display metadata-only events with MetadataCard --> <!-- Display metadata-only events with MetadataCard -->
@ -191,12 +301,12 @@
{#snippet renderIndexItems(items: EventIndexItem[], parentLevel: number = 0)} {#snippet renderIndexItems(items: EventIndexItem[], parentLevel: number = 0)}
{#each items as item (item.event.id)} {#each items as item (item.event.id)}
<div class="event-index-item" data-index={item.order} data-level={item.level}> <div class="event-index-item" data-index={item.order} data-level={item.level}>
<!-- Display title for nested kind 30040 indexes (chapter/subchapter indexes) --> <!-- Display title for kind 30040 indexes (chapter/subchapter indexes) -->
{#if item.event.kind === 30040 && item.level > 0} {#if item.event.kind === 30040}
{@const indexTitleTag = item.event.tags.find(t => t[0] === 'title')} {@const indexTitleTag = item.event.tags.find(t => t[0] === 'title')}
{#if indexTitleTag && indexTitleTag[1]} {#if indexTitleTag && indexTitleTag[1]}
{@const headingLevel = Math.min(2 + item.level, 6)} {@const headingLevel = Math.min(2 + item.level, 6)}
{@const textSize = item.level === 1 ? 'xl' : item.level === 2 ? 'lg' : 'base'} {@const textSize = item.level === 0 ? 'xl' : item.level === 1 ? 'lg' : 'base'}
{#if headingLevel === 2} {#if headingLevel === 2}
<h2 class="index-title text-{textSize} font-semibold mb-3 mt-4 text-fog-text dark:text-fog-dark-text"> <h2 class="index-title text-{textSize} font-semibold mb-3 mt-4 text-fog-text dark:text-fog-dark-text">
{indexTitleTag[1]} {indexTitleTag[1]}
@ -219,10 +329,15 @@
</h6> </h6>
{/if} {/if}
{/if} {/if}
{/if}
<!-- Recursively render children of kind 30040 indexes -->
{#if item.children && item.children.length > 0}
<div class="nested-index" style="margin-left: {item.level * 1.5}rem; margin-top: 1rem;">
{@render renderIndexItems(item.children, item.level)}
</div>
{/if}
{:else}
<!-- For content events (not kind 30040 indexes), render the content --> <!-- For content events (not kind 30040 indexes), render the content -->
{#if item.event.kind !== 30040}
<!-- Display chapter title prominently for kind 30041 (chapter sections) --> <!-- Display chapter title prominently for kind 30041 (chapter sections) -->
{#if item.event.kind === 30041 || item.event.kind === 1 || item.event.kind === 30817} {#if item.event.kind === 30041 || item.event.kind === 1 || item.event.kind === 30817}
{@const chapterTitleTag = item.event.tags.find(t => t[0] === 'title')} {@const chapterTitleTag = item.event.tags.find(t => t[0] === 'title')}
@ -243,14 +358,14 @@
<CommentThread threadId={item.event.id} event={item.event} /> <CommentThread threadId={item.event.id} event={item.event} />
</div> </div>
</div> </div>
{/if}
<!-- Recursively render children if this is a nested index --> <!-- Recursively render children if this content event has nested children -->
{#if item.children && item.children.length > 0} {#if item.children && item.children.length > 0}
<div class="nested-index" style="margin-left: {item.level * 1.5}rem; margin-top: 1rem;"> <div class="nested-index" style="margin-left: {item.level * 1.5}rem; margin-top: 1rem;">
{@render renderIndexItems(item.children, item.level)} {@render renderIndexItems(item.children, item.level)}
</div> </div>
{/if} {/if}
{/if}
</div> </div>
{/each} {/each}
{/snippet} {/snippet}
@ -350,6 +465,41 @@
border-bottom-color: var(--fog-dark-border, #374151); border-bottom-color: var(--fog-dark-border, #374151);
} }
.publication-metadata {
padding: 1rem;
background: var(--fog-bg-secondary, #f9fafb);
border-radius: 0.5rem;
border: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .publication-metadata {
background: var(--fog-dark-bg-secondary, #1f2937);
border-color: var(--fog-dark-border, #374151);
}
.topic-tag {
display: inline-block;
padding: 0.25rem 0.5rem;
margin: 0.125rem;
background: var(--fog-accent, #3b82f6);
color: white;
border-radius: 0.25rem;
font-size: 0.875rem;
}
:global(.dark) .topic-tag {
background: var(--fog-dark-accent, #2563eb);
}
.publication-cover-image {
margin-top: 1rem;
}
.publication-cover-image img {
max-width: 100%;
height: auto;
}
.chapter-title { .chapter-title {
margin-top: 1.5rem; margin-top: 1.5rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;

11
src/lib/services/nostr/config.ts

@ -5,17 +5,18 @@
const DEFAULT_RELAYS = [ const DEFAULT_RELAYS = [
'wss://theforest.nostr1.com', 'wss://theforest.nostr1.com',
'wss://nostr21.com', 'wss://thecitadel.nostr1.com',
'wss://nostr.land', 'wss://nostr.land'
'wss://orly-relay.imwald.eu',
'wss://nostr.wine'
]; ];
const PROFILE_RELAYS = [ const PROFILE_RELAYS = [
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://aggr.nostr.land', 'wss://aggr.nostr.land',
'wss://profiles.nostr1.com', 'wss://profiles.nostr1.com',
'wss://relay.primal.net' 'wss://relay.primal.net',
'wss://orly-relay.imwald.eu',
'wss://nostr.wine',
'wss://nostr21.com'
]; ];
const THREAD_PUBLISH_RELAYS = [ const THREAD_PUBLISH_RELAYS = [

195
src/lib/services/nostr/event-index-loader.ts

@ -38,6 +38,17 @@ export interface LoadEventIndexResult {
missingEvents: MissingEventInfo[]; missingEvents: MissingEventInfo[];
} }
/**
* Chunk an array into batches of specified size
*/
function chunkArray<T>(array: T[], chunkSize: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
/** /**
* Internal recursive function to load event index hierarchy * Internal recursive function to load event index hierarchy
* @param opEvent The kind 30040 event to load * @param opEvent The kind 30040 event to load
@ -122,59 +133,171 @@ async function loadEventIndexRecursive(
// Track which event IDs are already loaded by a-tags (a-tags take precedence) // Track which event IDs are already loaded by a-tags (a-tags take precedence)
const eventIdsLoadedByATags = new Set<string>(); const eventIdsLoadedByATags = new Set<string>();
// Batch fetch all a-tags and e-tags in parallel
// Load events by address (a-tags) - per NKBIP-01, a-tags are the standard method // Load events by address (a-tags) - per NKBIP-01, a-tags are the standard method
// Format: ["a", "<kind:pubkey:dtag>", "<relay hint>", "<event id>"] // Format: ["a", "<kind:pubkey:dtag>", "<relay hint>", "<event id>"]
if (aTags.length > 0) {
// Collect all event IDs from a-tags for batch fetching
const aTagEventIds: string[] = [];
const aTagEventIdMap = new Map<string, ATagInfo>(); // eventId -> aTagInfo
// Collect all address queries for a-tags (group by kind/pubkey/d-tag)
const addressQueries: Array<{ kind: number; pubkey: string; dTag: string; aTagInfo: ATagInfo }> = [];
for (const aTagInfo of aTags) { for (const aTagInfo of aTags) {
const parts = aTagInfo.address.split(':'); const parts = aTagInfo.address.split(':');
if (parts.length === 3) { if (parts.length === 3) {
const kind = parseInt(parts[0], 10); const kind = parseInt(parts[0], 10);
const pubkey = parts[1]; const pubkey = parts[1];
const dTag = parts[2]; const dTag = parts[2];
if (!isNaN(kind) && pubkey && dTag) { if (!isNaN(kind) && pubkey && dTag) {
let event: NostrEvent | undefined; // If event ID is provided, add to batch fetch list
// If event ID is provided in a-tag (4th element), try to fetch by ID first for version tracking
if (aTagInfo.eventId) { if (aTagInfo.eventId) {
const eventsById = await nostrClient.fetchEvents( aTagEventIds.push(aTagInfo.eventId);
[{ ids: [aTagInfo.eventId], limit: 1 }], aTagEventIdMap.set(aTagInfo.eventId, aTagInfo);
aTagInfo.relayHint ? [aTagInfo.relayHint] : relays, }
// Also add address query
addressQueries.push({ kind, pubkey, dTag, aTagInfo });
}
}
}
// Collect all relay hints from a-tags
const relayHintsForATags = new Set<string>();
for (const aTagInfo of aTags) {
if (aTagInfo.relayHint) {
relayHintsForATags.add(aTagInfo.relayHint);
}
}
const allRelaysForATags = relayHintsForATags.size > 0
? [...new Set([...Array.from(relayHintsForATags), ...relays])]
: relays;
// Batch fetch all a-tag event IDs in parallel (chunked to 100 per batch)
const BATCH_SIZE = 100;
const aTagEventsById: NostrEvent[] = [];
if (aTagEventIds.length > 0) {
const aTagEventIdChunks = chunkArray(aTagEventIds, BATCH_SIZE);
const aTagFetchPromises = aTagEventIdChunks.map(chunk =>
nostrClient.fetchEvents(
[{ ids: chunk, limit: chunk.length }],
allRelaysForATags,
{ useCache: true, cacheResults: true } { useCache: true, cacheResults: true }
)
); );
const aTagFetchResults = await Promise.all(aTagFetchPromises);
if (eventsById.length > 0) { for (const result of aTagFetchResults) {
const fetchedEvent = eventsById[0]; aTagEventsById.push(...result);
// Verify the event matches the address (kind, pubkey, d-tag)
if (fetchedEvent.kind === kind &&
fetchedEvent.pubkey === pubkey &&
fetchedEvent.tags.some(t => t[0] === 'd' && t[1] === dTag)) {
event = fetchedEvent;
} }
} }
// Group address queries by (kind, pubkey) to batch fetch
const addressQueryGroups = new Map<string, Array<{ dTag: string; aTagInfo: ATagInfo }>>();
for (const query of addressQueries) {
const key = `${query.kind}:${query.pubkey}`;
if (!addressQueryGroups.has(key)) {
addressQueryGroups.set(key, []);
}
addressQueryGroups.get(key)!.push({ dTag: query.dTag, aTagInfo: query.aTagInfo });
} }
// If not found by event ID, or no event ID provided, fetch by address // Batch fetch all addresses in parallel (chunked to 100 per batch)
if (!event) { const addressFetchPromises: Promise<NostrEvent[]>[] = [];
const fetchRelays = aTagInfo.relayHint ? [aTagInfo.relayHint, ...relays] : relays; const addressFetchMap = new Map<string, ATagInfo>(); // query key -> aTagInfo
const events = await nostrClient.fetchEvents(
[{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }], for (const [key, queries] of addressQueryGroups.entries()) {
fetchRelays, const [kindStr, pubkey] = key.split(':');
const kind = parseInt(kindStr, 10);
// Chunk queries into batches of 100
const queryChunks = chunkArray(queries, BATCH_SIZE);
for (const queryChunk of queryChunks) {
const dTags = queryChunk.map(q => q.dTag);
// Create fetch promise for this chunk
const fetchPromise = nostrClient.fetchEvents(
[{ kinds: [kind], authors: [pubkey], '#d': dTags, limit: dTags.length }],
allRelaysForATags,
{ useCache: true, cacheResults: true } { useCache: true, cacheResults: true }
); );
if (events.length > 0) { addressFetchPromises.push(fetchPromise);
// Get newest version (for replaceable events) // Store mapping for each d-tag in this chunk
event = events.sort((a, b) => b.created_at - a.created_at)[0]; for (const query of queryChunk) {
addressFetchMap.set(`${key}:${query.dTag}`, query.aTagInfo);
}
} }
} }
// Wait for all address fetches in parallel
const addressFetchResults = await Promise.all(addressFetchPromises);
const allAddressEvents: NostrEvent[] = [];
for (const result of addressFetchResults) {
allAddressEvents.push(...result);
}
// Process a-tag results
const aTagResults = new Map<string, NostrEvent>(); // address or eventId -> event
// Process events fetched by ID
for (const event of aTagEventsById) {
const aTagInfo = aTagEventIdMap.get(event.id);
if (aTagInfo) {
const parts = aTagInfo.address.split(':');
if (parts.length === 3) {
const kind = parseInt(parts[0], 10);
const pubkey = parts[1];
const dTag = parts[2];
// Verify the event matches the address
if (event.kind === kind &&
event.pubkey === pubkey &&
event.tags.some(t => t[0] === 'd' && t[1] === dTag)) {
aTagResults.set(aTagInfo.address, event);
eventIdsLoadedByATags.add(event.id);
}
}
}
}
// Process events fetched by address
for (const event of allAddressEvents) {
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
if (dTag) {
const key = `${event.kind}:${event.pubkey}:${dTag}`;
const aTagInfo = addressFetchMap.get(key);
if (aTagInfo && !aTagResults.has(aTagInfo.address)) {
// Get newest version if multiple found
const existing = aTagResults.get(aTagInfo.address);
if (!existing || event.created_at > existing.created_at) {
aTagResults.set(aTagInfo.address, event);
eventIdsLoadedByATags.add(event.id);
}
}
}
}
// Process all a-tag results
console.log(`[EventIndex] Processing ${aTags.length} a-tags, found ${aTagResults.size} events`);
for (const aTagInfo of aTags) {
const parts = aTagInfo.address.split(':');
if (parts.length === 3) {
const kind = parseInt(parts[0], 10);
const pubkey = parts[1];
const dTag = parts[2];
if (!isNaN(kind) && pubkey && dTag) {
const event = aTagResults.get(aTagInfo.address);
if (event) { if (event) {
console.log(`[EventIndex] Processing event ${event.id}, kind ${event.kind}, level ${level}`);
// Check if this event is also a kind 30040 (nested index) // Check if this event is also a kind 30040 (nested index)
if (event.kind === 30040) { if (event.kind === 30040) {
console.log(`[EventIndex] Found kind 30040 event ${event.id} at level ${level}, loading children...`);
// Recursively load nested index // Recursively load nested index
const nestedResult = await loadEventIndexRecursive(event, level + 1, maxDepth); const nestedResult = await loadEventIndexRecursive(event, level + 1, maxDepth);
console.log(`[EventIndex] Loaded nested index ${event.id} with ${nestedResult.items.length} children at level ${level + 1}`, nestedResult.items.map(i => ({ id: i.event.id, kind: i.event.kind, level: i.level })));
// Create a parent item with children // Create a parent item with children
const parentItem: EventIndexItem = { const parentItem: EventIndexItem = {
event, event,
@ -192,10 +315,7 @@ async function loadEventIndexRecursive(
loadedAddresses.add(aTagInfo.address); loadedAddresses.add(aTagInfo.address);
if (aTagInfo.eventId) { if (aTagInfo.eventId) {
loadedEventIds.add(aTagInfo.eventId); loadedEventIds.add(aTagInfo.eventId);
eventIdsLoadedByATags.add(aTagInfo.eventId);
} }
// Also track by event ID if we have it
eventIdsLoadedByATags.add(event.id);
} else { } else {
missingAddresses.push(aTagInfo.address); missingAddresses.push(aTagInfo.address);
missingEvents.push({ dTag, order: aTagInfo.order, type: 'a-tag' }); missingEvents.push({ dTag, order: aTagInfo.order, type: 'a-tag' });
@ -204,7 +324,6 @@ async function loadEventIndexRecursive(
} }
} }
} }
}
// Load events by ID (e-tags) - optional, for backwards compatibility // Load events by ID (e-tags) - optional, for backwards compatibility
// Only load e-tags that weren't already loaded by a-tags // Only load e-tags that weren't already loaded by a-tags
@ -223,15 +342,24 @@ async function loadEventIndexRecursive(
} }
} }
// Fetch all e-tag events at once // Fetch all e-tag events in batches of 100
const allRelays = relayHintsForETags.size > 0 const allRelays = relayHintsForETags.size > 0
? [...new Set([...Array.from(relayHintsForETags), ...relays])] ? [...new Set([...Array.from(relayHintsForETags), ...relays])]
: relays; : relays;
const eventsById = await nostrClient.fetchEvents(
[{ ids: eTagIdsToLoad, limit: eTagIdsToLoad.length }], const eTagIdChunks = chunkArray(eTagIdsToLoad, BATCH_SIZE);
const eTagFetchPromises = eTagIdChunks.map(chunk =>
nostrClient.fetchEvents(
[{ ids: chunk, limit: chunk.length }],
allRelays, allRelays,
{ useCache: true, cacheResults: true } { useCache: true, cacheResults: true }
)
); );
const eTagFetchResults = await Promise.all(eTagFetchPromises);
const eventsById: NostrEvent[] = [];
for (const result of eTagFetchResults) {
eventsById.push(...result);
}
for (const eTagInfo of eTags) { for (const eTagInfo of eTags) {
// Skip if already loaded by a-tag // Skip if already loaded by a-tag
@ -396,6 +524,9 @@ async function loadEventIndexRecursive(
items.sort((a, b) => a.order - b.order); items.sort((a, b) => a.order - b.order);
missingEvents.sort((a, b) => a.order - b.order); missingEvents.sort((a, b) => a.order - b.order);
const itemsWithChildren = items.filter(item => item.children && item.children.length > 0);
console.log(`[EventIndex] Returning ${items.length} items (${itemsWithChildren.length} with children) at level ${level}`);
return { items, missingEvents }; return { items, missingEvents };
} }

Loading…
Cancel
Save