| |
| |
| |
| |
|
|
| import type { NewsItem, ClusteredEvent, MarketData } from '@/types'; |
| import type { PredictionMarket } from '@/services/prediction'; |
| import type { CorrelationSignal } from './correlation'; |
| import { SOURCE_TIERS, SOURCE_TYPES, type SourceType } from '@/config/feeds'; |
|
|
| |
| import AnalysisWorker from '@/workers/analysis.worker?worker'; |
|
|
| interface PendingRequest<T> { |
| resolve: (value: T) => void; |
| reject: (error: Error) => void; |
| timeout: ReturnType<typeof setTimeout>; |
| } |
|
|
| interface ClusterResult { |
| type: 'cluster-result'; |
| id: string; |
| clusters: ClusteredEvent[]; |
| } |
|
|
| interface CorrelationResult { |
| type: 'correlation-result'; |
| id: string; |
| signals: CorrelationSignal[]; |
| } |
|
|
| type WorkerResult = ClusterResult | CorrelationResult | { type: 'ready' }; |
|
|
| class AnalysisWorkerManager { |
| private worker: Worker | null = null; |
| private pendingRequests: Map<string, PendingRequest<unknown>> = new Map(); |
| private requestIdCounter = 0; |
| private isReady = false; |
| |
| |
| |
| private workerUnavailable = false; |
| private readyPromise: Promise<void> | null = null; |
| private readyResolve: (() => void) | null = null; |
| private readyReject: ((error: Error) => void) | null = null; |
| private readyTimeout: ReturnType<typeof setTimeout> | null = null; |
|
|
| private static readonly READY_TIMEOUT_MS = 10000; |
|
|
| |
| |
| |
| private initWorker(): void { |
| if (this.worker || this.workerUnavailable) return; |
|
|
| this.readyPromise = new Promise((resolve, reject) => { |
| this.readyResolve = resolve; |
| this.readyReject = reject; |
| }); |
|
|
| |
| this.readyTimeout = setTimeout(() => { |
| if (!this.isReady) { |
| const error = new Error('Worker failed to become ready within timeout'); |
| console.error('[AnalysisWorker]', error.message); |
| this.readyReject?.(error); |
| this.cleanup(); |
| } |
| }, AnalysisWorkerManager.READY_TIMEOUT_MS); |
|
|
| try { |
| this.worker = new AnalysisWorker(); |
| } catch (error) { |
| |
| |
| |
| |
| |
| |
| |
| |
| console.error('[AnalysisWorker] Failed to create worker:', error); |
| this.workerUnavailable = true; |
| this.readyResolve?.(); |
| this.cleanup(); |
| return; |
| } |
|
|
| this.worker.onmessage = (event: MessageEvent<WorkerResult>) => { |
| const data = event.data; |
|
|
| if (data.type === 'ready') { |
| this.isReady = true; |
| if (this.readyTimeout) { |
| clearTimeout(this.readyTimeout); |
| this.readyTimeout = null; |
| } |
| this.readyResolve?.(); |
| return; |
| } |
|
|
| if ('id' in data) { |
| const pending = this.pendingRequests.get(data.id); |
| if (pending) { |
| clearTimeout(pending.timeout); |
| this.pendingRequests.delete(data.id); |
|
|
| if (data.type === 'cluster-result') { |
| |
| const clusters = data.clusters.map(cluster => ({ |
| ...cluster, |
| firstSeen: new Date(cluster.firstSeen), |
| lastUpdated: new Date(cluster.lastUpdated), |
| allItems: cluster.allItems.map(item => ({ |
| ...item, |
| pubDate: new Date(item.pubDate), |
| })), |
| })); |
| pending.resolve(clusters); |
| } else if (data.type === 'correlation-result') { |
| |
| const signals = data.signals.map(signal => ({ |
| ...signal, |
| timestamp: new Date(signal.timestamp), |
| })); |
| pending.resolve(signals); |
| } |
| } |
| } |
| }; |
|
|
| this.worker.onerror = (error) => { |
| console.error('[AnalysisWorker] Error:', error); |
|
|
| |
| if (!this.isReady) { |
| this.readyReject?.(new Error(`Worker failed to initialize: ${error.message}`)); |
| this.cleanup(); |
| return; |
| } |
|
|
| |
| for (const [id, pending] of this.pendingRequests) { |
| clearTimeout(pending.timeout); |
| pending.reject(new Error(`Worker error: ${error.message}`)); |
| this.pendingRequests.delete(id); |
| } |
| }; |
| } |
|
|
| |
| |
| |
| private cleanup(): void { |
| if (this.readyTimeout) { |
| clearTimeout(this.readyTimeout); |
| this.readyTimeout = null; |
| } |
| if (this.worker) { |
| this.worker.terminate(); |
| this.worker = null; |
| } |
| this.isReady = false; |
| this.readyPromise = null; |
| this.readyResolve = null; |
| this.readyReject = null; |
| } |
|
|
| |
| |
| |
| private async waitForReady(): Promise<void> { |
| this.initWorker(); |
| if (this.isReady) return; |
| await this.readyPromise; |
| } |
|
|
| |
| |
| |
| private generateId(): string { |
| return `req-${++this.requestIdCounter}-${Date.now()}`; |
| } |
|
|
| private request<T>( |
| type: 'cluster' | 'correlation', |
| payload: Record<string, unknown>, |
| timeoutMs: number, |
| timeoutMessage: string |
| ): Promise<T> { |
| return new Promise((resolve, reject) => { |
| const id = this.generateId(); |
| const timeout = setTimeout(() => { |
| this.pendingRequests.delete(id); |
| reject(new Error(timeoutMessage)); |
| }, timeoutMs); |
|
|
| this.pendingRequests.set(id, { |
| resolve: resolve as (value: unknown) => void, |
| reject, |
| timeout, |
| }); |
|
|
| this.worker!.postMessage({ |
| type, |
| id, |
| ...payload, |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| |
| async clusterNews(items: NewsItem[]): Promise<ClusteredEvent[]> { |
| await this.waitForReady(); |
| |
| |
| |
| if (!this.isReady) return []; |
| return this.request<ClusteredEvent[]>( |
| 'cluster', |
| { items, sourceTiers: SOURCE_TIERS }, |
| 30000, |
| 'Clustering request timed out' |
| ); |
| } |
|
|
| |
| |
| |
| |
| async analyzeCorrelations( |
| clusters: ClusteredEvent[], |
| predictions: PredictionMarket[], |
| markets: MarketData[] |
| ): Promise<CorrelationSignal[]> { |
| await this.waitForReady(); |
| if (!this.isReady) return []; |
| return this.request<CorrelationSignal[]>( |
| 'correlation', |
| { |
| clusters, |
| predictions, |
| markets, |
| sourceTypes: SOURCE_TYPES as Record<string, SourceType>, |
| }, |
| 10000, |
| 'Correlation analysis request timed out' |
| ); |
| } |
|
|
| |
| |
| |
| reset(): void { |
| |
| for (const pending of this.pendingRequests.values()) { |
| clearTimeout(pending.timeout); |
| pending.reject(new Error('Worker reset')); |
| } |
| this.pendingRequests.clear(); |
|
|
| if (this.worker) { |
| this.worker.postMessage({ type: 'reset' }); |
| } |
| } |
|
|
| |
| |
| |
| terminate(): void { |
| |
| for (const [id, pending] of this.pendingRequests) { |
| clearTimeout(pending.timeout); |
| pending.reject(new Error('Worker terminated')); |
| this.pendingRequests.delete(id); |
| } |
| this.cleanup(); |
| } |
|
|
| |
| |
| |
| get ready(): boolean { |
| return this.isReady; |
| } |
| } |
|
|
| |
| export const analysisWorker = new AnalysisWorkerManager(); |
|
|
| |
| export type { CorrelationSignal }; |
|
|