2025-12-22 09:48:57 +01:00

214 lines
5.5 KiB
TypeScript

const DB_NAME = 'nostr_paywall'
const DB_VERSION = 1
const STORE_NAME = 'article_content'
interface DBData {
id: string
data: any
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 = 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
// Create object store if it doesn't exist
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 })
}
}
})
try {
await this.initPromise
} catch (error) {
this.initPromise = null
throw error
}
}
/**
* Store data in IndexedDB
*/
async set(key: string, value: any, expiresIn?: number): Promise<void> {
try {
await this.init()
if (!this.db) {
throw new Error('Database not initialized')
}
const now = Date.now()
const data: DBData = {
id: key,
data: value,
createdAt: now,
expiresAt: expiresIn ? now + expiresIn : undefined,
}
return new Promise((resolve, reject) => {
const transaction = this.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 = any>(key: string): Promise<T | null> {
try {
await this.init()
if (!this.db) {
throw new Error('Database not initialized')
}
return new Promise((resolve, reject) => {
const transaction = this.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
}
// Check if expired
if (result.expiresAt && result.expiresAt < Date.now()) {
// Delete expired data
this.delete(key).catch(console.error)
resolve(null)
return
}
resolve(result.data as T)
}
request.onerror = () => reject(new Error(`Failed to get data: ${request.error}`))
})
} catch (error) {
console.error('Error getting from IndexedDB:', error)
return null
}
}
/**
* Delete data from IndexedDB
*/
async delete(key: string): Promise<void> {
try {
await this.init()
if (!this.db) {
throw new Error('Database not initialized')
}
return new Promise((resolve, reject) => {
const transaction = this.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')
}
return new Promise((resolve, reject) => {
const transaction = this.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()