mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 06:34:24 +01:00
fix: implement LRU cache eviction to handle QuotaExceededError
- Add LRU eviction strategy: limit to 1000 cached profiles, evict oldest when full - Track lastAccessed timestamp for each cached profile - Automatically evict old profiles when quota is exceeded - Reduce error logging spam: only log quota error once per session - Silently handle cache errors to match articleService pattern - Proactively evict before caching when approaching limit This prevents localStorage quota exceeded errors and ensures the most recently accessed profiles remain cached.
This commit is contained in:
@@ -9,10 +9,13 @@ import { UserSettings } from './settingsService'
|
|||||||
interface CachedProfile {
|
interface CachedProfile {
|
||||||
event: NostrEvent
|
event: NostrEvent
|
||||||
timestamp: number
|
timestamp: number
|
||||||
|
lastAccessed: number // For LRU eviction
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROFILE_CACHE_TTL = 30 * 24 * 60 * 60 * 1000 // 30 days in milliseconds (profiles change less frequently than articles)
|
const PROFILE_CACHE_TTL = 30 * 24 * 60 * 60 * 1000 // 30 days in milliseconds (profiles change less frequently than articles)
|
||||||
const PROFILE_CACHE_PREFIX = 'profile_cache_'
|
const PROFILE_CACHE_PREFIX = 'profile_cache_'
|
||||||
|
const MAX_CACHED_PROFILES = 1000 // Limit number of cached profiles to prevent quota issues
|
||||||
|
let quotaExceededLogged = false // Only log quota error once per session
|
||||||
|
|
||||||
function getProfileCacheKey(pubkey: string): string {
|
function getProfileCacheKey(pubkey: string): string {
|
||||||
return `${PROFILE_CACHE_PREFIX}${pubkey}`
|
return `${PROFILE_CACHE_PREFIX}${pubkey}`
|
||||||
@@ -21,6 +24,7 @@ function getProfileCacheKey(pubkey: string): string {
|
|||||||
/**
|
/**
|
||||||
* Get a cached profile from localStorage
|
* Get a cached profile from localStorage
|
||||||
* Returns null if not found, expired, or on error
|
* Returns null if not found, expired, or on error
|
||||||
|
* Updates lastAccessed timestamp for LRU eviction
|
||||||
*/
|
*/
|
||||||
export function getCachedProfile(pubkey: string): NostrEvent | null {
|
export function getCachedProfile(pubkey: string): NostrEvent | null {
|
||||||
try {
|
try {
|
||||||
@@ -30,45 +34,133 @@ export function getCachedProfile(pubkey: string): NostrEvent | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { event, timestamp }: CachedProfile = JSON.parse(cached)
|
const data: CachedProfile = JSON.parse(cached)
|
||||||
const age = Date.now() - timestamp
|
const age = Date.now() - data.timestamp
|
||||||
|
|
||||||
if (age > PROFILE_CACHE_TTL) {
|
if (age > PROFILE_CACHE_TTL) {
|
||||||
localStorage.removeItem(cacheKey)
|
localStorage.removeItem(cacheKey)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
// Update lastAccessed for LRU eviction (but don't fail if update fails)
|
||||||
|
try {
|
||||||
|
data.lastAccessed = Date.now()
|
||||||
|
localStorage.setItem(cacheKey, JSON.stringify(data))
|
||||||
|
} catch {
|
||||||
|
// Ignore update errors, still return the profile
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.event
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Log cache read errors for debugging
|
// Silently handle cache read errors (quota, invalid data, etc.)
|
||||||
console.error(`[npub-cache] Error reading cached profile for ${pubkey.slice(0, 16)}...:`, err)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cached profile keys for eviction
|
||||||
|
*/
|
||||||
|
function getAllCachedProfileKeys(): Array<{ key: string; lastAccessed: number }> {
|
||||||
|
const keys: Array<{ key: string; lastAccessed: number }> = []
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i)
|
||||||
|
if (key && key.startsWith(PROFILE_CACHE_PREFIX)) {
|
||||||
|
try {
|
||||||
|
const cached = localStorage.getItem(key)
|
||||||
|
if (cached) {
|
||||||
|
const data: CachedProfile = JSON.parse(cached)
|
||||||
|
keys.push({
|
||||||
|
key,
|
||||||
|
lastAccessed: data.lastAccessed || data.timestamp || 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip invalid entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors during enumeration
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evict oldest profiles (LRU) to free up space
|
||||||
|
* Removes the oldest accessed profiles until we're under the limit
|
||||||
|
*/
|
||||||
|
function evictOldProfiles(targetCount: number): void {
|
||||||
|
try {
|
||||||
|
const keys = getAllCachedProfileKeys()
|
||||||
|
if (keys.length <= targetCount) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by lastAccessed (oldest first) and remove oldest
|
||||||
|
keys.sort((a, b) => a.lastAccessed - b.lastAccessed)
|
||||||
|
const toRemove = keys.slice(0, keys.length - targetCount)
|
||||||
|
|
||||||
|
for (const { key } of toRemove) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently fail eviction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache a profile to localStorage
|
* Cache a profile to localStorage
|
||||||
* Handles errors gracefully (quota exceeded, invalid data, etc.)
|
* Handles errors gracefully (quota exceeded, invalid data, etc.)
|
||||||
|
* Implements LRU eviction when cache is full
|
||||||
*/
|
*/
|
||||||
export function cacheProfile(profile: NostrEvent): void {
|
export function cacheProfile(profile: NostrEvent): void {
|
||||||
try {
|
try {
|
||||||
if (profile.kind !== 0) {
|
if (profile.kind !== 0) {
|
||||||
console.warn(`[npub-cache] Attempted to cache non-profile event (kind ${profile.kind})`)
|
|
||||||
return // Only cache kind:0 (profile) events
|
return // Only cache kind:0 (profile) events
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = getProfileCacheKey(profile.pubkey)
|
const cacheKey = getProfileCacheKey(profile.pubkey)
|
||||||
|
|
||||||
|
// Check if we need to evict before caching
|
||||||
|
const existingKeys = getAllCachedProfileKeys()
|
||||||
|
if (existingKeys.length >= MAX_CACHED_PROFILES) {
|
||||||
|
// Check if this profile is already cached
|
||||||
|
const alreadyCached = existingKeys.some(k => k.key === cacheKey)
|
||||||
|
if (!alreadyCached) {
|
||||||
|
// Evict oldest profiles to make room (keep 90% of max)
|
||||||
|
evictOldProfiles(Math.floor(MAX_CACHED_PROFILES * 0.9))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cached: CachedProfile = {
|
const cached: CachedProfile = {
|
||||||
event: profile,
|
event: profile,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
|
lastAccessed: Date.now()
|
||||||
}
|
}
|
||||||
localStorage.setItem(cacheKey, JSON.stringify(cached))
|
localStorage.setItem(cacheKey, JSON.stringify(cached))
|
||||||
console.log(`[npub-cache] Cached profile:`, profile.pubkey.slice(0, 16) + '...')
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Log caching errors for debugging
|
// Handle quota exceeded by evicting and retrying once
|
||||||
console.error(`[npub-cache] Failed to cache profile ${profile.pubkey.slice(0, 16)}...:`, err)
|
if (err instanceof DOMException && err.name === 'QuotaExceededError') {
|
||||||
// Don't block the UI if caching fails
|
if (!quotaExceededLogged) {
|
||||||
// Handles quota exceeded, invalid data, and other errors gracefully
|
console.warn(`[npub-cache] localStorage quota exceeded, evicting old profiles...`)
|
||||||
|
quotaExceededLogged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try evicting more aggressively and retry
|
||||||
|
try {
|
||||||
|
evictOldProfiles(Math.floor(MAX_CACHED_PROFILES * 0.5))
|
||||||
|
const cached: CachedProfile = {
|
||||||
|
event: profile,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
lastAccessed: Date.now()
|
||||||
|
}
|
||||||
|
localStorage.setItem(getProfileCacheKey(profile.pubkey), JSON.stringify(cached))
|
||||||
|
} catch {
|
||||||
|
// Silently fail if still can't cache - don't block the UI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Silently handle other caching errors (invalid data, etc.)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user