story-research-zapwall/lib/objectCache/ObjectCacheService.ts
2026-01-13 14:49:19 +01:00

269 lines
9.6 KiB
TypeScript

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<ObjectType, IndexedDBHelper> = 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<IDBDatabase> {
const helper = this.getDBHelper(objectType)
return helper.init()
}
private async countObjectsWithHash(objectType: ObjectType, hash: string): Promise<number> {
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<void> {
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<number> {
if (index !== undefined) {
return index
}
return this.countObjectsWithHash(objectType, hash)
}
private async resolvePublishedForUpsert(helper: IndexedDBHelper, id: string, published: false | string[] | undefined): Promise<false | string[]> {
const nextPublished = published ?? false
if (nextPublished !== false) {
return nextPublished
}
const existing = await helper.get<CachedObject>(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<void> {
try {
const helper = this.getDBHelper(objectType)
const existing = await helper.get<CachedObject>(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<Array<{ id: string; event: NostrEvent }>> {
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<IDBCursorWithValue>).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<unknown> {
try {
const db = await this.initDB(objectType)
const store = db.transaction(['objects'], 'readonly').objectStore('objects')
const hashIndex = store.index('hash')
return new Promise<unknown>((resolve, reject) => {
const request = hashIndex.openCursor(IDBKeyRange.only(hash))
const objects: CachedObject[] = []
request.onsuccess = (event: globalThis.Event): void => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).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<unknown> {
try {
const helper = this.getDBHelper(objectType)
const obj = await helper.get<CachedObject>(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<NostrEvent | null> {
try {
const helper = this.getDBHelper(objectType)
const obj = await helper.get<CachedObject>(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<AuthorPresentationArticle | null> {
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<IDBCursorWithValue>).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<unknown[]> {
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<IDBCursorWithValue>).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<void> {
try {
const helper = this.getDBHelper(objectType)
await helper.clear()
} catch (clearError) {
console.error(`Error clearing ${objectType} cache:`, clearError)
}
}
}