diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 63823743..946ab813 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -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: `
Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}
`, - title: 'Error' + eventManager.fetchEvent(eventId).then( + (event) => { + if (!cancelled) { + displayEvent(event) + setReaderLoading(false) + } + }, + (err) => { + if (!cancelled) { + const errorContent: ReadableContent = { + url: '', + html: `
Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}
`, + 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]) } diff --git a/src/services/eventManager.ts b/src/services/eventManager.ts index a15993ca..a3a3a9b8 100644 --- a/src/services/eventManager.ts +++ b/src/services/eventManager.ts @@ -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 | null = null + // Track pending requests to deduplicate and resolve all at once + private pendingRequests = new Map() + /** * 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 | 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 | null { - if (!this.eventLoader) return null - return this.eventLoader({ id: eventId }) + fetchEvent(eventId: string): Promise { + // Check cache first + const cached = this.getCachedEvent(eventId) + if (cached) { + return Promise.resolve(cached) + } + + return new Promise((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) + }) } }