|
|
|
@ -1,6 +1,10 @@ |
|
|
|
/** |
|
|
|
/** |
|
|
|
* Event index loader for kind 30040 |
|
|
|
* Event index loader for kind 30040 (Publication Index per NKBIP-01) |
|
|
|
* Handles lazy-loading of event-index hierarchy with a-tags and e-tags |
|
|
|
* Handles lazy-loading of event-index hierarchy using a-tags (standard) and e-tags (optional) |
|
|
|
|
|
|
|
* A-tag format: ["a", "<kind:pubkey:dtag>", "<relay hint>", "<event id>"] |
|
|
|
|
|
|
|
* E-tag format: ["e", "<event id>", "<relay hint>"] |
|
|
|
|
|
|
|
* The event ID in a-tags (4th element) is optional and enables version tracking |
|
|
|
|
|
|
|
* A-tags are the standard method; e-tags are supported for backwards compatibility |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
|
@ -11,64 +15,118 @@ import { getEvent } from '../cache/event-cache.js'; |
|
|
|
export interface EventIndexItem { |
|
|
|
export interface EventIndexItem { |
|
|
|
event: NostrEvent; |
|
|
|
event: NostrEvent; |
|
|
|
order: number; // Original order in index
|
|
|
|
order: number; // Original order in index
|
|
|
|
|
|
|
|
level: number; // Nesting level (0 = root, 1 = first level nested, etc.)
|
|
|
|
|
|
|
|
children?: EventIndexItem[]; // Nested items if this is a kind 30040 index
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface MissingEventInfo { |
|
|
|
|
|
|
|
dTag: string; // The d-tag or event ID of the missing event
|
|
|
|
|
|
|
|
order: number; // Original order in index
|
|
|
|
|
|
|
|
type: 'a-tag' | 'e-tag'; // Whether referenced by a-tag (standard) or e-tag (optional)
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Load entire event-index hierarchy for a kind 30040 event (per NKBIP-01) |
|
|
|
|
|
|
|
* Uses a-tags (standard) and e-tags (optional) to reference events in desired display order |
|
|
|
|
|
|
|
* A-tags take precedence over e-tags when both reference the same event |
|
|
|
|
|
|
|
* Maintains original order from the index event |
|
|
|
|
|
|
|
* Recursively loads nested kind 30040 indexes |
|
|
|
|
|
|
|
* Returns both loaded items and information about missing events |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
export interface LoadEventIndexResult { |
|
|
|
|
|
|
|
items: EventIndexItem[]; |
|
|
|
|
|
|
|
missingEvents: MissingEventInfo[]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Load entire event-index hierarchy for a kind 30040 event |
|
|
|
* Internal recursive function to load event index hierarchy |
|
|
|
* Handles both a-tags and e-tags, maintains original order |
|
|
|
* @param opEvent The kind 30040 event to load |
|
|
|
|
|
|
|
* @param level Current nesting level (0 = root) |
|
|
|
|
|
|
|
* @param maxDepth Maximum recursion depth to prevent infinite loops (default: 10) |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
export async function loadEventIndex(opEvent: NostrEvent): Promise<EventIndexItem[]> { |
|
|
|
async function loadEventIndexRecursive( |
|
|
|
|
|
|
|
opEvent: NostrEvent, |
|
|
|
|
|
|
|
level: number = 0, |
|
|
|
|
|
|
|
maxDepth: number = 10 |
|
|
|
|
|
|
|
): Promise<LoadEventIndexResult> { |
|
|
|
if (opEvent.kind !== 30040) { |
|
|
|
if (opEvent.kind !== 30040) { |
|
|
|
throw new Error('Event is not kind 30040'); |
|
|
|
throw new Error('Event is not kind 30040'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Prevent infinite recursion
|
|
|
|
|
|
|
|
if (level >= maxDepth) { |
|
|
|
|
|
|
|
console.warn(`[EventIndex] Maximum recursion depth (${maxDepth}) reached for event ${opEvent.id}`); |
|
|
|
|
|
|
|
return { items: [], missingEvents: [] }; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Validate per NKBIP-01 spec
|
|
|
|
|
|
|
|
// The content field MUST be empty
|
|
|
|
|
|
|
|
if (opEvent.content && opEvent.content.trim() !== '') { |
|
|
|
|
|
|
|
console.warn('[EventIndex] Kind 30040 event has non-empty content (per NKBIP-01, content MUST be empty)'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MUST include a title tag
|
|
|
|
|
|
|
|
const titleTag = opEvent.tags.find(t => t[0] === 'title'); |
|
|
|
|
|
|
|
if (!titleTag || !titleTag[1]) { |
|
|
|
|
|
|
|
console.warn('[EventIndex] Kind 30040 event missing title tag (per NKBIP-01, title tag is REQUIRED)'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const items: EventIndexItem[] = []; |
|
|
|
const items: EventIndexItem[] = []; |
|
|
|
const loadedEventIds = new Set<string>(); |
|
|
|
const loadedEventIds = new Set<string>(); |
|
|
|
const loadedAddresses = new Set<string>(); |
|
|
|
const loadedAddresses = new Set<string>(); |
|
|
|
const missingIds: string[] = []; |
|
|
|
|
|
|
|
const missingAddresses: string[] = []; |
|
|
|
const missingAddresses: string[] = []; |
|
|
|
|
|
|
|
const missingEventIds: string[] = []; |
|
|
|
|
|
|
|
const missingEvents: MissingEventInfo[] = []; |
|
|
|
|
|
|
|
|
|
|
|
// Parse a-tags and e-tags from OP event
|
|
|
|
// Parse a-tags (standard) and e-tags (optional) from OP event
|
|
|
|
const aTags: string[] = []; |
|
|
|
// A-tag format per NKBIP-01: ["a", "<kind:pubkey:dtag>", "<relay hint>", "<event id>"]
|
|
|
|
const eTags: string[] = []; |
|
|
|
// E-tag format: ["e", "<event id>", "<relay hint>"]
|
|
|
|
|
|
|
|
// The event ID in a-tags (4th element) is optional and enables version tracking
|
|
|
|
|
|
|
|
interface ATagInfo { |
|
|
|
|
|
|
|
address: string; // kind:pubkey:dtag
|
|
|
|
|
|
|
|
relayHint?: string; // Optional relay hint (2nd element)
|
|
|
|
|
|
|
|
eventId?: string; // Optional event ID for version tracking (3rd element)
|
|
|
|
|
|
|
|
order: number; // Original order in index
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface ETagInfo { |
|
|
|
|
|
|
|
eventId: string; |
|
|
|
|
|
|
|
relayHint?: string; // Optional relay hint (2nd element)
|
|
|
|
|
|
|
|
order: number; // Original order in index
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const aTags: ATagInfo[] = []; |
|
|
|
|
|
|
|
const eTags: ETagInfo[] = []; |
|
|
|
|
|
|
|
|
|
|
|
for (const tag of opEvent.tags) { |
|
|
|
for (let i = 0; i < opEvent.tags.length; i++) { |
|
|
|
|
|
|
|
const tag = opEvent.tags[i]; |
|
|
|
if (tag[0] === 'a' && tag[1]) { |
|
|
|
if (tag[0] === 'a' && tag[1]) { |
|
|
|
aTags.push(tag[1]); |
|
|
|
aTags.push({ |
|
|
|
|
|
|
|
address: tag[1], |
|
|
|
|
|
|
|
relayHint: tag[2] || undefined, |
|
|
|
|
|
|
|
eventId: tag[3] || undefined, // Optional event ID for version tracking
|
|
|
|
|
|
|
|
order: i |
|
|
|
|
|
|
|
}); |
|
|
|
} else if (tag[0] === 'e' && tag[1]) { |
|
|
|
} else if (tag[0] === 'e' && tag[1]) { |
|
|
|
eTags.push(tag[1]); |
|
|
|
eTags.push({ |
|
|
|
|
|
|
|
eventId: tag[1], |
|
|
|
|
|
|
|
relayHint: tag[2] || undefined, |
|
|
|
|
|
|
|
order: i |
|
|
|
|
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// First pass: try to load all events from cache and relays
|
|
|
|
// First pass: try to load all events from cache and relays
|
|
|
|
const relays = relayManager.getProfileReadRelays(); |
|
|
|
const relays = relayManager.getProfileReadRelays(); |
|
|
|
|
|
|
|
|
|
|
|
// Load events by ID (e-tags)
|
|
|
|
// Track which event IDs are already loaded by a-tags (a-tags take precedence)
|
|
|
|
if (eTags.length > 0) { |
|
|
|
const eventIdsLoadedByATags = new Set<string>(); |
|
|
|
const eventsById = await nostrClient.fetchEvents( |
|
|
|
|
|
|
|
[{ ids: eTags, limit: eTags.length }], |
|
|
|
|
|
|
|
relays, |
|
|
|
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < eTags.length; i++) { |
|
|
|
|
|
|
|
const eventId = eTags[i]; |
|
|
|
|
|
|
|
const event = eventsById.find(e => e.id === eventId); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (event) { |
|
|
|
// Load events by address (a-tags) - per NKBIP-01, a-tags are the standard method
|
|
|
|
items.push({ event, order: i }); |
|
|
|
// Format: ["a", "<kind:pubkey:dtag>", "<relay hint>", "<event id>"]
|
|
|
|
loadedEventIds.add(eventId); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
missingIds.push(eventId); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Load events by address (a-tags)
|
|
|
|
|
|
|
|
if (aTags.length > 0) { |
|
|
|
if (aTags.length > 0) { |
|
|
|
for (let i = 0; i < aTags.length; i++) { |
|
|
|
for (const aTagInfo of aTags) { |
|
|
|
const aTag = aTags[i]; |
|
|
|
const parts = aTagInfo.address.split(':'); |
|
|
|
const parts = aTag.split(':'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (parts.length === 3) { |
|
|
|
if (parts.length === 3) { |
|
|
|
const kind = parseInt(parts[0], 10); |
|
|
|
const kind = parseInt(parts[0], 10); |
|
|
|
@ -76,74 +134,257 @@ export async function loadEventIndex(opEvent: NostrEvent): Promise<EventIndexIte |
|
|
|
const dTag = parts[2]; |
|
|
|
const dTag = parts[2]; |
|
|
|
|
|
|
|
|
|
|
|
if (!isNaN(kind) && pubkey && dTag) { |
|
|
|
if (!isNaN(kind) && pubkey && dTag) { |
|
|
|
// Fetch from relays (cache is checked inside fetchEvents)
|
|
|
|
let event: NostrEvent | undefined; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If event ID is provided in a-tag (4th element), try to fetch by ID first for version tracking
|
|
|
|
|
|
|
|
if (aTagInfo.eventId) { |
|
|
|
|
|
|
|
const eventsById = await nostrClient.fetchEvents( |
|
|
|
|
|
|
|
[{ ids: [aTagInfo.eventId], limit: 1 }], |
|
|
|
|
|
|
|
aTagInfo.relayHint ? [aTagInfo.relayHint] : relays, |
|
|
|
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (eventsById.length > 0) { |
|
|
|
|
|
|
|
const fetchedEvent = eventsById[0]; |
|
|
|
|
|
|
|
// 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; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If not found by event ID, or no event ID provided, fetch by address
|
|
|
|
|
|
|
|
if (!event) { |
|
|
|
|
|
|
|
const fetchRelays = aTagInfo.relayHint ? [aTagInfo.relayHint, ...relays] : relays; |
|
|
|
const events = await nostrClient.fetchEvents( |
|
|
|
const events = await nostrClient.fetchEvents( |
|
|
|
[{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }], |
|
|
|
[{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }], |
|
|
|
relays, |
|
|
|
fetchRelays, |
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (events.length > 0) { |
|
|
|
if (events.length > 0) { |
|
|
|
// Get newest version
|
|
|
|
// Get newest version (for replaceable events)
|
|
|
|
const event = events.sort((a, b) => b.created_at - a.created_at)[0]; |
|
|
|
event = events.sort((a, b) => b.created_at - a.created_at)[0]; |
|
|
|
items.push({ event, order: eTags.length + i }); |
|
|
|
} |
|
|
|
loadedAddresses.add(aTag); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (event) { |
|
|
|
|
|
|
|
// Check if this event is also a kind 30040 (nested index)
|
|
|
|
|
|
|
|
if (event.kind === 30040) { |
|
|
|
|
|
|
|
// Recursively load nested index
|
|
|
|
|
|
|
|
const nestedResult = await loadEventIndexRecursive(event, level + 1, maxDepth); |
|
|
|
|
|
|
|
// Create a parent item with children
|
|
|
|
|
|
|
|
const parentItem: EventIndexItem = { |
|
|
|
|
|
|
|
event, |
|
|
|
|
|
|
|
order: aTagInfo.order, |
|
|
|
|
|
|
|
level, |
|
|
|
|
|
|
|
children: nestedResult.items |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
items.push(parentItem); |
|
|
|
|
|
|
|
// Merge missing events from nested index
|
|
|
|
|
|
|
|
missingEvents.push(...nestedResult.missingEvents); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Regular event (content section)
|
|
|
|
|
|
|
|
items.push({ event, order: aTagInfo.order, level }); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
loadedAddresses.add(aTagInfo.address); |
|
|
|
|
|
|
|
if (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(aTag); |
|
|
|
missingAddresses.push(aTagInfo.address); |
|
|
|
|
|
|
|
missingEvents.push({ dTag, order: aTagInfo.order, type: 'a-tag' }); |
|
|
|
|
|
|
|
console.warn(`[EventIndex] Missing event referenced by a-tag: ${aTagInfo.address} (d-tag: ${dTag})`); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Second pass: retry missing events (but don't loop infinitely)
|
|
|
|
// Load events by ID (e-tags) - optional, for backwards compatibility
|
|
|
|
if (missingIds.length > 0 || missingAddresses.length > 0) { |
|
|
|
// Only load e-tags that weren't already loaded by a-tags
|
|
|
|
// Wait a bit before retry
|
|
|
|
// E-tags maintain their original order but come after a-tags
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
|
|
|
if (eTags.length > 0) { |
|
|
|
|
|
|
|
const eTagIdsToLoad = eTags |
|
|
|
|
|
|
|
.filter(eTag => !eventIdsLoadedByATags.has(eTag.eventId)) |
|
|
|
|
|
|
|
.map(eTag => eTag.eventId); |
|
|
|
|
|
|
|
|
|
|
|
// Retry missing IDs
|
|
|
|
if (eTagIdsToLoad.length > 0) { |
|
|
|
if (missingIds.length > 0) { |
|
|
|
// Collect relay hints from e-tags
|
|
|
|
const retryEvents = await nostrClient.fetchEvents( |
|
|
|
const relayHintsForETags = new Set<string>(); |
|
|
|
[{ ids: missingIds, limit: missingIds.length }], |
|
|
|
for (const eTag of eTags) { |
|
|
|
relays, |
|
|
|
if (eTag.relayHint && !eventIdsLoadedByATags.has(eTag.eventId)) { |
|
|
|
{ useCache: false, cacheResults: true } // Force relay query
|
|
|
|
relayHintsForETags.add(eTag.relayHint); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch all e-tag events at once
|
|
|
|
|
|
|
|
const allRelays = relayHintsForETags.size > 0
|
|
|
|
|
|
|
|
? [...new Set([...Array.from(relayHintsForETags), ...relays])] |
|
|
|
|
|
|
|
: relays; |
|
|
|
|
|
|
|
const eventsById = await nostrClient.fetchEvents( |
|
|
|
|
|
|
|
[{ ids: eTagIdsToLoad, limit: eTagIdsToLoad.length }], |
|
|
|
|
|
|
|
allRelays, |
|
|
|
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
for (const eventId of missingIds) { |
|
|
|
for (const eTagInfo of eTags) { |
|
|
|
const event = retryEvents.find(e => e.id === eventId); |
|
|
|
// Skip if already loaded by a-tag
|
|
|
|
|
|
|
|
if (eventIdsLoadedByATags.has(eTagInfo.eventId)) { |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Use relay hint if available
|
|
|
|
|
|
|
|
const fetchRelays = eTagInfo.relayHint
|
|
|
|
|
|
|
|
? [eTagInfo.relayHint, ...relays] |
|
|
|
|
|
|
|
: allRelays; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const event = eventsById.find(e => e.id === eTagInfo.eventId); |
|
|
|
|
|
|
|
|
|
|
|
if (event) { |
|
|
|
if (event) { |
|
|
|
const originalIndex = eTags.indexOf(eventId); |
|
|
|
// Check if this event is also a kind 30040 (nested index)
|
|
|
|
if (originalIndex >= 0) { |
|
|
|
if (event.kind === 30040) { |
|
|
|
items.push({ event, order: originalIndex }); |
|
|
|
// Recursively load nested index
|
|
|
|
loadedEventIds.add(eventId); |
|
|
|
const nestedResult = await loadEventIndexRecursive(event, level + 1, maxDepth); |
|
|
|
|
|
|
|
// Create a parent item with children
|
|
|
|
|
|
|
|
const parentItem: EventIndexItem = { |
|
|
|
|
|
|
|
event, |
|
|
|
|
|
|
|
order: eTagInfo.order, |
|
|
|
|
|
|
|
level, |
|
|
|
|
|
|
|
children: nestedResult.items |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
items.push(parentItem); |
|
|
|
|
|
|
|
// Merge missing events from nested index
|
|
|
|
|
|
|
|
missingEvents.push(...nestedResult.missingEvents); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Regular event (content section)
|
|
|
|
|
|
|
|
items.push({ event, order: eTagInfo.order, level }); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
loadedEventIds.add(eTagInfo.eventId); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
missingEventIds.push(eTagInfo.eventId); |
|
|
|
|
|
|
|
// For e-tags, try to extract d-tag from the event if we can find it
|
|
|
|
|
|
|
|
// Otherwise use event ID as identifier
|
|
|
|
|
|
|
|
let dTag = eTagInfo.eventId; |
|
|
|
|
|
|
|
// Try to find a corresponding a-tag that might have the d-tag
|
|
|
|
|
|
|
|
const correspondingATag = aTags.find(aTag => aTag.eventId === eTagInfo.eventId); |
|
|
|
|
|
|
|
if (correspondingATag) { |
|
|
|
|
|
|
|
const parts = correspondingATag.address.split(':'); |
|
|
|
|
|
|
|
if (parts.length === 3) { |
|
|
|
|
|
|
|
dTag = parts[2]; // Extract d-tag from a-tag
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
missingEvents.push({ dTag, order: eTagInfo.order, type: 'e-tag' }); |
|
|
|
|
|
|
|
console.warn(`[EventIndex] Missing event referenced by e-tag: ${eTagInfo.eventId} (displaying as: ${dTag})`); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second pass: retry missing events (but don't loop infinitely)
|
|
|
|
|
|
|
|
if (missingAddresses.length > 0 || missingEventIds.length > 0) { |
|
|
|
|
|
|
|
// Wait a bit before retry
|
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
|
|
|
|
|
|
|
|
|
|
|
// Retry missing addresses
|
|
|
|
// Retry missing addresses
|
|
|
|
if (missingAddresses.length > 0) { |
|
|
|
for (const aTagInfo of aTags) { |
|
|
|
for (const aTag of missingAddresses) { |
|
|
|
if (missingAddresses.includes(aTagInfo.address)) { |
|
|
|
const parts = aTag.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) { |
|
|
|
|
|
|
|
const fetchRelays = aTagInfo.relayHint ? [aTagInfo.relayHint, ...relays] : relays; |
|
|
|
const retryEvents = await nostrClient.fetchEvents( |
|
|
|
const retryEvents = await nostrClient.fetchEvents( |
|
|
|
[{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }], |
|
|
|
[{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }], |
|
|
|
relays, |
|
|
|
fetchRelays, |
|
|
|
{ useCache: false, cacheResults: true } // Force relay query
|
|
|
|
{ useCache: false, cacheResults: true } // Force relay query
|
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (retryEvents.length > 0) { |
|
|
|
if (retryEvents.length > 0) { |
|
|
|
const event = retryEvents.sort((a, b) => b.created_at - a.created_at)[0]; |
|
|
|
const event = retryEvents.sort((a, b) => b.created_at - a.created_at)[0]; |
|
|
|
const originalIndex = aTags.indexOf(aTag); |
|
|
|
// Check if this event is also a kind 30040 (nested index)
|
|
|
|
if (originalIndex >= 0) { |
|
|
|
if (event.kind === 30040) { |
|
|
|
items.push({ event, order: eTags.length + originalIndex }); |
|
|
|
// Recursively load nested index
|
|
|
|
loadedAddresses.add(aTag); |
|
|
|
const nestedResult = await loadEventIndexRecursive(event, level + 1, maxDepth); |
|
|
|
|
|
|
|
// Create a parent item with children
|
|
|
|
|
|
|
|
const parentItem: EventIndexItem = { |
|
|
|
|
|
|
|
event, |
|
|
|
|
|
|
|
order: aTagInfo.order, |
|
|
|
|
|
|
|
level, |
|
|
|
|
|
|
|
children: nestedResult.items |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
items.push(parentItem); |
|
|
|
|
|
|
|
// Merge missing events from nested index
|
|
|
|
|
|
|
|
missingEvents.push(...nestedResult.missingEvents); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Regular event (content section)
|
|
|
|
|
|
|
|
items.push({ event, order: aTagInfo.order, level }); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
loadedAddresses.add(aTagInfo.address); |
|
|
|
|
|
|
|
// Remove from missing events since we found it
|
|
|
|
|
|
|
|
const missingIndex = missingEvents.findIndex(m => m.order === aTagInfo.order && m.type === 'a-tag' && m.dTag === dTag); |
|
|
|
|
|
|
|
if (missingIndex >= 0) { |
|
|
|
|
|
|
|
missingEvents.splice(missingIndex, 1); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Remove from missing addresses list
|
|
|
|
|
|
|
|
const addrIndex = missingAddresses.indexOf(aTagInfo.address); |
|
|
|
|
|
|
|
if (addrIndex >= 0) { |
|
|
|
|
|
|
|
missingAddresses.splice(addrIndex, 1); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Retry missing e-tag events
|
|
|
|
|
|
|
|
if (missingEventIds.length > 0) { |
|
|
|
|
|
|
|
const retryEvents = await nostrClient.fetchEvents( |
|
|
|
|
|
|
|
[{ ids: missingEventIds, limit: missingEventIds.length }], |
|
|
|
|
|
|
|
relays, |
|
|
|
|
|
|
|
{ useCache: false, cacheResults: true } // Force relay query
|
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const eTagInfo of eTags) { |
|
|
|
|
|
|
|
if (missingEventIds.includes(eTagInfo.eventId)) { |
|
|
|
|
|
|
|
const event = retryEvents.find(e => e.id === eTagInfo.eventId); |
|
|
|
|
|
|
|
if (event) { |
|
|
|
|
|
|
|
// Check if this event is also a kind 30040 (nested index)
|
|
|
|
|
|
|
|
if (event.kind === 30040) { |
|
|
|
|
|
|
|
// Recursively load nested index
|
|
|
|
|
|
|
|
const nestedResult = await loadEventIndexRecursive(event, level + 1, maxDepth); |
|
|
|
|
|
|
|
// Create a parent item with children
|
|
|
|
|
|
|
|
const parentItem: EventIndexItem = { |
|
|
|
|
|
|
|
event, |
|
|
|
|
|
|
|
order: eTagInfo.order, |
|
|
|
|
|
|
|
level, |
|
|
|
|
|
|
|
children: nestedResult.items |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
items.push(parentItem); |
|
|
|
|
|
|
|
// Merge missing events from nested index
|
|
|
|
|
|
|
|
missingEvents.push(...nestedResult.missingEvents); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Regular event (content section)
|
|
|
|
|
|
|
|
items.push({ event, order: eTagInfo.order, level }); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
loadedEventIds.add(eTagInfo.eventId); |
|
|
|
|
|
|
|
// Remove from missing events
|
|
|
|
|
|
|
|
const missingIndex = missingEvents.findIndex(m => m.order === eTagInfo.order && m.type === 'e-tag'); |
|
|
|
|
|
|
|
if (missingIndex >= 0) { |
|
|
|
|
|
|
|
missingEvents.splice(missingIndex, 1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Remove from missing IDs list
|
|
|
|
|
|
|
|
const idIndex = missingEventIds.indexOf(eTagInfo.eventId); |
|
|
|
|
|
|
|
if (idIndex >= 0) { |
|
|
|
|
|
|
|
missingEventIds.splice(idIndex, 1); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -153,6 +394,14 @@ export async function loadEventIndex(opEvent: NostrEvent): Promise<EventIndexIte |
|
|
|
|
|
|
|
|
|
|
|
// Sort by original order
|
|
|
|
// Sort by original order
|
|
|
|
items.sort((a, b) => a.order - b.order); |
|
|
|
items.sort((a, b) => a.order - b.order); |
|
|
|
|
|
|
|
missingEvents.sort((a, b) => a.order - b.order); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { items, missingEvents }; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return items; |
|
|
|
/** |
|
|
|
|
|
|
|
* Public function to load event index hierarchy (starts at level 0) |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
export async function loadEventIndex(opEvent: NostrEvent): Promise<LoadEventIndexResult> { |
|
|
|
|
|
|
|
return loadEventIndexRecursive(opEvent, 0); |
|
|
|
} |
|
|
|
} |
|
|
|
|