import { RelayPool } from 'applesauce-relay' import { IEventStore } from 'applesauce-core' import { Highlight } from '../types/highlights' import { queryEvents } from './dataFetch' import { KINDS } from '../config/kinds' import { eventToHighlight, sortHighlights } from './highlightEventProcessor' type HighlightsCallback = (highlights: Highlight[]) => void type LoadingCallback = (loading: boolean) => void const LAST_SYNCED_KEY = 'nostrverse_highlights_last_synced' class NostrverseHighlightsController { private highlightsListeners: HighlightsCallback[] = [] private loadingListeners: LoadingCallback[] = [] private currentHighlights: Highlight[] = [] private loaded = false private generation = 0 onHighlights(cb: HighlightsCallback): () => void { this.highlightsListeners.push(cb) return () => { this.highlightsListeners = this.highlightsListeners.filter(l => l !== cb) } } onLoading(cb: LoadingCallback): () => void { this.loadingListeners.push(cb) return () => { this.loadingListeners = this.loadingListeners.filter(l => l !== cb) } } private setLoading(loading: boolean): void { this.loadingListeners.forEach(cb => cb(loading)) } private emitHighlights(highlights: Highlight[]): void { this.highlightsListeners.forEach(cb => cb(highlights)) } getHighlights(): Highlight[] { return [...this.currentHighlights] } isLoaded(): boolean { return this.loaded } private getLastSyncedAt(): number | null { try { const raw = localStorage.getItem(LAST_SYNCED_KEY) if (!raw) return null const parsed = JSON.parse(raw) return typeof parsed?.ts === 'number' ? parsed.ts : null } catch { return null } } private setLastSyncedAt(timestamp: number): void { try { localStorage.setItem(LAST_SYNCED_KEY, JSON.stringify({ ts: timestamp })) } catch { /* ignore */ } } async start(options: { relayPool: RelayPool eventStore: IEventStore force?: boolean }): Promise { const { relayPool, eventStore, force = false } = options if (!force && this.loaded) { this.emitHighlights(this.currentHighlights) return } this.generation++ const currentGeneration = this.generation this.setLoading(true) try { const seenIds = new Set() const highlightsMap = new Map() const lastSyncedAt = force ? null : this.getLastSyncedAt() const filter: { kinds: number[]; since?: number } = { kinds: [KINDS.Highlights] } if (lastSyncedAt) filter.since = lastSyncedAt const events = await queryEvents( relayPool, filter, { onEvent: (evt) => { if (currentGeneration !== this.generation) return if (seenIds.has(evt.id)) return seenIds.add(evt.id) eventStore.add(evt) const highlight = eventToHighlight(evt) highlightsMap.set(highlight.id, highlight) const sorted = sortHighlights(Array.from(highlightsMap.values())) this.currentHighlights = sorted this.emitHighlights(sorted) } } ) if (currentGeneration !== this.generation) return events.forEach(evt => eventStore.add(evt)) const highlights = events.map(eventToHighlight) const unique = Array.from(new Map(highlights.map(h => [h.id, h])).values()) const sorted = sortHighlights(unique) this.currentHighlights = sorted this.loaded = true this.emitHighlights(sorted) if (sorted.length > 0) { const newest = Math.max(...sorted.map(h => h.created_at)) this.setLastSyncedAt(newest) } } catch (err) { this.currentHighlights = [] this.emitHighlights(this.currentHighlights) } finally { if (currentGeneration === this.generation) this.setLoading(false) } } } export const nostrverseHighlightsController = new NostrverseHighlightsController()