/** * Publication log service - stores publication attempts and results in IndexedDB */ const DB_NAME = 'nostr_publish_log' const DB_VERSION = 1 const STORE_NAME = 'publications' interface PublicationLogEntry { id: string // Event ID eventId: string // Event ID (duplicate for easier querying) relayUrl: string success: boolean error?: string timestamp: number objectType?: string // Type of object being published (author, series, publication, etc.) objectId?: string // ID of the object in cache } class PublishLogService { private db: IDBDatabase | null = null private initPromise: Promise | null = null private async init(): Promise { 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 { return new Promise((resolve, reject) => { if (typeof window === 'undefined' || !window.indexedDB) { reject(new Error('IndexedDB is not available')) return } const request = indexedDB.open(DB_NAME, DB_VERSION) request.onerror = (): void => { reject(new Error(`Failed to open IndexedDB: ${request.error}`)) } request.onsuccess = (): void => { this.db = request.result resolve() } request.onupgradeneeded = (event: IDBVersionChangeEvent): void => { const db = (event.target as IDBOpenDBRequest).result if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true }) store.createIndex('eventId', 'eventId', { unique: false }) store.createIndex('relayUrl', 'relayUrl', { unique: false }) store.createIndex('timestamp', 'timestamp', { unique: false }) store.createIndex('success', 'success', { unique: false }) } } }) } /** * Log a publication attempt * Utilise writeService pour écrire via Web Worker */ async logPublication( eventId: string, relayUrl: string, success: boolean, error?: string, objectType?: string, objectId?: string ): Promise { // Utiliser writeService pour logger via Web Worker const { writeService } = await import('./writeService') await writeService.logPublication(eventId, relayUrl, success, error, objectType, objectId) } /** * Log a publication attempt (ancienne méthode, conservée pour fallback dans writeService) * @deprecated Utiliser logPublication qui utilise writeService * @internal Utilisé uniquement par writeService en fallback */ async logPublicationDirect( eventId: string, relayUrl: string, success: boolean, error?: string, objectType?: string, objectId?: string ): Promise { try { await this.init() if (!this.db) { throw new Error('Database not initialized') } const entry: PublicationLogEntry = { id: `${eventId}_${relayUrl}_${Date.now()}`, // Unique ID eventId, relayUrl, success, ...(error !== undefined ? { error } : {}), timestamp: Date.now(), ...(objectType !== undefined ? { objectType } : {}), ...(objectId !== undefined ? { objectId } : {}), } const transaction = this.db.transaction([STORE_NAME], 'readwrite') const store = transaction.objectStore(STORE_NAME) await new Promise((resolve, reject) => { const request = store.add(entry) request.onsuccess = (): void => { resolve() } request.onerror = (): void => { reject(new Error(`Failed to log publication: ${request.error}`)) } }) } catch (logError) { console.error('[PublishLog] Error logging publication:', logError) } } /** * Get publication logs for an event */ async getLogsForEvent(eventId: string): Promise { try { await this.init() if (!this.db) { return [] } const transaction = this.db.transaction([STORE_NAME], 'readonly') const store = transaction.objectStore(STORE_NAME) const index = store.index('eventId') return new Promise((resolve, reject) => { const request = index.getAll(eventId) request.onsuccess = (): void => { resolve((request.result as PublicationLogEntry[]) ?? []) } request.onerror = (): void => { reject(request.error) } }) } catch (error) { console.error('[PublishLog] Error getting logs for event:', error) return [] } } /** * Get publication logs for a relay */ async getLogsForRelay(relayUrl: string, limit: number = 100): Promise { try { await this.init() if (!this.db) { return [] } const transaction = this.db.transaction([STORE_NAME], 'readonly') const store = transaction.objectStore(STORE_NAME) const index = store.index('relayUrl') return new Promise((resolve, reject) => { const request = index.openCursor(IDBKeyRange.only(relayUrl)) const entries: PublicationLogEntry[] = [] request.onsuccess = (event: globalThis.Event): void => { const cursor = (event.target as IDBRequest).result if (cursor) { entries.push(cursor.value as PublicationLogEntry) if (entries.length < limit) { cursor.continue() } else { resolve(entries.sort((a, b) => b.timestamp - a.timestamp)) } } else { resolve(entries.sort((a, b) => b.timestamp - a.timestamp)) } } request.onerror = (): void => { reject(request.error) } }) } catch (error) { console.error('[PublishLog] Error getting logs for relay:', error) return [] } } /** * Get all publication logs (successful and failed) */ async getAllLogs(limit: number = 1000): Promise { try { await this.init() if (!this.db) { return [] } const transaction = this.db.transaction([STORE_NAME], 'readonly') const store = transaction.objectStore(STORE_NAME) const index = store.index('timestamp') return new Promise((resolve, reject) => { const request = index.openCursor(null, 'prev') // Descending order const entries: PublicationLogEntry[] = [] request.onsuccess = (event: globalThis.Event): void => { const cursor = (event.target as IDBRequest).result if (cursor) { entries.push(cursor.value as PublicationLogEntry) if (entries.length < limit) { cursor.continue() } else { resolve(entries) } } else { resolve(entries) } } request.onerror = (): void => { reject(request.error) } }) } catch (error) { console.error('[PublishLog] Error getting all logs:', error) return [] } } /** * Get statistics for a relay */ async getRelayStats(relayUrl: string): Promise<{ total: number; success: number; failed: number }> { const logs = await this.getLogsForRelay(relayUrl, 10000) return { total: logs.length, success: logs.filter((log) => log.success).length, failed: logs.filter((log) => !log.success).length, } } } export const publishLog = new PublishLogService()