fix: prevent cascading hydration loops in bookmark controller

Run all coordinate queries in parallel with Promise.all instead of
sequential awaits. This prevents each query from triggering a rebuild
that causes another hydration cycle, which was creating infinite loops.

The issue was that awaiting each query sequentially would:
1. Fetch articles for author A
2. Call onProgress, rebuild bookmarks
3. Trigger new hydration because coordinates changed
4. Repeat indefinitely

Now all queries start at once and stream results as they arrive,
matching the original loader behavior.
This commit is contained in:
Gigi
2025-10-22 11:27:12 +02:00
parent e40f820fdc
commit 3ab0610e1e

View File

@@ -194,7 +194,9 @@ class BookmarkController {
byPubkey.get(coord.pubkey)!.push(coord.identifier || '')
}
// Fetch each group
// Kick off all queries in parallel (fire-and-forget)
const promises: Promise<void>[] = []
for (const [kind, byPubkey] of filtersByKind) {
for (const [pubkey, identifiers] of byPubkey) {
// Separate empty and non-empty identifiers
@@ -205,63 +207,74 @@ class BookmarkController {
// Fetch events with non-empty d-tags
if (nonEmptyIdentifiers.length > 0) {
await queryEvents(
this.relayPool,
{ kinds: [kind], authors: [pubkey], '#d': nonEmptyIdentifiers },
{
onEvent: (event) => {
// Check if hydration was cancelled
if (this.hydrationGeneration !== generation) return
promises.push(
queryEvents(
this.relayPool,
{ kinds: [kind], authors: [pubkey], '#d': nonEmptyIdentifiers },
{
onEvent: (event) => {
// Check if hydration was cancelled
if (this.hydrationGeneration !== generation) return
const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || ''
const coordinate = `${event.kind}:${event.pubkey}:${dTag}`
console.log('[BookmarkController] Hydrated article (non-empty d):', coordinate, getArticleTitle(event) || 'No title')
idToEvent.set(coordinate, event)
idToEvent.set(event.id, event)
// Add to external event store if available
if (this.externalEventStore) {
this.externalEventStore.add(event)
const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || ''
const coordinate = `${event.kind}:${event.pubkey}:${dTag}`
console.log('[BookmarkController] Hydrated article (non-empty d):', coordinate, getArticleTitle(event) || 'No title')
idToEvent.set(coordinate, event)
idToEvent.set(event.id, event)
// Add to external event store if available
if (this.externalEventStore) {
this.externalEventStore.add(event)
}
onProgress()
}
onProgress()
}
}
).catch(() => {
// Silent error - individual query failed
})
)
}
// Fetch events with empty d-tag separately (without '#d' filter)
if (hasEmptyIdentifier) {
console.log('[BookmarkController] Fetching events with empty d-tag for kind', kind, 'pubkey', pubkey.slice(0, 8))
await queryEvents(
this.relayPool,
{ kinds: [kind], authors: [pubkey] },
{
onEvent: (event) => {
// Check if hydration was cancelled
if (this.hydrationGeneration !== generation) return
// Only process events with empty d-tag
const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || ''
if (dTag !== '') return
promises.push(
queryEvents(
this.relayPool,
{ kinds: [kind], authors: [pubkey] },
{
onEvent: (event) => {
// Check if hydration was cancelled
if (this.hydrationGeneration !== generation) return
// Only process events with empty d-tag
const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || ''
if (dTag !== '') return
const coordinate = `${event.kind}:${event.pubkey}:`
console.log('[BookmarkController] Hydrated article (empty d):', coordinate, getArticleTitle(event) || 'No title')
idToEvent.set(coordinate, event)
idToEvent.set(event.id, event)
// Add to external event store if available
if (this.externalEventStore) {
this.externalEventStore.add(event)
const coordinate = `${event.kind}:${event.pubkey}:`
console.log('[BookmarkController] Hydrated article (empty d):', coordinate, getArticleTitle(event) || 'No title')
idToEvent.set(coordinate, event)
idToEvent.set(event.id, event)
// Add to external event store if available
if (this.externalEventStore) {
this.externalEventStore.add(event)
}
onProgress()
}
onProgress()
}
}
).catch(() => {
// Silent error - individual query failed
})
)
}
}
}
// Wait for all queries to complete
await Promise.all(promises)
console.log('[BookmarkController] Coordinate hydration complete')
}