story-research-zapwall/lib/publishLog.ts
2026-01-07 01:51:26 +01:00

267 lines
7.6 KiB
TypeScript

/**
* 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<void> | null = null
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'))
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<void> {
// 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<void> {
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<void>((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<PublicationLogEntry[]> {
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<PublicationLogEntry[]> {
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<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 => {
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<PublicationLogEntry[]> {
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<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 => {
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()