2025-12-23 02:20:57 +01:00

241 lines
6.1 KiB
TypeScript

import { decryptPayload, encryptPayload, type EncryptedPayload } from './cryptoHelpers'
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 db: IDBDatabase | null = null
private initPromise: Promise<void> | null = null
/**
* Initialize the IndexedDB database
*/
private async init(): Promise<void> {
if (this.db) {
return
}
if (this.initPromise) {
return this.initPromise
}
this.initPromise = this.openDatabase()
try {
await this.initPromise
} catch (error) {
this.initPromise = null
throw error
}
}
private openDatabase(): Promise<void> {
return new Promise((resolve, reject) => {
if (typeof window === 'undefined' || !window.indexedDB) {
reject(new Error('IndexedDB is not available. This application requires IndexedDB support.'))
return
}
const request = indexedDB.open(DB_NAME, DB_VERSION)
request.onerror = () => {
reject(new Error(`Failed to open IndexedDB: ${request.error}`))
}
request.onsuccess = () => {
this.db = request.result
resolve()
}
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })
store.createIndex('createdAt', 'createdAt', { unique: false })
store.createIndex('expiresAt', 'expiresAt', { unique: false })
}
}
})
}
/**
* Store data in IndexedDB
*/
async set(key: string, value: unknown, secret: string, expiresIn?: number): Promise<void> {
try {
await this.init()
if (!this.db) {
throw new Error('Database not initialized')
}
const encrypted = await encryptPayload(secret, value)
const now = Date.now()
const data: DBData = {
id: key,
data: encrypted,
createdAt: now,
...(expiresIn ? { expiresAt: now + expiresIn } : {}),
}
const db = this.db
if (!db) {
throw new Error('Database not initialized')
}
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite')
const store = transaction.objectStore(STORE_NAME)
const request = store.put(data)
request.onsuccess = () => resolve()
request.onerror = () => reject(new Error(`Failed to store data: ${request.error}`))
})
} 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 {
await this.init()
if (!this.db) {
throw new Error('Database not initialized')
}
return this.readValue<T>(key, secret)
} catch (error) {
console.error('Error getting from IndexedDB:', error)
return null
}
}
private readValue<T>(key: string, secret: string): Promise<T | null> {
const db = this.db
if (!db) {
throw new Error('Database not initialized')
}
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly')
const store = transaction.objectStore(STORE_NAME)
const request = store.get(key)
request.onsuccess = () => {
const result = request.result as DBData | undefined
if (!result) {
resolve(null)
return
}
if (result.expiresAt && result.expiresAt < Date.now()) {
this.delete(key).catch(console.error)
resolve(null)
return
}
decryptPayload<T>(secret, result.data)
.then((value) => resolve(value))
.catch((error) => {
console.error('Error decrypting from IndexedDB:', error)
resolve(null)
})
}
request.onerror = () => reject(new Error(`Failed to get data: ${request.error}`))
})
}
/**
* Delete data from IndexedDB
*/
async delete(key: string): Promise<void> {
try {
await this.init()
if (!this.db) {
throw new Error('Database not initialized')
}
const db = this.db
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite')
const store = transaction.objectStore(STORE_NAME)
const request = store.delete(key)
request.onsuccess = () => resolve()
request.onerror = () => reject(new Error(`Failed to delete data: ${request.error}`))
})
} catch (error) {
console.error('Error deleting from IndexedDB:', error)
throw error
}
}
/**
* Clear all expired entries
*/
async clearExpired(): Promise<void> {
try {
await this.init()
if (!this.db) {
throw new Error('Database not initialized')
}
const db = this.db
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite')
const store = transaction.objectStore(STORE_NAME)
const index = store.index('expiresAt')
const request = index.openCursor(IDBKeyRange.upperBound(Date.now()))
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result
if (cursor) {
cursor.delete()
cursor.continue()
} else {
resolve()
}
}
request.onerror = () => reject(new Error(`Failed to clear expired: ${request.error}`))
})
} 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()