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

/**
* 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;
}