140 lines
3.5 KiB
TypeScript
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()
|