2026-01-07 03:10:40 +01:00

140 lines
3.5 KiB
TypeScript

import { decryptPayload, encryptPayload, type EncryptedPayload } from './cryptoHelpers'
import { createIndexedDBHelper, type IndexedDBHelper } from '../helpers/indexedDBHelper'
const DB_NAME = 'nostr_paywall'
const DB_VERSION = 1
const STORE_NAME = 'article_content'
interface DBData {
id: string
data: EncryptedPayload
createdAt: number
expiresAt?: number
}
/**
* IndexedDB storage service for article content
* More robust than localStorage and supports larger data sizes
*/
export class IndexedDBStorage {
private readonly dbHelper: IndexedDBHelper
constructor() {
this.dbHelper = createIndexedDBHelper({
dbName: DB_NAME,
version: DB_VERSION,
storeName: STORE_NAME,
keyPath: 'id',
indexes: [
{ name: 'createdAt', keyPath: 'createdAt', unique: false },
{ name: 'expiresAt', keyPath: 'expiresAt', unique: false },
],
})
}
/**
* Store data in IndexedDB
*/
async set(key: string, value: unknown, secret: string, expiresIn?: number): Promise<void> {
try {
const encrypted = await encryptPayload(secret, value)
const now = Date.now()
const data: DBData = {
id: key,
data: encrypted,
createdAt: now,
...(expiresIn ? { expiresAt: now + expiresIn } : {}),
}
await this.dbHelper.put(data)
} catch (error) {
console.error('Error storing in IndexedDB:', error)
throw error
}
}
/**
* Get data from IndexedDB
*/
async get<T = unknown>(key: string, secret: string): Promise<T | null> {
try {
const result = await this.dbHelper.get<DBData>(key)
if (!result) {
return null
}
if (result.expiresAt && result.expiresAt < Date.now()) {
await this.delete(key).catch(console.error)
return null
}
try {
return await decryptPayload<T>(secret, result.data)
} catch (decryptError) {
console.error('Error decrypting from IndexedDB:', decryptError)
return null
}
} catch (error) {
console.error('Error getting from IndexedDB:', error)
return null
}
}
/**
* Delete data from IndexedDB
*/
async delete(key: string): Promise<void> {
try {
await this.dbHelper.delete(key)
} catch (error) {
console.error('Error deleting from IndexedDB:', error)
throw error
}
}
/**
* Clear all expired entries
*/
async clearExpired(): Promise<void> {
try {
const store = await this.dbHelper.getStoreWrite('readwrite')
const index = store.index('expiresAt')
return new Promise<void>((resolve, reject) => {
const request = index.openCursor(IDBKeyRange.upperBound(Date.now()))
request.onsuccess = (event): void => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
cursor.delete()
cursor.continue()
} else {
resolve()
}
}
request.onerror = (): void => {
if (request.error) {
reject(new Error(`Failed to clear expired: ${request.error}`))
} else {
reject(new Error('Unknown error clearing expired entries'))
}
}
})
} catch (error) {
console.error('Error clearing expired entries:', error)
throw error
}
}
/**
* Check if IndexedDB is available
*/
static isAvailable(): boolean {
return typeof window !== 'undefined' && typeof window.indexedDB !== 'undefined'
}
}
export const storageService = new IndexedDBStorage()