fix: simplify eventManager and restore working event fetching

- Revert eventManager to simpler role: initialization and service coordination
- Restore original working fetching logic in useEventLoader
- eventManager now provides: getCachedEvent, getEventLoader, setServices
- Fixes broken bookmark hydration and direct event loading
- Uses eventManager for cache checking but direct subscription for fetching
This commit is contained in:
Gigi
2025-10-22 00:54:33 +02:00
parent c04ba0c787
commit 160dca628d
2 changed files with 56 additions and 98 deletions

View File

@@ -55,18 +55,40 @@ 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)
// Fetch event using the event manager // If no relay pool yet, wait for it (will re-run when relayPool changes)
eventManager.fetchEvent(eventId).then( if (!relayPool) {
(event) => { return
}
// 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) displayEvent(event)
setReaderLoading(false) setReaderLoading(false)
}, },
(err) => { error: (err) => {
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>`,
@@ -75,6 +97,8 @@ export function useEventLoader({
setReaderContent(errorContent) setReaderContent(errorContent)
setReaderLoading(false) setReaderLoading(false)
} }
) })
}, [eventId, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent])
return () => subscription.unsubscribe()
}, [eventId, relayPool, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent])
} }

View File

@@ -2,26 +2,17 @@ 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 { BehaviorSubject, Observable } from 'rxjs' import { Observable } from 'rxjs'
type EventCallback = (event: NostrEvent) => void
type ErrorCallback = (error: Error) => void
/** /**
* Centralized event manager for fetching and caching events * Centralized event manager for event fetching coordination
* Handles deduplication of requests and provides a single source of truth * Manages initialization and provides utilities for event loading
*/ */
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 avoid duplicates
private pendingRequests = new Map<string, Array<{ onSuccess: EventCallback; onError: ErrorCallback }>>()
// Event stream for real-time updates
private eventSubject = new BehaviorSubject<NostrEvent | null>(null)
/** /**
* Initialize the event manager with event store and relay pool * Initialize the event manager with event store and relay pool
*/ */
@@ -29,7 +20,8 @@ class EventManager {
this.eventStore = eventStore this.eventStore = eventStore
this.relayPool = relayPool this.relayPool = relayPool
if (relayPool && this.eventLoader === null) { // Recreate loader when services change
if (relayPool) {
this.eventLoader = createEventLoader(relayPool, { this.eventLoader = createEventLoader(relayPool, {
eventStore: eventStore || undefined eventStore: eventStore || undefined
}) })
@@ -37,98 +29,40 @@ class EventManager {
} }
/** /**
* Fetch an event by ID, with automatic deduplication and caching * Get the event loader for fetching events
*/ */
async fetchEvent(eventId: string): Promise<NostrEvent> { getEventLoader(): ReturnType<typeof createEventLoader> | null {
// Check cache first return this.eventLoader
if (this.eventStore) {
const cached = this.eventStore.getEvent(eventId)
if (cached) {
return cached
}
}
// Return a promise that will be resolved when the event is fetched
return new Promise((resolve, reject) => {
this.fetchEventAsync(eventId, resolve, reject)
})
} }
/** /**
* Subscribe to event fetching with callbacks * Get the event store
*/ */
private fetchEventAsync( getEventStore(): IEventStore | null {
eventId: string, return this.eventStore
onSuccess: EventCallback,
onError: ErrorCallback
): void {
// Check if we're already fetching this event
if (this.pendingRequests.has(eventId)) {
// Add to existing request queue
this.pendingRequests.get(eventId)!.push({ onSuccess, onError })
return
}
// Start a new fetch request
this.pendingRequests.set(eventId, [{ onSuccess, onError }])
// If no relay pool yet, wait for it
if (!this.relayPool || !this.eventLoader) {
// Will retry when services are set
setTimeout(() => {
// Retry if still no pool
if (!this.relayPool) {
this.retryPendingRequest(eventId)
}
}, 1000)
return
}
const subscription = this.eventLoader({ id: eventId }).subscribe({
next: (event: NostrEvent) => {
// Call all pending callbacks
const callbacks = this.pendingRequests.get(eventId) || []
this.pendingRequests.delete(eventId)
callbacks.forEach(cb => cb.onSuccess(event))
// Emit to stream
this.eventSubject.next(event)
subscription.unsubscribe()
},
error: (err: unknown) => {
// Call all pending callbacks with error
const callbacks = this.pendingRequests.get(eventId) || []
this.pendingRequests.delete(eventId)
const error = err instanceof Error ? err : new Error(String(err))
callbacks.forEach(cb => cb.onError(error))
subscription.unsubscribe()
}
})
} }
/** /**
* Retry pending requests after delay (useful when relay pool becomes available) * Get the relay pool
*/ */
private retryPendingRequest(eventId: string): void { getRelayPool(): RelayPool | null {
const callbacks = this.pendingRequests.get(eventId) return this.relayPool
if (!callbacks) return
// Re-trigger the fetch
this.pendingRequests.delete(eventId)
if (callbacks.length > 0) {
this.fetchEventAsync(eventId, callbacks[0].onSuccess, callbacks[0].onError)
}
} }
/** /**
* Get the event stream for reactive updates * Check if event exists in store and return it if available
*/ */
getEventStream(): Observable<NostrEvent | null> { getCachedEvent(eventId: string): NostrEvent | null {
return this.eventSubject.asObservable() if (!this.eventStore) return null
return this.eventStore.getEvent(eventId) || null
}
/**
* Fetch event by ID, returning an observable
*/
fetchEvent(eventId: string): Observable<NostrEvent> | null {
if (!this.eventLoader) return null
return this.eventLoader({ id: eventId })
} }
} }