fix: properly implement eventManager with promise-based API

- Fix eventManager to handle async fetching with proper promise resolution
- Track pending requests and deduplicate concurrent requests for same event
- Auto-retry when relay pool becomes available
- Resolve all pending callbacks when event arrives
- Update useEventLoader to use eventManager.fetchEvent
- Simplify useEventLoader with just one effect for fetching
- Handles both instant cache hits and deferred relay fetching
This commit is contained in:
Gigi
2025-10-22 00:55:20 +02:00
parent 160dca628d
commit 1929b50892
2 changed files with 103 additions and 66 deletions

View File

@@ -55,50 +55,36 @@ export function useEventLoader({
useEffect(() => {
if (!eventId) return
// Try to get from event store first (check cache synchronously)
const cachedEvent = eventManager.getCachedEvent(eventId)
if (cachedEvent) {
displayEvent(cachedEvent)
setReaderLoading(false)
setIsCollapsed(false)
setSelectedUrl('')
return
}
// Event not in cache, set loading state and fetch from relays
setReaderLoading(true)
setReaderContent(undefined)
setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights
setIsCollapsed(false)
// If no relay pool yet, wait for it (will re-run when relayPool changes)
if (!relayPool) {
return
}
// Fetch using event manager (handles cache, deduplication, and retry)
let cancelled = false
// Fetch from relays using event manager's loader
const eventLoader = eventManager.getEventLoader()
if (!eventLoader) {
setReaderLoading(false)
return
}
const subscription = eventLoader({ id: eventId }).subscribe({
next: (event) => {
displayEvent(event)
setReaderLoading(false)
},
error: (err) => {
const errorContent: ReadableContent = {
url: '',
html: `<div style="padding: 1rem; color: var(--color-error, red);">Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}</div>`,
title: 'Error'
eventManager.fetchEvent(eventId).then(
(event) => {
if (!cancelled) {
displayEvent(event)
setReaderLoading(false)
}
},
(err) => {
if (!cancelled) {
const errorContent: ReadableContent = {
url: '',
html: `<div style="padding: 1rem; color: var(--color-error, red);">Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}</div>`,
title: 'Error'
}
setReaderContent(errorContent)
setReaderLoading(false)
}
setReaderContent(errorContent)
setReaderLoading(false)
}
})
)
return () => subscription.unsubscribe()
}, [eventId, relayPool, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent])
return () => {
cancelled = true
}
}, [eventId, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent])
}

View File

@@ -2,17 +2,24 @@ import { RelayPool } from 'applesauce-relay'
import { IEventStore } from 'applesauce-core'
import { createEventLoader } from 'applesauce-loaders/loaders'
import { NostrEvent } from 'nostr-tools'
import { Observable } from 'rxjs'
type PendingRequest = {
resolve: (event: NostrEvent) => void
reject: (error: Error) => void
}
/**
* Centralized event manager for event fetching coordination
* Manages initialization and provides utilities for event loading
* Centralized event manager for event fetching and caching
* Handles deduplication of concurrent requests and coordinate with relay pool
*/
class EventManager {
private eventStore: IEventStore | null = null
private relayPool: RelayPool | null = null
private eventLoader: ReturnType<typeof createEventLoader> | null = null
// Track pending requests to deduplicate and resolve all at once
private pendingRequests = new Map<string, PendingRequest[]>()
/**
* Initialize the event manager with event store and relay pool
*/
@@ -25,32 +32,14 @@ class EventManager {
this.eventLoader = createEventLoader(relayPool, {
eventStore: eventStore || undefined
})
// Retry any pending requests now that we have a loader
this.retryAllPending()
}
}
/**
* Get the event loader for fetching events
*/
getEventLoader(): ReturnType<typeof createEventLoader> | null {
return this.eventLoader
}
/**
* Get the event store
*/
getEventStore(): IEventStore | null {
return this.eventStore
}
/**
* Get the relay pool
*/
getRelayPool(): RelayPool | null {
return this.relayPool
}
/**
* Check if event exists in store and return it if available
* Get cached event from event store
*/
getCachedEvent(eventId: string): NostrEvent | null {
if (!this.eventStore) return null
@@ -58,11 +47,73 @@ class EventManager {
}
/**
* Fetch event by ID, returning an observable
* Fetch an event by ID, returning a promise
* Automatically deduplicates concurrent requests for the same event
*/
fetchEvent(eventId: string): Observable<NostrEvent> | null {
if (!this.eventLoader) return null
return this.eventLoader({ id: eventId })
fetchEvent(eventId: string): Promise<NostrEvent> {
// Check cache first
const cached = this.getCachedEvent(eventId)
if (cached) {
return Promise.resolve(cached)
}
return new Promise<NostrEvent>((resolve, reject) => {
// Check if we're already fetching this event
if (this.pendingRequests.has(eventId)) {
// Add to existing request queue
this.pendingRequests.get(eventId)!.push({ resolve, reject })
return
}
// Start a new fetch request
this.pendingRequests.set(eventId, [{ resolve, reject }])
this.fetchFromRelay(eventId)
})
}
/**
* Actually fetch the event from relay
*/
private fetchFromRelay(eventId: string): void {
// If no loader yet, schedule retry
if (!this.relayPool || !this.eventLoader) {
setTimeout(() => {
if (this.eventLoader && this.pendingRequests.has(eventId)) {
this.fetchFromRelay(eventId)
}
}, 500)
return
}
const subscription = this.eventLoader({ id: eventId }).subscribe({
next: (event: NostrEvent) => {
// Resolve all pending requests
const requests = this.pendingRequests.get(eventId) || []
this.pendingRequests.delete(eventId)
requests.forEach(req => req.resolve(event))
subscription.unsubscribe()
},
error: (err: unknown) => {
// Reject all pending requests
const requests = this.pendingRequests.get(eventId) || []
this.pendingRequests.delete(eventId)
const error = err instanceof Error ? err : new Error(String(err))
requests.forEach(req => req.reject(error))
subscription.unsubscribe()
}
})
}
/**
* Retry all pending requests after relay pool becomes available
*/
private retryAllPending(): void {
const pendingIds = Array.from(this.pendingRequests.keys())
pendingIds.forEach(eventId => {
this.fetchFromRelay(eventId)
})
}
}