You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
6.5 KiB
225 lines
6.5 KiB
/** |
|
* 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<void> { |
|
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<void> { |
|
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<CachedEvent | undefined> { |
|
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<CachedEvent[]> { |
|
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<CachedEvent[]> { |
|
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<void> { |
|
const db = await getDB(); |
|
await db.delete('events', id); |
|
} |
|
|
|
/** |
|
* Delete multiple events by ID from cache |
|
*/ |
|
export async function deleteEvents(ids: string[]): Promise<void> { |
|
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<CachedEvent[]> { |
|
try { |
|
const db = await getDB(); |
|
const now = Date.now(); |
|
const cutoffTime = now - maxAge; |
|
|
|
const results: CachedEvent[] = []; |
|
const seen = new Set<string>(); |
|
|
|
// 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<CachedEvent[]> { |
|
return getRecentCachedEvents(kinds, maxAge, limit); |
|
} |
|
|
|
/** |
|
* Clear old events (older than specified timestamp) |
|
*/ |
|
export async function clearOldEvents(olderThan: number): Promise<void> { |
|
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; |
|
}
|
|
|