267 lines
7.6 KiB
TypeScript
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()
|