story-research-zapwall/lib/publishLog.ts
2026-01-09 02:11:12 +01:00

200 lines
6.4 KiB
TypeScript

/**
* 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
}
interface LogPublicationParams {
eventId: string
relayUrl: string
success: boolean
error?: string
objectType?: string
objectId?: string
}
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(params: LogPublicationParams): Promise<void> {
// Utiliser writeService pour logger via Web Worker
const { writeService } = await import('./writeService')
await writeService.logPublication(params)
}
/**
* 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(params: LogPublicationParams): Promise<void> {
try {
const entry: PublicationLogEntry = {
id: `${params.eventId}_${params.relayUrl}_${Date.now()}`, // Unique ID
eventId: params.eventId,
relayUrl: params.relayUrl,
success: params.success,
...(params.error !== undefined ? { error: params.error } : {}),
timestamp: Date.now(),
...(params.objectType !== undefined ? { objectType: params.objectType } : {}),
...(params.objectId !== undefined ? { objectId: params.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<PublicationLogEntry[]> {
try {
return await this.dbHelper.getAllByIndex<PublicationLogEntry>('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<PublicationLogEntry[]> {
try {
const entries: PublicationLogEntry[] = []
const store = await this.dbHelper.getStore('readonly')
const index = store.index('relayUrl')
return new Promise<PublicationLogEntry[]>((resolve, reject) => {
const request = index.openCursor(IDBKeyRange.only(relayUrl))
request.onsuccess = (event: globalThis.Event): void => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).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<PublicationLogEntry[]> {
try {
const entries: PublicationLogEntry[] = []
const store = await this.dbHelper.getStore('readonly')
const index = store.index('timestamp')
return new Promise<PublicationLogEntry[]>((resolve, reject) => {
const request = index.openCursor(null, 'prev') // Descending order
request.onsuccess = (event: globalThis.Event): void => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).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()