story-research-zapwall/lib/objectCache.ts
2026-01-06 00:54:49 +01:00

195 lines
6.0 KiB
TypeScript

/**
* IndexedDB cache for Nostr objects (authors, series, publications, reviews)
* Objects are indexed by their hash ID for fast retrieval
* One database per object type
*/
import type { Event } from 'nostr-tools'
import type { AuthorPresentationArticle } from '@/types/nostr'
export type ObjectType = 'author' | 'series' | 'publication' | 'review'
interface CachedObject {
hashId: string
event: Event
parsed: unknown // Parsed object (AuthorPresentationArticle, Series, etc.)
version: number
hidden: boolean
createdAt: number
cachedAt: number
}
const DB_PREFIX = 'nostr_objects_'
const DB_VERSION = 1
class ObjectCacheService {
private dbs: Map<ObjectType, IDBDatabase> = new Map()
private async initDB(objectType: ObjectType): Promise<IDBDatabase> {
if (this.dbs.has(objectType)) {
return this.dbs.get(objectType)!
}
return new Promise((resolve, reject) => {
if (typeof window === 'undefined' || !window.indexedDB) {
reject(new Error('IndexedDB is not available'))
return
}
const dbName = `${DB_PREFIX}${objectType}`
const request = indexedDB.open(dbName, DB_VERSION)
request.onerror = () => {
reject(new Error(`Failed to open IndexedDB: ${request.error}`))
}
request.onsuccess = () => {
const db = request.result
this.dbs.set(objectType, db)
resolve(db)
}
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result
if (!db.objectStoreNames.contains('objects')) {
const store = db.createObjectStore('objects', { keyPath: 'hashId' })
store.createIndex('version', 'version', { unique: false })
store.createIndex('hidden', 'hidden', { unique: false })
store.createIndex('cachedAt', 'cachedAt', { unique: false })
}
}
})
}
/**
* Store an object in cache
*/
async set(objectType: ObjectType, hashId: string, event: Event, parsed: unknown, version: number, hidden: boolean): Promise<void> {
try {
const db = await this.initDB(objectType)
const transaction = db.transaction(['objects'], 'readwrite')
const store = transaction.objectStore('objects')
const cached: CachedObject = {
hashId,
event,
parsed,
version,
hidden,
createdAt: event.created_at,
cachedAt: Date.now(),
}
await new Promise<void>((resolve, reject) => {
const request = store.put(cached)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error(`Error caching ${objectType} object:`, error)
}
}
/**
* Get an object from cache by hash ID
* Returns the latest non-hidden version
*/
async get(objectType: ObjectType, hashId: string): Promise<unknown | null> {
try {
const db = await this.initDB(objectType)
const transaction = db.transaction(['objects'], 'readonly')
const store = transaction.objectStore('objects')
const index = store.index('version')
return new Promise((resolve, reject) => {
const request = index.openCursor(IDBKeyRange.bound([hashId, 0], [hashId, Infinity]))
const objects: CachedObject[] = []
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
const obj = cursor.value as CachedObject
if (obj && obj.hashId === hashId && !obj.hidden) {
objects.push(obj)
}
cursor.continue()
} else {
// Sort by version descending and return the latest
if (objects.length > 0) {
objects.sort((a, b) => b.version - a.version)
resolve(objects[0]?.parsed ?? null)
} else {
resolve(null)
}
}
}
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error(`Error retrieving ${objectType} object from cache:`, error)
return null
}
}
/**
* Get an author presentation by pubkey (searches all cached authors)
*/
async getAuthorByPubkey(pubkey: string): Promise<AuthorPresentationArticle | null> {
try {
const db = await this.initDB('author')
const transaction = db.transaction(['objects'], 'readonly')
const store = transaction.objectStore('objects')
return new Promise((resolve, reject) => {
const request = store.openCursor()
const objects: CachedObject[] = []
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
const obj = cursor.value as CachedObject
if (obj && obj.event.pubkey === pubkey && !obj.hidden) {
objects.push(obj)
}
cursor.continue()
} else {
// Sort by version descending and return the latest
if (objects.length > 0) {
objects.sort((a, b) => b.version - a.version)
resolve((objects[0]?.parsed ?? null) as AuthorPresentationArticle | null)
} else {
resolve(null)
}
}
}
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error('Error retrieving author from cache by pubkey:', error)
return null
}
}
/**
* Clear cache for an object type
*/
async clear(objectType: ObjectType): Promise<void> {
try {
const db = await this.initDB(objectType)
const transaction = db.transaction(['objects'], 'readwrite')
const store = transaction.objectStore('objects')
await new Promise<void>((resolve, reject) => {
const request = store.clear()
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error(`Error clearing ${objectType} cache:`, error)
}
}
}
export const objectCache = new ObjectCacheService()