mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 14:44:26 +01:00
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:
@@ -55,40 +55,23 @@ export function useEventLoader({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!eventId) return
|
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)
|
setReaderLoading(true)
|
||||||
setReaderContent(undefined)
|
setReaderContent(undefined)
|
||||||
setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights
|
setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights
|
||||||
setIsCollapsed(false)
|
setIsCollapsed(false)
|
||||||
|
|
||||||
// If no relay pool yet, wait for it (will re-run when relayPool changes)
|
// Fetch using event manager (handles cache, deduplication, and retry)
|
||||||
if (!relayPool) {
|
let cancelled = false
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch from relays using event manager's loader
|
eventManager.fetchEvent(eventId).then(
|
||||||
const eventLoader = eventManager.getEventLoader()
|
(event) => {
|
||||||
if (!eventLoader) {
|
if (!cancelled) {
|
||||||
setReaderLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscription = eventLoader({ id: eventId }).subscribe({
|
|
||||||
next: (event) => {
|
|
||||||
displayEvent(event)
|
displayEvent(event)
|
||||||
setReaderLoading(false)
|
setReaderLoading(false)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
|
if (!cancelled) {
|
||||||
const errorContent: ReadableContent = {
|
const errorContent: ReadableContent = {
|
||||||
url: '',
|
url: '',
|
||||||
html: `<div style="padding: 1rem; color: var(--color-error, red);">Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}</div>`,
|
html: `<div style="padding: 1rem; color: var(--color-error, red);">Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}</div>`,
|
||||||
@@ -97,8 +80,11 @@ export function useEventLoader({
|
|||||||
setReaderContent(errorContent)
|
setReaderContent(errorContent)
|
||||||
setReaderLoading(false)
|
setReaderLoading(false)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return () => subscription.unsubscribe()
|
return () => {
|
||||||
}, [eventId, relayPool, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent])
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [eventId, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,24 @@ import { RelayPool } from 'applesauce-relay'
|
|||||||
import { IEventStore } from 'applesauce-core'
|
import { IEventStore } from 'applesauce-core'
|
||||||
import { createEventLoader } from 'applesauce-loaders/loaders'
|
import { createEventLoader } from 'applesauce-loaders/loaders'
|
||||||
import { NostrEvent } from 'nostr-tools'
|
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
|
* Centralized event manager for event fetching and caching
|
||||||
* Manages initialization and provides utilities for event loading
|
* Handles deduplication of concurrent requests and coordinate with relay pool
|
||||||
*/
|
*/
|
||||||
class EventManager {
|
class EventManager {
|
||||||
private eventStore: IEventStore | null = null
|
private eventStore: IEventStore | null = null
|
||||||
private relayPool: RelayPool | null = null
|
private relayPool: RelayPool | null = null
|
||||||
private eventLoader: ReturnType<typeof createEventLoader> | 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
|
* Initialize the event manager with event store and relay pool
|
||||||
*/
|
*/
|
||||||
@@ -25,32 +32,14 @@ class EventManager {
|
|||||||
this.eventLoader = createEventLoader(relayPool, {
|
this.eventLoader = createEventLoader(relayPool, {
|
||||||
eventStore: eventStore || undefined
|
eventStore: eventStore || undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Retry any pending requests now that we have a loader
|
||||||
|
this.retryAllPending()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the event loader for fetching events
|
* Get cached event from event store
|
||||||
*/
|
|
||||||
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
|
|
||||||
*/
|
*/
|
||||||
getCachedEvent(eventId: string): NostrEvent | null {
|
getCachedEvent(eventId: string): NostrEvent | null {
|
||||||
if (!this.eventStore) return 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 {
|
fetchEvent(eventId: string): Promise<NostrEvent> {
|
||||||
if (!this.eventLoader) return null
|
// Check cache first
|
||||||
return this.eventLoader({ id: eventId })
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user