/** * Event caching with IndexedDB */ import { getDB } from './indexeddb-store.js'; import { isEventDeleted, getDeletedEventIds } from './deletion-tracker.js'; import type { NostrEvent } from '../../types/nostr.js'; export interface CachedEvent extends NostrEvent { cached_at: number; } /** * Store an event in cache */ export async function cacheEvent(event: NostrEvent): Promise { try { // Don't cache events that have deletion requests if (await isEventDeleted(event.id)) { return; } const db = await getDB(); const cached: CachedEvent = { ...event, cached_at: Date.now() }; await db.put('events', cached); } catch (error) { console.debug('Error caching event:', error); // Don't throw - caching failures shouldn't break the app } } /** * Store multiple events in cache */ export async function cacheEvents(events: NostrEvent[]): Promise { try { if (events.length === 0) return; // Check which events are marked as deleted (complete this transaction first) const eventIds = events.map(e => e.id); const deletedIds = await getDeletedEventIds(eventIds); // Filter out deleted events const eventsToCache = events.filter(e => !deletedIds.has(e.id)); if (eventsToCache.length === 0) return; // Create a new transaction for writing (after the read transaction is complete) const db = await getDB(); const tx = db.transaction('events', 'readwrite'); // Prepare all cached events first const cachedEvents: CachedEvent[] = eventsToCache.map(event => ({ ...event, cached_at: Date.now() })); // Put all events in a single batch using Promise.all for better performance await Promise.all(cachedEvents.map(cached => tx.store.put(cached))); // Wait for transaction to complete await tx.done; } catch (error) { console.debug('Error caching events:', error); // Don't throw - caching failures shouldn't break the app } } /** * Get event by ID from cache */ export async function getEvent(id: string): Promise { try { const db = await getDB(); return await db.get('events', id); } catch (error) { console.debug('Error getting event from cache:', error); return undefined; } } /** * Get events by kind */ export async function getEventsByKind(kind: number, limit?: number): Promise { try { const db = await getDB(); const tx = db.transaction('events', 'readonly'); const index = tx.store.index('kind'); // Use getAll() to get all matching events in one operation // This keeps the transaction active and avoids cursor iteration issues const events = await index.getAll(kind); await tx.done; // Sort and limit after fetching const sorted = events.sort((a, b) => b.created_at - a.created_at); return limit ? sorted.slice(0, limit) : sorted; } catch (error) { console.debug('Error getting events by kind from cache:', error); return []; } } /** * Get events by pubkey */ export async function getEventsByPubkey(pubkey: string, limit?: number): Promise { try { const db = await getDB(); const tx = db.transaction('events', 'readonly'); const index = tx.store.index('pubkey'); // Use getAll() to get all matching events in one operation // This keeps the transaction active and avoids cursor iteration issues const events = await index.getAll(pubkey); await tx.done; // Sort and limit after fetching const sorted = events.sort((a, b) => b.created_at - a.created_at); return limit ? sorted.slice(0, limit) : sorted; } catch (error) { console.debug('Error getting events by pubkey from cache:', error); return []; } } /** * Delete an event by ID from cache */ export async function deleteEvent(id: string): Promise { const db = await getDB(); await db.delete('events', id); } /** * Delete multiple events by ID from cache */ export async function deleteEvents(ids: string[]): Promise { const db = await getDB(); const tx = db.transaction('events', 'readwrite'); // Use Promise.all for batch deletion await Promise.all(ids.map(id => tx.store.delete(id))); await tx.done; } /** * Get recent events from cache by kind(s) (within cache TTL) * Returns events that were cached recently and match the specified kinds */ export async function getRecentCachedEvents(kinds: number[], maxAge: number = 15 * 60 * 1000, limit: number = 50): Promise { try { const db = await getDB(); const now = Date.now(); const cutoffTime = now - maxAge; const results: CachedEvent[] = []; const seen = new Set(); // Optimized: Use single transaction for all kinds const tx = db.transaction('events', 'readonly'); const kindIndex = tx.store.index('kind'); // Get events for all kinds in parallel within single transaction const kindPromises = kinds.map(async (kind) => { try { return await kindIndex.getAll(kind); } catch (error) { console.debug(`Error getting events for kind ${kind}:`, error); return []; } }); const allKindResults = await Promise.all(kindPromises); await tx.done; // Flatten and filter by cache age and deduplicate for (const events of allKindResults) { for (const event of events) { if (event.cached_at >= cutoffTime && !seen.has(event.id)) { seen.add(event.id); results.push(event); } } } // Sort by created_at (newest first) and limit const sorted = results.sort((a, b) => b.created_at - a.created_at); return sorted.slice(0, limit); } catch (error) { console.debug('Error getting recent cached events:', error); return []; } } /** * Get recent feed events from cache (within cache TTL) * Returns events that were cached recently and match feed kinds * @deprecated Use getRecentCachedEvents instead */ export async function getRecentFeedEvents(kinds: number[], maxAge: number = 15 * 60 * 1000, limit: number = 50): Promise { return getRecentCachedEvents(kinds, maxAge, limit); } /** * Clear old events (older than specified timestamp) */ export async function clearOldEvents(olderThan: number): Promise { const db = await getDB(); const tx = db.transaction('events', 'readwrite'); const index = tx.store.index('created_at'); for await (const cursor of index.iterate()) { if (cursor.value.created_at < olderThan) { await cursor.delete(); } } await tx.done; }