/** * streamWorker.ts — SSE Parsing in Web Worker (Sessione 11) * * Sposta il parsing degli eventi SSE dal main thread (React/UI) a un * Dedicated Worker, liberando il frame budget per l'UI su iOS. * * PROTOCOLLO postMessage: * * Main → Worker: * { type: "parse", id: string, chunk: string } * { type: "ping" } * { type: "shutdown" } * * Worker → Main: * { type: "events", id: string, events: SSEWorkerEvent[] } * { type: "pong" } * { type: "error", id: string, message: string } * * COMPATIBILITÀ: * - Safari 15+: Dedicated Worker ✅ * - SharedWorker: NON usato (Safari supporto limitato) * - Fallback: workerManager ritorna null → parsing sincrono nel main thread * * FORMATO SSEWorkerEvent (sottoinsieme di SSEEvent del main thread): * { event: string; data: string; id?: string } * * INVARIANTI: * - Nessun import esterno (Worker gira in contesto isolato senza bundler) * - Nessun accesso a DOM, localStorage, Dexie (non disponibili in Worker) * - Nessun try/catch globale silenzioso — errori inviati via postMessage * - Auto-cleanup: porta chiusa da workerManager dopo idle > 60s */ export interface SSEWorkerEvent { event: string; data: string; id?: string; } // ─── SSE Chunk Parser ───────────────────────────────────────────────────────── /** * Parsa un chunk SSE raw (può contenere più eventi) in una lista di SSEWorkerEvent. * * Formato SSE RFC 6455: * field: value\n * field: value\n * \n ← separa eventi multipli * * Campi supportati: event, data, id */ function parseSSEChunk(chunk: string): SSEWorkerEvent[] { const events: SSEWorkerEvent[] = []; // Separa per doppia newline (separatore evento SSE) const rawBlocks = chunk.split(/\n\n+/); for (const block of rawBlocks) { const lines = block.split("\n"); let event = "message"; let data = ""; let id: string | undefined; let hasData = false; for (const line of lines) { if (line.startsWith(":")) continue; // commento SSE const colonIdx = line.indexOf(":"); if (colonIdx === -1) continue; const field = line.slice(0, colonIdx).trim(); const value = line.slice(colonIdx + 1).trimStart(); switch (field) { case "event": event = value; break; case "data": data = data ? data + "\n" + value : value; hasData = true; break; case "id": id = value; break; default: /* ignora campi sconosciuti */ } } if (hasData) { events.push({ event, data, ...(id !== undefined && { id }) }); } } return events; } // ─── Worker message handler ─────────────────────────────────────────────────── // Questo codice gira nel contesto del Worker (non del main thread). // self = DedicatedWorkerGlobalScope self.addEventListener("message", (e: MessageEvent) => { const msg = e.data as { type: string; id?: string; chunk?: string }; switch (msg.type) { case "parse": { try { const events = parseSSEChunk(msg.chunk ?? ""); self.postMessage({ type: "events", id: msg.id, events }); } catch (err) { self.postMessage({ type: "error", id: msg.id, message: err instanceof Error ? err.message : String(err), }); } break; } case "ping": { self.postMessage({ type: "pong" }); break; } case "shutdown": { self.close(); break; } default: // Ignora messaggi sconosciuti } });