import type { ObjectType } from '../objectCache' import { isWebWorkerAvailable } from './env' import type { CreateNotificationParams, LogPublicationParams, WriteObjectParams } from './types' import { decideCreateNotificationWorkerMessage } from './notificationDecision' import { createNotificationDirect, logPublicationDirect, updatePublishedDirect, writeObjectDirect } from './direct' import { isRecord, isWorkerErrorForOperation, isWorkerMessageEnvelope, readWorkerErrorData } from './workerMessage' import { buildCreateNotificationWorkerPayload, buildWriteObjectWorkerPayload } from './workerPayloads' export class WriteService { private writeWorker: Worker | null = null private initPromise: Promise | null = null private async init(): Promise { 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 { return new Promise((resolve) => this.createWorkerOrFallback(resolve)) } private createWorkerOrFallback(resolve: () => void): void { if (!isWebWorkerAvailable()) { console.warn('[WriteService] Web Workers not available, using direct writes') resolve() return } try { this.writeWorker = new Worker('/writeWorker.js', { type: 'classic' }) this.registerWorkerListeners(this.writeWorker, resolve) } catch (error) { console.warn('[WriteService] Failed to create worker, using direct writes:', error) resolve() } } private registerWorkerListeners(worker: Worker, resolve: () => void): void { worker.addEventListener('message', (event: MessageEvent) => { if (!isWorkerMessageEnvelope(event.data)) { console.error('[WriteService] Received invalid worker message envelope', { data: event.data }) return } if (event.data.type === 'ERROR') { console.error('[WriteService] Worker error:', event.data.data) } }) worker.addEventListener('error', (error) => { console.error('[WriteService] Worker error:', error) console.warn('[WriteService] Falling back to direct writes') this.writeWorker = null resolve() }) const readyTimeout = setTimeout(() => { console.warn('[WriteService] Worker ready timeout, using direct writes') this.writeWorker?.terminate() this.writeWorker = null resolve() }, 2000) const readyHandler = (event: MessageEvent): void => { if (isWorkerMessageEnvelope(event.data) && event.data.type === 'WORKER_READY') { clearTimeout(readyTimeout) this.writeWorker?.removeEventListener('message', readyHandler) resolve() } } worker.addEventListener('message', readyHandler) } async writeObject(params: WriteObjectParams): Promise { try { await this.init() if (this.writeWorker) { return this.postWriteObjectToWorker(params) } await writeObjectDirect(params) } catch (error) { console.error('[WriteService] Error writing object:', error) throw error } } async updatePublished(objectType: ObjectType, id: string, published: false | string[]): Promise { try { await this.init() if (this.writeWorker) { return this.postUpdatePublishedToWorker({ objectType, id, published }) } await updatePublishedDirect({ objectType, id, published }) } catch (error) { console.error('[WriteService] Error updating published status:', error) throw error } } async createNotification(params: CreateNotificationParams): Promise { try { await this.init() if (this.writeWorker) { return this.postCreateNotificationToWorker(params) } await createNotificationDirect(params) } catch (error) { console.error('[WriteService] Error creating notification:', error) throw error } } async logPublication(params: LogPublicationParams): Promise { try { await this.init() if (this.writeWorker) { this.writeWorker.postMessage({ type: 'LOG_PUBLICATION', data: { ...params } }) return } await logPublicationDirect(params) } catch (logError) { console.error('[WriteService] Error logging publication:', logError) } } terminate(): void { if (this.writeWorker) { this.writeWorker.terminate() this.writeWorker = null this.initPromise = null } } private postWriteObjectToWorker(params: WriteObjectParams): Promise { const published = params.published ?? false const payload = buildWriteObjectWorkerPayload({ params, published }) return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Write operation timeout')), 10000) const handler = (event: MessageEvent): void => { if (!isWorkerMessageEnvelope(event.data)) { return } if (event.data.type === 'WRITE_OBJECT_SUCCESS' && isRecord(event.data.data) && event.data.data.hash === params.hash) { clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) resolve() return } if (event.data.type === 'ERROR') { const errorData = readWorkerErrorData(event.data.data) if (errorData.originalType !== 'WRITE_OBJECT') { return } clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) reject(new Error(errorData.error ?? 'Write worker error')) } } this.writeWorker?.addEventListener('message', handler) this.writeWorker?.postMessage(payload) }) } private postUpdatePublishedToWorker(params: { objectType: ObjectType; id: string; published: false | string[] }): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Update published operation timeout')), 10000) const handler = (event: MessageEvent): void => { if (!isWorkerMessageEnvelope(event.data)) { return } if (event.data.type === 'UPDATE_PUBLISHED_SUCCESS') { if (!isRecord(event.data.data) || event.data.data.id !== params.id) { return } clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) resolve() return } if (event.data.type !== 'ERROR') { return } const errorData = readWorkerErrorData(event.data.data) if (!isWorkerErrorForOperation(errorData, 'UPDATE_PUBLISHED')) { return } clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) reject(new Error(errorData.error ?? 'Write worker error')) } this.writeWorker?.addEventListener('message', handler) this.writeWorker?.postMessage({ type: 'UPDATE_PUBLISHED', data: { ...params } }) }) } private postCreateNotificationToWorker(params: CreateNotificationParams): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Create notification operation timeout')), 10000) const handler = (event: MessageEvent): void => { const decision = decideCreateNotificationWorkerMessage({ eventData: event.data, expectedEventId: params.eventId }) if (decision.status === 'ignore') { return } clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) if (decision.status === 'resolve') { resolve() } else { reject(new Error(decision.message)) } } this.writeWorker?.addEventListener('message', handler) this.writeWorker?.postMessage(buildCreateNotificationWorkerPayload(params)) }) } }