2ecd557430
openDatabase() now catches errors from idb's openDB() (which throws synchronously when indexedDB is undefined) and returns null. All consumers — profileCache, nip05Cache, dmMessageStore — check for null and silently degrade to in-memory only. The DM message store also stops re-throwing errors, which previously could produce unhandled rejections in DMProvider.
120 lines
3.9 KiB
TypeScript
120 lines
3.9 KiB
TypeScript
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<string, ProfileCacheEntry>();
|
|
let hydrated = false;
|
|
let hydratePromise: Promise<void> | null = null;
|
|
|
|
/** Ensure the in-memory mirror is populated. Safe to call many times. */
|
|
export function hydrateProfileCache(): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
memoryCache.clear();
|
|
|
|
try {
|
|
const db = await openDatabase();
|
|
if (db) await db.clear(STORE.PROFILES);
|
|
} catch {
|
|
// Non-critical.
|
|
}
|
|
}
|