diff --git a/src/services/highlights/cache.ts b/src/services/highlights/cache.ts new file mode 100644 index 00000000..8ca18af1 --- /dev/null +++ b/src/services/highlights/cache.ts @@ -0,0 +1,96 @@ +import { Highlight } from '../../types/highlights' + +interface CacheEntry { + highlights: Highlight[] + timestamp: number +} + +/** + * Simple in-memory session cache for highlight queries with TTL + */ +class HighlightCache { + private cache = new Map() + private ttlMs = 60000 // 60 seconds + + /** + * Generate cache key for article coordinate + */ + articleKey(coordinate: string): string { + return `article:${coordinate}` + } + + /** + * Generate cache key for URL + */ + urlKey(url: string): string { + // Normalize URL for consistent caching + try { + const normalized = new URL(url) + normalized.hash = '' // Remove hash + return `url:${normalized.toString()}` + } catch { + return `url:${url}` + } + } + + /** + * Generate cache key for author pubkey + */ + authorKey(pubkey: string): string { + return `author:${pubkey}` + } + + /** + * Get cached highlights if not expired + */ + get(key: string): Highlight[] | null { + const entry = this.cache.get(key) + if (!entry) return null + + const now = Date.now() + if (now - entry.timestamp > this.ttlMs) { + this.cache.delete(key) + return null + } + + return entry.highlights + } + + /** + * Store highlights in cache + */ + set(key: string, highlights: Highlight[]): void { + this.cache.set(key, { + highlights, + timestamp: Date.now() + }) + } + + /** + * Clear specific cache entry + */ + clear(key: string): void { + this.cache.delete(key) + } + + /** + * Clear all cache entries + */ + clearAll(): void { + this.cache.clear() + } + + /** + * Get cache stats + */ + stats(): { size: number; keys: string[] } { + return { + size: this.cache.size, + keys: Array.from(this.cache.keys()) + } + } +} + +// Singleton instance +export const highlightCache = new HighlightCache() + diff --git a/src/services/highlights/fetchByAuthor.ts b/src/services/highlights/fetchByAuthor.ts index 2f047fcc..ae18eccd 100644 --- a/src/services/highlights/fetchByAuthor.ts +++ b/src/services/highlights/fetchByAuthor.ts @@ -6,13 +6,28 @@ import { UserSettings } from '../settingsService' import { rebroadcastEvents } from '../rebroadcastService' import { KINDS } from '../../config/kinds' import { queryEvents } from '../dataFetch' +import { highlightCache } from './cache' export const fetchHighlights = async ( relayPool: RelayPool, pubkey: string, onHighlight?: (highlight: Highlight) => void, - settings?: UserSettings + settings?: UserSettings, + force = false ): Promise => { + // Check cache first unless force refresh + if (!force) { + const cacheKey = highlightCache.authorKey(pubkey) + const cached = highlightCache.get(cacheKey) + if (cached) { + console.log(`📌 Using cached highlights for author (${cached.length} items)`) + // Stream cached highlights if callback provided + if (onHighlight) { + cached.forEach(h => onHighlight(h)) + } + return cached + } + } try { const seenIds = new Set() const rawEvents: NostrEvent[] = await queryEvents( @@ -37,7 +52,13 @@ export const fetchHighlights = async ( const uniqueEvents = dedupeHighlights(rawEvents) const highlights = uniqueEvents.map(eventToHighlight) - return sortHighlights(highlights) + const sorted = sortHighlights(highlights) + + // Cache the results + const cacheKey = highlightCache.authorKey(pubkey) + highlightCache.set(cacheKey, sorted) + + return sorted } catch { return [] } diff --git a/src/services/highlights/fetchForArticle.ts b/src/services/highlights/fetchForArticle.ts index 04fb5441..90c14c65 100644 --- a/src/services/highlights/fetchForArticle.ts +++ b/src/services/highlights/fetchForArticle.ts @@ -6,14 +6,29 @@ import { eventToHighlight, dedupeHighlights, sortHighlights } from '../highlight import { UserSettings } from '../settingsService' import { rebroadcastEvents } from '../rebroadcastService' import { queryEvents } from '../dataFetch' +import { highlightCache } from './cache' export const fetchHighlightsForArticle = async ( relayPool: RelayPool, articleCoordinate: string, eventId?: string, onHighlight?: (highlight: Highlight) => void, - settings?: UserSettings + settings?: UserSettings, + force = false ): Promise => { + // Check cache first unless force refresh + if (!force) { + const cacheKey = highlightCache.articleKey(articleCoordinate) + const cached = highlightCache.get(cacheKey) + if (cached) { + console.log(`📌 Using cached highlights for article (${cached.length} items)`) + // Stream cached highlights if callback provided + if (onHighlight) { + cached.forEach(h => onHighlight(h)) + } + return cached + } + } try { const seenIds = new Set() const onEvent = (event: NostrEvent) => { @@ -41,7 +56,13 @@ export const fetchHighlightsForArticle = async ( const uniqueEvents = dedupeHighlights(rawEvents) const highlights: Highlight[] = uniqueEvents.map(eventToHighlight) - return sortHighlights(highlights) + const sorted = sortHighlights(highlights) + + // Cache the results + const cacheKey = highlightCache.articleKey(articleCoordinate) + highlightCache.set(cacheKey, sorted) + + return sorted } catch { return [] } diff --git a/src/services/highlights/fetchForUrl.ts b/src/services/highlights/fetchForUrl.ts index ba454d2e..c1e569f0 100644 --- a/src/services/highlights/fetchForUrl.ts +++ b/src/services/highlights/fetchForUrl.ts @@ -6,13 +6,28 @@ import { eventToHighlight, dedupeHighlights, sortHighlights } from '../highlight import { UserSettings } from '../settingsService' import { rebroadcastEvents } from '../rebroadcastService' import { queryEvents } from '../dataFetch' +import { highlightCache } from './cache' export const fetchHighlightsForUrl = async ( relayPool: RelayPool, url: string, onHighlight?: (highlight: Highlight) => void, - settings?: UserSettings + settings?: UserSettings, + force = false ): Promise => { + // Check cache first unless force refresh + if (!force) { + const cacheKey = highlightCache.urlKey(url) + const cached = highlightCache.get(cacheKey) + if (cached) { + console.log(`📌 Using cached highlights for URL (${cached.length} items)`) + // Stream cached highlights if callback provided + if (onHighlight) { + cached.forEach(h => onHighlight(h)) + } + return cached + } + } try { const seenIds = new Set() const rawEvents: NostrEvent[] = await queryEvents( @@ -38,7 +53,13 @@ export const fetchHighlightsForUrl = async ( const uniqueEvents = dedupeHighlights(rawEvents) const highlights: Highlight[] = uniqueEvents.map(eventToHighlight) - return sortHighlights(highlights) + const sorted = sortHighlights(highlights) + + // Cache the results + const cacheKey = highlightCache.urlKey(url) + highlightCache.set(cacheKey, sorted) + + return sorted } catch (err) { console.error('Error fetching highlights for URL:', err) return []