Spaces:
Sleeping
Sleeping
| /** | |
| * 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 | |
| } | |
| }); | |