Browse Source

remove in-memory profile and replaceables cache

imwald
Silberengel 1 month ago
parent
commit
61bf136119
  1. 54
      nip66-cron/index.mjs
  2. 7
      src/components/Sidebar/AccountButton.tsx
  3. 87
      src/hooks/useFetchProfile.tsx
  4. 2
      src/providers/NostrProvider/index.tsx
  5. 200
      src/services/client-replaceable-events.service.ts
  6. 11
      src/services/indexed-db.service.ts

54
nip66-cron/index.mjs

@ -179,9 +179,30 @@ async function fetchRelayUrlsFromKind10002 (authorPubkey, queryRelayUrls) { @@ -179,9 +179,30 @@ async function fetchRelayUrlsFromKind10002 (authorPubkey, queryRelayUrls) {
try {
ws = new WebSocket(relayUrl, { handshakeTimeout: 12000 })
await new Promise((resolve, reject) => {
ws.on('open', resolve)
ws.on('error', reject)
setTimeout(() => reject(new Error('open timeout')), 15000)
let timeoutId
let resolved = false
const onOpen = () => {
if (resolved) return
resolved = true
clearTimeout(timeoutId)
resolve()
}
const onError = (err) => {
if (resolved) return
resolved = true
clearTimeout(timeoutId)
ws.removeListener('open', onOpen)
reject(err)
}
timeoutId = setTimeout(() => {
if (resolved) return
resolved = true
ws.removeListener('open', onOpen)
ws.removeListener('error', onError)
reject(new Error('open timeout'))
}, 15000)
ws.once('open', onOpen)
ws.on('error', onError)
})
ws.send(JSON.stringify(['REQ', subId, filter]))
const events = await new Promise((resolve) => {
@ -306,9 +327,30 @@ async function publishEvent (relayUrls, event) { @@ -306,9 +327,30 @@ async function publishEvent (relayUrls, event) {
try {
const ws = new WebSocket(url, { handshakeTimeout: 8000 })
await new Promise((resolve, reject) => {
ws.on('open', resolve)
ws.on('error', reject)
setTimeout(() => reject(new Error('open timeout')), 10000)
let timeoutId
let resolved = false
const onOpen = () => {
if (resolved) return
resolved = true
clearTimeout(timeoutId)
resolve()
}
const onError = (err) => {
if (resolved) return
resolved = true
clearTimeout(timeoutId)
ws.removeListener('open', onOpen)
reject(err)
}
timeoutId = setTimeout(() => {
if (resolved) return
resolved = true
ws.removeListener('open', onOpen)
ws.removeListener('error', onError)
reject(new Error('open timeout'))
}, 10000)
ws.once('open', onOpen)
ws.on('error', onError)
})
conns.push(ws)
ws.send(msg)

7
src/components/Sidebar/AccountButton.tsx

@ -8,7 +8,7 @@ import { @@ -8,7 +8,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { toWallet } from '@/lib/link'
import { formatPubkey, generateImageByPubkey } from '@/lib/pubkey'
import { formatPubkey, generateImageByPubkey, pubkeyToNpub, formatNpub } from '@/lib/pubkey'
import { usePrimaryPage, useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import { ArrowDownUp, LogIn, LogOut, UserRound, Wallet } from 'lucide-react'
@ -39,7 +39,10 @@ function ProfileButton() { @@ -39,7 +39,10 @@ function ProfileButton() {
if (!pubkey) return null
const defaultAvatar = generateImageByPubkey(pubkey)
const { username, avatar } = profile || { username: formatPubkey(pubkey), avatar: defaultAvatar }
// Fallback to formatted npub if no profile
const npub = pubkeyToNpub(pubkey)
const fallbackUsername = npub ? formatNpub(npub) : formatPubkey(pubkey)
const { username, avatar } = profile || { username: fallbackUsername, avatar: defaultAvatar }
return (
<DropdownMenu>

87
src/hooks/useFetchProfile.tsx

@ -2,9 +2,9 @@ import { getProfileFromEvent } from '@/lib/event-metadata' @@ -2,9 +2,9 @@ import { getProfileFromEvent } from '@/lib/event-metadata'
import { userIdToPubkey } from '@/lib/pubkey'
import { useNostr } from '@/providers/NostrProvider'
import { replaceableEventService } from '@/services/client.service'
import { kinds } from 'nostr-tools'
import { TProfile } from '@/types'
import { useEffect, useState, useRef } from 'react'
import { useEffect, useState, useRef, useCallback } from 'react'
import logger from '@/lib/logger'
export function useFetchProfile(id?: string, skipCache = false) {
const { profile: currentAccountProfile } = useNostr()
@ -15,36 +15,50 @@ export function useFetchProfile(id?: string, skipCache = false) { @@ -15,36 +15,50 @@ export function useFetchProfile(id?: string, skipCache = false) {
const checkIntervalRef = useRef<NodeJS.Timeout | null>(null)
// Function to check for profile updates
const checkProfile = async (pubkey: string, cancelled: { current: boolean }) => {
if (cancelled.current) return
// fetchProfileEvent already checks: 1) in-memory cache, 2) IndexedDB, 3) network (with author's relays)
// Memoize to prevent recreation on every render
const checkProfile = useCallback(async (pubkey: string, cancelled: { current: boolean }) => {
if (cancelled.current) return false
try {
// Re-check cache (might have been updated by background fetch)
const profileEvent = await replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Metadata)
// Use fetchProfileEvent which includes author's relay list for better profile discovery
// fetchProfileEvent handles all cache layers:
// 1. In-memory cache (instant return)
// 2. IndexedDB (fast async)
// 3. Network (with author's relay list for better discovery)
const profileEvent = await replaceableEventService.fetchProfileEvent(pubkey, skipCache)
if (cancelled.current) return
if (cancelled.current) return false
if (profileEvent) {
// getProfileFromEvent always returns a profile object (with fallback username)
const newProfile = getProfileFromEvent(profileEvent)
if (newProfile) {
setProfile(newProfile)
setIsFetching(false)
// Clear interval once we have a profile
if (checkIntervalRef.current) {
clearInterval(checkIntervalRef.current)
checkIntervalRef.current = null
}
return true
logger.debug('[useFetchProfile] Profile found', {
pubkey: pubkey.substring(0, 8),
username: newProfile.username,
hasAvatar: !!newProfile.avatar
})
setProfile(newProfile)
setIsFetching(false)
// Clear interval once we have a profile
if (checkIntervalRef.current) {
clearInterval(checkIntervalRef.current)
checkIntervalRef.current = null
}
return true
}
logger.debug('[useFetchProfile] No profile event found', {
pubkey: pubkey.substring(0, 8)
})
return false
} catch (err) {
if (!cancelled.current) {
setError(err as Error)
setIsFetching(false)
}
return false
}
}
}, [skipCache])
useEffect(() => {
if (!id) {
@ -57,24 +71,33 @@ export function useFetchProfile(id?: string, skipCache = false) { @@ -57,24 +71,33 @@ export function useFetchProfile(id?: string, skipCache = false) {
const cancelled = { current: false }
const pubkey = userIdToPubkey(id)
if (!pubkey) {
setProfile(null)
setPubkey(null)
setIsFetching(false)
setError(new Error('Invalid id: could not extract pubkey'))
return
}
setPubkey(pubkey)
const run = async () => {
setIsFetching(true)
setError(null)
// Initial fetch
// Initial fetch - fetchReplaceableEvent checks: 1) in-memory, 2) IndexedDB, 3) network
const found = await checkProfile(pubkey, cancelled)
if (cancelled.current) return
if (found) {
// Profile found, we're done
// Profile found (from cache or network), we're done
return
}
// No profile found yet - set fetching to false but keep checking in background
// No profile found yet - set fetching to false so UI can show fallback
// The profile will remain null, allowing components to show npub fallback
setIsFetching(false)
setError(null) // Clear any previous errors
// If no profile was found, periodically re-check (profiles might load asynchronously)
// Check every 2 seconds for up to 30 seconds (15 checks)
@ -110,18 +133,26 @@ export function useFetchProfile(id?: string, skipCache = false) { @@ -110,18 +133,26 @@ export function useFetchProfile(id?: string, skipCache = false) {
checkIntervalRef.current = null
}
}
}, [id, skipCache])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, skipCache]) // checkProfile is memoized and stable, no need to include it
useEffect(() => {
if (currentAccountProfile && pubkey === currentAccountProfile.pubkey) {
setProfile(currentAccountProfile)
// Clear interval if we got the profile from current account
if (checkIntervalRef.current) {
clearInterval(checkIntervalRef.current)
checkIntervalRef.current = null
// Only use currentAccountProfile if it matches the pubkey we're looking for
// Use pubkey from the profile object to avoid reference equality issues
if (currentAccountProfile?.pubkey && pubkey && pubkey === currentAccountProfile.pubkey) {
// Only update if we don't have a profile yet (avoid unnecessary updates)
// Using a ref to track if we've already set it to prevent loops
if (!profile) {
setProfile(currentAccountProfile)
setIsFetching(false)
// Clear interval if we got the profile from current account
if (checkIntervalRef.current) {
clearInterval(checkIntervalRef.current)
checkIntervalRef.current = null
}
}
}
}, [currentAccountProfile, pubkey])
}, [currentAccountProfile?.pubkey, pubkey]) // Removed profile?.pubkey to prevent loops
return { isFetching, error, profile }
}

2
src/providers/NostrProvider/index.tsx

@ -439,6 +439,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -439,6 +439,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
if (profileEvent) {
const updatedProfileEvent = await indexedDb.putReplaceableEvent(profileEvent)
if (updatedProfileEvent.id === profileEvent.id) {
// Update in-memory cache so it's immediately available
await replaceableEventService.updateReplaceableEventCache(updatedProfileEvent)
setProfileEvent(updatedProfileEvent)
setProfile(getProfileFromEvent(updatedProfileEvent))
}

200
src/services/client-replaceable-events.service.ts

@ -22,18 +22,6 @@ export class ReplaceableEventService { @@ -22,18 +22,6 @@ export class ReplaceableEventService {
max: 50,
ttl: 1000 * 60 * 60
})
// In-memory cache for profiles - instant access, no IndexedDB blocking
private profileMemoryCache = new LRUCache<string, NEvent>({
max: 1000, // Cache up to 1000 profiles in memory
ttl: 1000 * 60 * 30, // 30 minutes TTL
updateAgeOnGet: true // Refresh TTL on access
})
// In-memory cache for all replaceable events - fast access
private replaceableEventMemoryCache = new LRUCache<string, NEvent>({
max: 2000, // Cache up to 2000 events in memory
ttl: 1000 * 60 * 30, // 30 minutes TTL
updateAgeOnGet: true
})
private replaceableEventFromBigRelaysDataloader: DataLoader<
{ pubkey: string; kind: number },
NEvent | null,
@ -146,36 +134,25 @@ export class ReplaceableEventService { @@ -146,36 +134,25 @@ export class ReplaceableEventService {
d?: string,
containingEventRelays: string[] = []
): Promise<NEvent | undefined> {
const cacheKey = d ? `${kind}:${pubkey}:${d}` : `${kind}:${pubkey}`
// 1. Check in-memory cache FIRST - instant return, no async overhead
const memoryCached = this.replaceableEventMemoryCache.get(cacheKey)
if (memoryCached) {
// Check tombstone in background (non-blocking)
this.checkTombstoneAndUpdateCache(memoryCached, kind).catch(() => {})
// Fetch in background to update cache if newer version exists
this.refreshInBackground(pubkey, kind, d).catch(() => {})
return memoryCached
}
// 2. Check IndexedDB (async but faster than network)
// 1. Check IndexedDB (async but faster than network)
try {
const indexedDbCached = await indexedDb.getReplaceableEvent(pubkey, kind, d)
if (indexedDbCached) {
// Check tombstone (non-blocking - check in background)
// Check tombstone in background (non-blocking)
const tombstoneKey = isReplaceableEvent(kind)
? getReplaceableCoordinateFromEvent(indexedDbCached)
: indexedDbCached.id
// Check tombstone in background, don't block
indexedDb.isTombstoned(tombstoneKey).then(isTombstoned => {
if (isTombstoned) {
// Remove from caches if tombstoned
this.replaceableEventMemoryCache.delete(cacheKey)
} else {
// Add to memory cache for next time
this.replaceableEventMemoryCache.set(cacheKey, indexedDbCached)
// Event is tombstoned - will be handled by IndexedDB cleanup
logger.debug('[ReplaceableEventService] Event is tombstoned', {
pubkey: formatPubkey(pubkey),
kind
})
}
}).catch(() => {})
}).catch(() => {
// If tombstone check fails, keep it in cache (better to show stale than nothing)
})
// Fetch in background to update cache if newer version exists
this.refreshInBackground(pubkey, kind, d).catch(() => {})
@ -183,9 +160,14 @@ export class ReplaceableEventService { @@ -183,9 +160,14 @@ export class ReplaceableEventService {
}
} catch (error) {
// IndexedDB error - continue to network fetch
logger.warn('[ReplaceableEventService] IndexedDB error', {
pubkey: formatPubkey(pubkey),
kind,
error: error instanceof Error ? error.message : String(error)
})
}
// 3. Not in cache, fetch from network
// 2. Not in cache, fetch from network
// Note: DataLoader will use comprehensive relay list from batch load function
// For profiles: if we have containingEventRelays (from fetchProfileEvent), include them
// Profiles are often on the same relays where the author publishes their events
@ -218,12 +200,6 @@ export class ReplaceableEventService { @@ -218,12 +200,6 @@ export class ReplaceableEventService {
// Extract relay hints from the found event (for future related fetches)
const eventRelayHints = this.extractRelayHintsFromEvent(event)
// Add to memory cache for instant access next time
this.replaceableEventMemoryCache.set(cacheKey, event)
if (kind === kinds.Metadata) {
this.profileMemoryCache.set(pubkey, event)
}
// If we found relay hints, log them (they're already used in the batch load function)
if (eventRelayHints.length > 0) {
logger.debug('[ReplaceableEventService] Found relay hints in event', {
@ -238,8 +214,7 @@ export class ReplaceableEventService { @@ -238,8 +214,7 @@ export class ReplaceableEventService {
// Log when no event is found (helps debug relay failures)
if (kind === kinds.Metadata) {
logger.debug('[ReplaceableEventService] No profile found for pubkey', {
pubkey: formatPubkey(pubkey),
cacheKey
pubkey: formatPubkey(pubkey)
})
}
} catch (error) {
@ -255,22 +230,6 @@ export class ReplaceableEventService { @@ -255,22 +230,6 @@ export class ReplaceableEventService {
return undefined
}
/**
* Check tombstone and update cache (non-blocking background operation)
*/
private async checkTombstoneAndUpdateCache(event: NEvent, kind: number): Promise<void> {
const tombstoneKey = isReplaceableEvent(kind)
? getReplaceableCoordinateFromEvent(event)
: event.id
const isTombstoned = await indexedDb.isTombstoned(tombstoneKey)
if (isTombstoned) {
const cacheKey = isReplaceableEvent(kind)
? `${kind}:${event.pubkey}`
: `${kind}:${event.pubkey}:${event.id}`
this.replaceableEventMemoryCache.delete(cacheKey)
}
}
/**
* Refresh event in background (non-blocking)
*/
@ -279,11 +238,7 @@ export class ReplaceableEventService { @@ -279,11 +238,7 @@ export class ReplaceableEventService {
if (d) {
await this.replaceableEventDataLoader.load({ pubkey, kind, d })
} else {
const event = await this.replaceableEventFromBigRelaysDataloader.load({ pubkey, kind })
if (event) {
const cacheKey = `${kind}:${pubkey}`
this.replaceableEventMemoryCache.set(cacheKey, event)
}
await this.replaceableEventFromBigRelaysDataloader.load({ pubkey, kind })
}
} catch {
// Ignore errors in background refresh
@ -292,47 +247,31 @@ export class ReplaceableEventService { @@ -292,47 +247,31 @@ export class ReplaceableEventService {
/**
* Batch fetch replaceable events from profile fetch relays
* Optimized: checks memory cache first (instant), then IndexedDB, then network
* Checks IndexedDB first, then network
*/
async fetchReplaceableEventsFromProfileFetchRelays(pubkeys: string[], kind: number): Promise<(NEvent | undefined)[]> {
// First check memory cache (instant)
const memoryCached: (NEvent | undefined)[] = []
const memoryMisses: { pubkey: string; index: number }[] = []
pubkeys.forEach((pubkey, i) => {
const cacheKey = `${kind}:${pubkey}`
const cached = this.replaceableEventMemoryCache.get(cacheKey)
if (cached) {
memoryCached[i] = cached
} else {
memoryMisses.push({ pubkey, index: i })
}
})
const results: (NEvent | undefined)[] = []
const misses: { pubkey: string; index: number }[] = []
// For memory misses, check IndexedDB in parallel
const indexedDbPromises = memoryMisses.map(async ({ pubkey, index }) => {
// Check IndexedDB in parallel
const indexedDbPromises = pubkeys.map(async (pubkey, index) => {
try {
const event = await indexedDb.getReplaceableEvent(pubkey, kind)
if (event) {
// Add to memory cache
const cacheKey = `${kind}:${pubkey}`
this.replaceableEventMemoryCache.set(cacheKey, event)
if (kind === kinds.Metadata) {
this.profileMemoryCache.set(pubkey, event)
}
memoryCached[index] = event
results[index] = event
return { index, event }
}
} catch {
// Ignore errors
}
misses.push({ pubkey, index })
return null
})
await Promise.allSettled(indexedDbPromises)
// Find what's still missing and fetch from network
const stillMissing = memoryMisses.filter(({ index }) => memoryCached[index] === undefined)
const stillMissing = misses.filter(({ index }) => results[index] === undefined)
if (stillMissing.length > 0) {
const newEvents = await this.replaceableEventFromBigRelaysDataloader.loadMany(
stillMissing.map(({ pubkey }) => ({ pubkey, kind }))
@ -341,27 +280,20 @@ export class ReplaceableEventService { @@ -341,27 +280,20 @@ export class ReplaceableEventService {
if (event && !(event instanceof Error)) {
const { index } = stillMissing[idx]!
if (index !== undefined) {
memoryCached[index] = event ?? undefined
// Add to memory cache
if (event) {
const cacheKey = `${kind}:${stillMissing[idx]!.pubkey}`
this.replaceableEventMemoryCache.set(cacheKey, event)
if (kind === kinds.Metadata) {
this.profileMemoryCache.set(stillMissing[idx]!.pubkey, event)
}
}
results[index] = event ?? undefined
}
}
})
}
return memoryCached
return results
}
/**
* Update replaceable event cache
*/
async updateReplaceableEventCache(event: NEvent): Promise<void> {
// Update DataLoader cache and IndexedDB
await this.updateReplaceableEventFromBigRelaysCache(event)
}
@ -371,28 +303,6 @@ export class ReplaceableEventService { @@ -371,28 +303,6 @@ export class ReplaceableEventService {
clearCaches(): void {
this.replaceableEventFromBigRelaysDataloader.clearAll()
this.replaceableEventDataLoader.clearAll()
this.replaceableEventMemoryCache.clear()
this.profileMemoryCache.clear()
}
/**
* Pre-load profiles into memory cache for instant access
*/
async preloadProfiles(pubkeys: string[]): Promise<void> {
// Load from IndexedDB in parallel
const promises = pubkeys.map(async (pubkey) => {
try {
const event = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata)
if (event) {
const cacheKey = `${kinds.Metadata}:${pubkey}`
this.replaceableEventMemoryCache.set(cacheKey, event)
this.profileMemoryCache.set(pubkey, event)
}
} catch {
// Ignore errors
}
})
await Promise.allSettled(promises)
}
/**
@ -457,25 +367,10 @@ export class ReplaceableEventService { @@ -457,25 +367,10 @@ export class ReplaceableEventService {
}
for (const event of events) {
// Check tombstone in background (non-blocking)
const tombstoneKey = isReplaceableEvent(event.kind)
? getReplaceableCoordinateFromEvent(event)
: event.id
// Don't block on tombstone check - do it in background
indexedDb.isTombstoned(tombstoneKey).then(isTombstoned => {
if (isTombstoned) {
const cacheKey = `${event.kind}:${event.pubkey}`
this.replaceableEventMemoryCache.delete(cacheKey)
}
}).catch(() => {})
const key = `${event.pubkey}:${event.kind}`
const existing = eventsMap.get(key)
if (!existing || existing.created_at < event.created_at) {
eventsMap.set(key, event)
// Add to memory cache
const cacheKey = `${event.kind}:${event.pubkey}`
this.replaceableEventMemoryCache.set(cacheKey, event)
}
}
})
@ -485,12 +380,6 @@ export class ReplaceableEventService { @@ -485,12 +380,6 @@ export class ReplaceableEventService {
const key = `${pubkey}:${kind}`
const event = eventsMap.get(key)
if (event) {
// Add to memory cache for instant access
const cacheKey = `${kind}:${pubkey}`
this.replaceableEventMemoryCache.set(cacheKey, event)
if (kind === kinds.Metadata) {
this.profileMemoryCache.set(pubkey, event)
}
indexedDb.putReplaceableEvent(event)
return event
} else {
@ -537,25 +426,10 @@ export class ReplaceableEventService { @@ -537,25 +426,10 @@ export class ReplaceableEventService {
})
for (const event of events) {
// Check tombstone in background (non-blocking)
const tombstoneKey = isReplaceableEvent(event.kind)
? getReplaceableCoordinateFromEvent(event)
: event.id
// Don't block on tombstone check - do it in background
indexedDb.isTombstoned(tombstoneKey).then(isTombstoned => {
if (isTombstoned) {
const cacheKey = `${event.kind}:${event.pubkey}:${d ?? ''}`
this.replaceableEventMemoryCache.delete(cacheKey)
}
}).catch(() => {})
const eventKey = `${event.pubkey}:${event.kind}:${d ?? ''}`
const existing = eventsMap.get(eventKey)
if (!existing || existing.created_at < event.created_at) {
eventsMap.set(eventKey, event)
// Add to memory cache
const cacheKey = `${event.kind}:${event.pubkey}:${d ?? ''}`
this.replaceableEventMemoryCache.set(cacheKey, event)
}
}
})
@ -565,12 +439,6 @@ export class ReplaceableEventService { @@ -565,12 +439,6 @@ export class ReplaceableEventService {
const eventKey = `${pubkey}:${kind}:${d ?? ''}`
const event = eventsMap.get(eventKey)
if (event) {
// Add to memory cache for instant access
const cacheKey = `${kind}:${pubkey}:${d ?? ''}`
this.replaceableEventMemoryCache.set(cacheKey, event)
if (kind === kinds.Metadata) {
this.profileMemoryCache.set(pubkey, event)
}
indexedDb.putReplaceableEvent(event)
return event
} else {
@ -589,6 +457,7 @@ export class ReplaceableEventService { @@ -589,6 +457,7 @@ export class ReplaceableEventService {
{ pubkey: event.pubkey, kind: event.kind },
Promise.resolve(event)
)
// Store in IndexedDB
await indexedDb.putReplaceableEvent(event)
}
@ -599,7 +468,7 @@ export class ReplaceableEventService { @@ -599,7 +468,7 @@ export class ReplaceableEventService {
/**
* Fetch profile event by id (hex, npub, nprofile)
*/
async fetchProfileEvent(id: string, skipCache: boolean = false): Promise<NEvent | undefined> {
async fetchProfileEvent(id: string, _skipCache: boolean = false): Promise<NEvent | undefined> {
let pubkey: string | undefined
let relays: string[] = []
if (/^[0-9a-f]{64}$/.test(id)) {
@ -620,12 +489,6 @@ export class ReplaceableEventService { @@ -620,12 +489,6 @@ export class ReplaceableEventService {
if (!pubkey) {
throw new Error('Invalid id')
}
if (!skipCache) {
const localProfile = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata)
if (localProfile) {
return localProfile
}
}
// For profiles: get author's relay list (from cache if available) and use those relays
// Profiles are often on the same relays where the author publishes their events
@ -650,6 +513,7 @@ export class ReplaceableEventService { @@ -650,6 +513,7 @@ export class ReplaceableEventService {
})
}
// Use fetchReplaceableEvent which checks IndexedDB then network
const profileEvent = await this.fetchReplaceableEvent(pubkey, kinds.Metadata, undefined, relays)
if (profileEvent) {
await this.indexProfile(profileEvent)

11
src/services/indexed-db.service.ts

@ -452,10 +452,17 @@ class IndexedDbService { @@ -452,10 +452,17 @@ class IndexedDbService {
return resolve(undefined)
}
// Invalidate profile and payment info cache when stale so they refetch regularly
// BUT: Always return cached profiles even if stale - we'll refresh in background
// This ensures profiles are always visible, even if slightly outdated
const isProfileOrPayment = kind === kinds.Metadata || kind === ExtendedKind.PAYMENT_INFO
if (isProfileOrPayment && row.addedAt && Date.now() - row.addedAt > PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS) {
transaction.commit()
return resolve(undefined)
// Profile is stale, but return it anyway - refresh will happen in background
// This prevents the "no profile" state when cache exists but is just old
logger.debug('[IndexedDB] Profile cache is stale but returning anyway', {
pubkey: pubkey.substring(0, 8),
age: Date.now() - row.addedAt,
maxAge: PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS
})
}
transaction.commit()
resolve(row.value)

Loading…
Cancel
Save