mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 14:44:26 +01:00
feat: add centralized eventManager for event fetching
- Create eventManager singleton for fetching and caching events - Handles deduplication of concurrent requests for same event - Waits for relay pool to become available before fetching - Provides both async/await and callback-based APIs - Update useEventLoader to use eventManager instead of direct loader - Simplifies event fetching logic and enables better reuse across app
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useCallback } from 'react'
|
import { useEffect, useCallback } from 'react'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { IEventStore } from 'applesauce-core'
|
import { IEventStore } from 'applesauce-core'
|
||||||
import { createEventLoader } from 'applesauce-loaders/loaders'
|
|
||||||
import { NostrEvent } from 'nostr-tools'
|
import { NostrEvent } from 'nostr-tools'
|
||||||
import { ReadableContent } from '../services/readerService'
|
import { ReadableContent } from '../services/readerService'
|
||||||
|
import { eventManager } from '../services/eventManager'
|
||||||
|
|
||||||
interface UseEventLoaderProps {
|
interface UseEventLoaderProps {
|
||||||
eventId?: string
|
eventId?: string
|
||||||
@@ -47,44 +47,26 @@ export function useEventLoader({
|
|||||||
setReaderContent(content)
|
setReaderContent(content)
|
||||||
}, [setReaderContent])
|
}, [setReaderContent])
|
||||||
|
|
||||||
|
// Initialize event manager with services
|
||||||
|
useEffect(() => {
|
||||||
|
eventManager.setServices(eventStore || null, relayPool || null)
|
||||||
|
}, [eventStore, relayPool])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!eventId) return
|
if (!eventId) return
|
||||||
|
|
||||||
// Try to get from event store first - do this synchronously before setting loading state
|
|
||||||
if (eventStore) {
|
|
||||||
const cachedEvent = eventStore.getEvent(eventId)
|
|
||||||
if (cachedEvent) {
|
|
||||||
displayEvent(cachedEvent)
|
|
||||||
setReaderLoading(false)
|
|
||||||
setIsCollapsed(false)
|
|
||||||
setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event not in cache, now 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 or show a placeholder
|
// Fetch event using the event manager
|
||||||
if (!relayPool) {
|
eventManager.fetchEvent(eventId).then(
|
||||||
// Show loading state until relayPool becomes available
|
(event) => {
|
||||||
// The effect will re-run once relayPool is set
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventLoader = createEventLoader(relayPool, {
|
|
||||||
eventStore: eventStore ?? undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const subscription = eventLoader({ id: eventId }).subscribe({
|
|
||||||
next: (event) => {
|
|
||||||
displayEvent(event)
|
displayEvent(event)
|
||||||
setReaderLoading(false)
|
setReaderLoading(false)
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(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>`,
|
||||||
@@ -93,8 +75,6 @@ export function useEventLoader({
|
|||||||
setReaderContent(errorContent)
|
setReaderContent(errorContent)
|
||||||
setReaderLoading(false)
|
setReaderLoading(false)
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
}, [eventId, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent])
|
||||||
return () => subscription.unsubscribe()
|
|
||||||
}, [eventId, relayPool, eventStore, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent])
|
|
||||||
}
|
}
|
||||||
|
|||||||
136
src/services/eventManager.ts
Normal file
136
src/services/eventManager.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { RelayPool } from 'applesauce-relay'
|
||||||
|
import { IEventStore } from 'applesauce-core'
|
||||||
|
import { createEventLoader } from 'applesauce-loaders/loaders'
|
||||||
|
import { NostrEvent } from 'nostr-tools'
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs'
|
||||||
|
|
||||||
|
type EventCallback = (event: NostrEvent) => void
|
||||||
|
type ErrorCallback = (error: Error) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized event manager for fetching and caching events
|
||||||
|
* Handles deduplication of requests and provides a single source of truth
|
||||||
|
*/
|
||||||
|
class EventManager {
|
||||||
|
private eventStore: IEventStore | null = null
|
||||||
|
private relayPool: RelayPool | 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
|
||||||
|
*/
|
||||||
|
setServices(eventStore: IEventStore | null, relayPool: RelayPool | null): void {
|
||||||
|
this.eventStore = eventStore
|
||||||
|
this.relayPool = relayPool
|
||||||
|
|
||||||
|
if (relayPool && this.eventLoader === null) {
|
||||||
|
this.eventLoader = createEventLoader(relayPool, {
|
||||||
|
eventStore: eventStore || undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch an event by ID, with automatic deduplication and caching
|
||||||
|
*/
|
||||||
|
async fetchEvent(eventId: string): Promise<NostrEvent> {
|
||||||
|
// Check cache first
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private fetchEventAsync(
|
||||||
|
eventId: string,
|
||||||
|
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)
|
||||||
|
*/
|
||||||
|
private retryPendingRequest(eventId: string): void {
|
||||||
|
const callbacks = this.pendingRequests.get(eventId)
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
getEventStream(): Observable<NostrEvent | null> {
|
||||||
|
return this.eventSubject.asObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const eventManager = new EventManager()
|
||||||
Reference in New Issue
Block a user