import type { Event as NostrEvent } from 'nostr-tools' import type { AuthorPresentationArticle } from '@/types/nostr' import { buildObjectId } from '../urlGenerator' import type { IndexedDBHelper } from '../helpers/indexedDBHelper' import type { CachedObject, ObjectType } from './types' import { createDbHelperForObjectType, getRequiredDbHelper } from './db' export class ObjectCacheService { private readonly dbHelpers: Map = new Map() private getDBHelper(objectType: ObjectType): IndexedDBHelper { if (!this.dbHelpers.has(objectType)) { const helper = createDbHelperForObjectType(objectType) this.dbHelpers.set(objectType, helper) } return getRequiredDbHelper(this.dbHelpers, objectType) } private async initDB(objectType: ObjectType): Promise { const helper = this.getDBHelper(objectType) return helper.init() } private async countObjectsWithHash(objectType: ObjectType, hash: string): Promise { try { const helper = this.getDBHelper(objectType) return helper.countByIndex('hash', IDBKeyRange.only(hash)) } catch (countError) { console.error(`Error counting objects with hash ${hash}:`, countError) return 0 } } async set(params: { objectType: ObjectType hash: string event: NostrEvent parsed: unknown version: number hidden: boolean index?: number published?: false | string[] }): Promise { try { const helper = this.getDBHelper(params.objectType) const index = await this.resolveIndex(params.objectType, params.hash, params.index) const id = buildObjectId(params.hash, index, params.version) const published = await this.resolvePublishedForUpsert(helper, id, params.published) await helper.put(this.buildCachedObject(params, id, index, published)) } catch (cacheError) { console.error(`Error caching ${params.objectType} object:`, cacheError) } } private async resolveIndex(objectType: ObjectType, hash: string, index: number | undefined): Promise { if (index !== undefined) { return index } return this.countObjectsWithHash(objectType, hash) } private async resolvePublishedForUpsert(helper: IndexedDBHelper, id: string, published: false | string[] | undefined): Promise { const nextPublished = published ?? false if (nextPublished !== false) { return nextPublished } const existing = await helper.get(id).catch(() => null) return existing ? existing.published : false } private buildCachedObject( params: { objectType: ObjectType; hash: string; event: NostrEvent; parsed: unknown; version: number; hidden: boolean }, id: string, index: number, published: false | string[] ): CachedObject { return { id, hash: params.hash, hashId: params.hash, index, event: params.event, parsed: params.parsed, version: params.version, hidden: params.hidden, createdAt: params.event.created_at, cachedAt: Date.now(), published, } } async updatePublished(objectType: ObjectType, id: string, published: false | string[]): Promise { try { const helper = this.getDBHelper(objectType) const existing = await helper.get(id) if (!existing) { console.warn(`Object ${id} not found in cache, cannot update published status`) return } const oldPublished = existing.published await helper.put({ ...existing, published }) if (oldPublished === false && Array.isArray(published) && published.length > 0) { const eventId = id.split(':')[1] ?? id void import('../notificationDetector') .then(({ notificationDetector }) => { void notificationDetector.checkObjectChange({ objectType, objectId: id, eventId, oldPublished, newPublished: published }) }) .catch((error) => { console.error('Failed to notify published status change:', error) }) } } catch (updateError) { console.error(`Error updating published status for ${objectType} object:`, updateError) } } async getUnpublished(objectType: ObjectType): Promise> { try { const db = await this.initDB(objectType) const store = db.transaction(['objects'], 'readonly').objectStore('objects') return new Promise((resolve, reject) => { const request = store.openCursor() const unpublished: Array<{ id: string; event: NostrEvent }> = [] request.onsuccess = (event: globalThis.Event): void => { const cursor = (event.target as IDBRequest).result if (cursor) { const obj = cursor.value as CachedObject if (obj.published === false && !obj.hidden) { unpublished.push({ id: obj.id, event: obj.event }) } cursor.continue() } else { resolve(unpublished) } } request.onerror = (): void => reject(request.error) }) } catch (getUnpublishedError) { console.error(`Error retrieving unpublished ${objectType} objects:`, getUnpublishedError) return [] } } async get(objectType: ObjectType, hash: string): Promise { try { const db = await this.initDB(objectType) const store = db.transaction(['objects'], 'readonly').objectStore('objects') const hashIndex = store.index('hash') return new Promise((resolve, reject) => { const request = hashIndex.openCursor(IDBKeyRange.only(hash)) const objects: CachedObject[] = [] request.onsuccess = (event: globalThis.Event): void => { const cursor = (event.target as IDBRequest).result if (cursor) { const obj = cursor.value as CachedObject if (obj?.hash === hash && !obj.hidden) { objects.push(obj) } cursor.continue() } else { if (objects.length === 0) { resolve(null) return } objects.sort((a, b) => b.version - a.version) resolve(objects[0]?.parsed ?? null) } } request.onerror = (): void => reject(request.error) }) } catch (retrieveError) { console.error(`Error retrieving ${objectType} object from cache:`, retrieveError) return null } } async getById(objectType: ObjectType, id: string): Promise { try { const helper = this.getDBHelper(objectType) const obj = await helper.get(id) return obj && !obj.hidden ? obj.parsed : null } catch (retrieveByIdError) { console.error(`Error retrieving ${objectType} object by ID from cache:`, retrieveByIdError) return null } } async getEventById(objectType: ObjectType, id: string): Promise { try { const helper = this.getDBHelper(objectType) const obj = await helper.get(id) return obj && !obj.hidden ? obj.event : null } catch (retrieveByIdError) { console.error(`Error retrieving ${objectType} event by ID from cache:`, retrieveByIdError) return null } } async getAuthorByPubkey(pubkey: string): Promise { try { const db = await this.initDB('author') const store = db.transaction(['objects'], 'readonly').objectStore('objects') return new Promise((resolve, reject) => { const request = store.openCursor() const objects: CachedObject[] = [] request.onsuccess = (event: globalThis.Event): void => { const cursor = (event.target as IDBRequest).result if (cursor) { const obj = cursor.value as CachedObject if (obj?.event.pubkey === pubkey && !obj.hidden) { objects.push(obj) } cursor.continue() } else { if (objects.length === 0) { resolve(null) return } objects.sort((a, b) => b.version - a.version) resolve((objects[0]?.parsed ?? null) as AuthorPresentationArticle | null) } } request.onerror = (): void => reject(request.error) }) } catch (authorRetrieveError) { console.error('Error retrieving author from cache by pubkey:', authorRetrieveError) return null } } async getAll(objectType: ObjectType): Promise { try { const db = await this.initDB(objectType) const store = db.transaction(['objects'], 'readonly').objectStore('objects') return new Promise((resolve, reject) => { const request = store.openCursor() const objects: unknown[] = [] request.onsuccess = (event: globalThis.Event): void => { const cursor = (event.target as IDBRequest).result if (cursor) { const obj = cursor.value as CachedObject if (!obj.hidden) { objects.push(obj.parsed) } cursor.continue() } else { resolve(objects) } } request.onerror = (): void => reject(request.error) }) } catch (getAllError) { console.error(`Error retrieving all ${objectType} objects from cache:`, getAllError) return [] } } async clear(objectType: ObjectType): Promise { try { const helper = this.getDBHelper(objectType) await helper.clear() } catch (clearError) { console.error(`Error clearing ${objectType} cache:`, clearError) } } }