import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify'; import { openDatabase, STORE } from '@/lib/db'; // ============================================================================ // Kind 0 Profile IndexedDB Cache // // Caches kind 0 profile events so repeat visits render author names, avatars, // and other metadata instantly instead of showing loading skeletons. // Each entry stores the raw NostrEvent, the pre-parsed metadata, and a // `lastFetched` timestamp so the caller can decide when to re-check. // // Storing the parsed metadata avoids re-running expensive Zod validation // on every render when used as TanStack Query `initialData`. // ============================================================================ export interface ProfileCacheEntry { /** Hex pubkey of the profile author */ pubkey: string; /** The raw kind 0 NostrEvent */ event: NostrEvent; /** Pre-parsed metadata (avoids re-running Zod on every render) */ metadata?: NostrMetadata; /** Unix-ms timestamp of the last successful fetch */ lastFetched: number; } // --------------------------------------------------------------------------- // In-memory mirror — hydrated once from IndexedDB so React hooks can read // synchronously on first render (no async waterfall). // --------------------------------------------------------------------------- const memoryCache = new Map(); let hydrated = false; let hydratePromise: Promise | null = null; /** Ensure the in-memory mirror is populated. Safe to call many times. */ export function hydrateProfileCache(): Promise { if (hydrated) return Promise.resolve(); if (hydratePromise) return hydratePromise; hydratePromise = (async () => { try { const db = await openDatabase(); if (!db) return; // IndexedDB unavailable — skip hydration. const entries: ProfileCacheEntry[] = await db.getAll(STORE.PROFILES); for (const entry of entries) { memoryCache.set(entry.pubkey, entry); } } catch { // IndexedDB read failure — silently degrade. } finally { hydrated = true; } })(); return hydratePromise; } /** Read a cached profile synchronously from the in-memory mirror. */ export function getProfileCached(pubkey: string): ProfileCacheEntry | undefined { return memoryCache.get(pubkey); } /** * Persist a kind 0 profile event with its pre-parsed metadata. * Only writes if the event is newer than (or equal to) what we already have, * so out-of-order arrivals don't downgrade the cache. * Updates both the in-memory mirror and IndexedDB. * * Passing `metadata` is optional but recommended — it avoids re-running Zod * validation when the cached entry is later used as TanStack Query `initialData`. */ export async function setProfileCached(event: NostrEvent, metadata?: NostrMetadata): Promise { const existing = memoryCache.get(event.pubkey); if (existing && existing.event.created_at > event.created_at) { return; // Don't overwrite a newer event with an older one. } const entry: ProfileCacheEntry = { pubkey: event.pubkey, event, metadata, lastFetched: Date.now(), }; memoryCache.set(event.pubkey, entry); try { const db = await openDatabase(); if (db) await db.put(STORE.PROFILES, entry, event.pubkey); } catch { // Write failure is non-critical — the in-memory cache still works. } } /** Remove a single profile entry. */ export async function deleteProfileCached(pubkey: string): Promise { memoryCache.delete(pubkey); try { const db = await openDatabase(); if (db) await db.delete(STORE.PROFILES, pubkey); } catch { // Non-critical. } } /** Clear the entire profile cache. */ export async function clearProfileCache(): Promise { memoryCache.clear(); try { const db = await openDatabase(); if (db) await db.clear(STORE.PROFILES); } catch { // Non-critical. } }