- **Motivations :** Assurer passage du lint strict et clarifier la logique paiements/publications. - **Root causes :** Fonctions trop longues, promesses non gérées et typages WebLN/Nostr incomplets. - **Correctifs :** Refactor PaymentModal (handlers void), extraction helpers articlePublisher, simplification polling sponsoring/zap, corrections curly et awaits. - **Evolutions :** Nouveau module articlePublisherHelpers pour présentation/aiguillage contenu privé. - **Page affectées :** components/PaymentModal.tsx, lib/articlePublisher.ts, lib/articlePublisherHelpers.ts, lib/paymentPolling.ts, lib/sponsoring.ts, lib/nostrZapVerification.ts et dépendances liées.
233 lines
5.7 KiB
TypeScript
233 lines
5.7 KiB
TypeScript
const DB_NAME = 'nostr_paywall'
|
|
const DB_VERSION = 1
|
|
const STORE_NAME = 'article_content'
|
|
|
|
interface DBData {
|
|
id: string
|
|
data: unknown
|
|
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, 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,
|
|
}
|
|
|
|
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): Promise<T | null> {
|
|
try {
|
|
await this.init()
|
|
|
|
if (!this.db) {
|
|
throw new Error('Database not initialized')
|
|
}
|
|
|
|
return this.readValue<T>(key)
|
|
} catch (error) {
|
|
console.error('Error getting from IndexedDB:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
private readValue<T>(key: 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
|
|
}
|
|
|
|
resolve(result.data as T)
|
|
}
|
|
|
|
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()
|