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)
+ })
}
}