269 lines
9.6 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
}
|