story-research-zapwall/lib/writeService.ts
2026-01-07 02:24:24 +01:00

306 lines
9.2 KiB
TypeScript

/**
* Write service - manages all write operations to IndexedDB
* Routes writes through Web Worker for IndexedDB operations
* Routes network operations through WebSocket service
*/
import type { NostrEvent } from 'nostr-tools'
import type { ObjectType } from './objectCache'
class WriteService {
private writeWorker: Worker | null = null
private initPromise: Promise<void> | null = null
/**
* Initialize the write worker
*/
private async init(): Promise<void> {
if (this.writeWorker) {
return
}
if (this.initPromise) {
return this.initPromise
}
this.initPromise = this.createWorker()
try {
await this.initPromise
} catch (error) {
this.initPromise = null
throw error
}
}
private createWorker(): Promise<void> {
return new Promise((resolve, _reject) => {
if (typeof window === 'undefined' || !window.Worker) {
// Fallback: write directly if Worker not available
console.warn('[WriteService] Web Workers not available, using direct writes')
resolve()
return
}
try {
// Worker dans public/ pour Next.js
this.writeWorker = new Worker('/writeWorker.js', { type: 'classic' })
this.writeWorker.addEventListener('message', (event) => {
const { type, data } = event.data
if (type === 'ERROR') {
console.error('[WriteService] Worker error:', data)
}
})
this.writeWorker.addEventListener('error', (error) => {
console.error('[WriteService] Worker error:', error)
// Ne pas rejeter, utiliser fallback
console.warn('[WriteService] Falling back to direct writes')
this.writeWorker = null
resolve()
})
// Attendre que le worker soit prêt
const readyTimeout = setTimeout(() => {
console.warn('[WriteService] Worker ready timeout, using direct writes')
if (this.writeWorker) {
this.writeWorker.terminate()
this.writeWorker = null
}
resolve()
}, 2000)
// Le worker est prêt quand il répond
const readyHandler = (event: MessageEvent): void => {
if (event.data?.type === 'WORKER_READY') {
clearTimeout(readyTimeout)
this.writeWorker?.removeEventListener('message', readyHandler)
resolve()
}
}
this.writeWorker.addEventListener('message', readyHandler)
} catch (error) {
console.warn('[WriteService] Failed to create worker, using direct writes:', error)
resolve() // Fallback to direct writes
}
})
}
/**
* Write object to IndexedDB (via Web Worker)
*/
async writeObject(
objectType: ObjectType,
hash: string,
event: NostrEvent,
parsed: unknown,
version: number,
hidden: boolean,
index?: number,
published: false | string[] = false
): Promise<void> {
try {
await this.init()
if (this.writeWorker) {
// Send to worker
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Write operation timeout'))
}, 10000)
const handler = (event: MessageEvent): void => {
const { type, data } = event.data
if (type === 'WRITE_OBJECT_SUCCESS' && data.hash === hash) {
clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler)
resolve()
} else if (type === 'ERROR' && data.originalType === 'WRITE_OBJECT') {
clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler)
reject(new Error(data.error))
}
}
this.writeWorker!.addEventListener('message', handler)
this.writeWorker!.postMessage({
type: 'WRITE_OBJECT',
data: {
objectType,
hash,
event,
parsed,
version,
hidden,
index,
published,
},
})
})
}
// Fallback: direct write
const { objectCache } = await import('./objectCache')
await objectCache.set(objectType, hash, event, parsed, version, hidden, index, published)
} catch (error) {
console.error('[WriteService] Error writing object:', error)
throw error
}
}
/**
* Update published status (via Web Worker)
*/
async updatePublished(
objectType: ObjectType,
id: string,
published: false | string[]
): Promise<void> {
try {
await this.init()
if (this.writeWorker) {
// Send to worker
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Update published operation timeout'))
}, 10000)
const handler = (event: MessageEvent): void => {
const { type, data } = event.data
if (type === 'UPDATE_PUBLISHED_SUCCESS' && data.id === id) {
clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler)
resolve()
} else if (type === 'ERROR' && (data.originalType === 'UPDATE_PUBLISHED' || data.taskId?.startsWith('UPDATE_PUBLISHED'))) {
clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler)
reject(new Error(data.error))
}
}
this.writeWorker!.addEventListener('message', handler)
this.writeWorker!.postMessage({
type: 'UPDATE_PUBLISHED',
data: { objectType, id, published },
})
})
}
// Fallback: direct write
const { objectCache } = await import('./objectCache')
await objectCache.updatePublished(objectType, id, published)
} catch (error) {
console.error('[WriteService] Error updating published status:', error)
throw error
}
}
/**
* Create notification (via Web Worker)
*/
async createNotification(
type: string,
objectType: string,
objectId: string,
eventId: string,
data?: Record<string, unknown>
): Promise<void> {
try {
await this.init()
if (this.writeWorker) {
// Send to worker
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Create notification operation timeout'))
}, 10000)
const handler = (event: MessageEvent): void => {
const { type: responseType, data: responseData } = event.data
if (responseType === 'CREATE_NOTIFICATION_SUCCESS' && responseData.eventId === eventId) {
clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler)
resolve()
} else if (responseType === 'ERROR' && (responseData.originalType === 'CREATE_NOTIFICATION' || responseData.taskId?.startsWith('CREATE_NOTIFICATION'))) {
clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler)
reject(new Error(responseData.error))
}
}
this.writeWorker!.addEventListener('message', handler)
this.writeWorker!.postMessage({
type: 'CREATE_NOTIFICATION',
data: { type, objectType, objectId, eventId, notificationData: data },
})
})
}
// Fallback: direct write
const { notificationService } = await import('./notificationService')
const notificationParams: Parameters<typeof notificationService.createNotification>[0] = {
type: type as Parameters<typeof notificationService.createNotification>[0]['type'],
objectType,
objectId,
eventId,
}
if (data !== undefined) {
notificationParams.data = data
}
await notificationService.createNotification(notificationParams)
} catch (error) {
console.error('[WriteService] Error creating notification:', error)
throw error
}
}
/**
* Log publication (via Web Worker)
*/
async logPublication(
eventId: string,
relayUrl: string,
success: boolean,
error?: string,
objectType?: string,
objectId?: string
): Promise<void> {
try {
await this.init()
if (this.writeWorker) {
// Send to worker
this.writeWorker.postMessage({
type: 'LOG_PUBLICATION',
data: { eventId, relayUrl, success, error, objectType, objectId },
})
// Don't wait for response for logs (fire and forget)
} else {
// Fallback: direct write
const { publishLog } = await import('./publishLog')
await publishLog.logPublicationDirect(eventId, relayUrl, success, error, objectType, objectId)
}
} catch (error) {
console.error('[WriteService] Error logging publication:', error)
// Don't throw for logs
}
}
/**
* Terminate the worker
*/
terminate(): void {
if (this.writeWorker) {
this.writeWorker.terminate()
this.writeWorker = null
this.initPromise = null
}
}
}
export const writeService = new WriteService()