mirror of
https://github.com/dergigi/boris.git
synced 2026-01-02 14:34:25 +01:00
feat(highlights): add optional session cache with TTL
- Add in-memory cache with 60s TTL for article/url/author queries - Check cache before network fetch to reduce redundant queries - Support force flag to bypass cache when needed - Stream cached results through onHighlight callback for consistency
This commit is contained in:
96
src/services/highlights/cache.ts
Normal file
96
src/services/highlights/cache.ts
Normal file
@@ -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<string, CacheEntry>()
|
||||
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()
|
||||
|
||||
@@ -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<Highlight[]> => {
|
||||
// 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<string>()
|
||||
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 []
|
||||
}
|
||||
|
||||
@@ -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<Highlight[]> => {
|
||||
// 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<string>()
|
||||
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 []
|
||||
}
|
||||
|
||||
@@ -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<Highlight[]> => {
|
||||
// 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<string>()
|
||||
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 []
|
||||
|
||||
Reference in New Issue
Block a user