mirror of
https://github.com/dergigi/boris.git
synced 2026-01-30 20:24:27 +01:00
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:
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user