/** * Publication log service - stores publication attempts and results in IndexedDB */ import { createIndexedDBHelper, type IndexedDBHelper } from './helpers/indexedDBHelper' 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 readonly dbHelper: IndexedDBHelper constructor() { this.dbHelper = createIndexedDBHelper({ dbName: DB_NAME, version: DB_VERSION, storeName: STORE_NAME, keyPath: 'id', indexes: [ { name: 'eventId', keyPath: 'eventId', unique: false }, { name: 'relayUrl', keyPath: 'relayUrl', unique: false }, { name: 'timestamp', keyPath: 'timestamp', unique: false }, { name: 'success', keyPath: 'success', unique: false }, ], onUpgrade: (db: IDBDatabase): void => { // Note: autoIncrement is handled in the store creation, but IndexedDBHelper doesn't support it directly // We need to handle this in the upgrade handler 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 { 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 } : {}), } await this.dbHelper.add(entry) } catch (logError) { console.error('[PublishLog] Error logging publication:', logError) } } /** * Get publication logs for an event */ async getLogsForEvent(eventId: string): Promise { try { return await this.dbHelper.getAllByIndex('eventId', eventId) } 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 { const entries: PublicationLogEntry[] = [] const store = await this.dbHelper.getStore('readonly') const index = store.index('relayUrl') return new Promise((resolve, reject) => { const request = index.openCursor(IDBKeyRange.only(relayUrl)) 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 => { if (request.error) { reject(request.error) } else { reject(new Error('Unknown error opening cursor')) } } }) } 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 { const entries: PublicationLogEntry[] = [] const store = await this.dbHelper.getStore('readonly') const index = store.index('timestamp') return new Promise((resolve, reject) => { const request = index.openCursor(null, 'prev') // Descending order 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 => { if (request.error) { reject(request.error) } else { reject(new Error('Unknown error opening cursor')) } } }) } 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()