/** * 极简 SSE(text/event-stream)解析器。 * * 只依赖 Web 标准的 ReadableStream,可直接消费 fetch 响应体。 * 上游既可能是标准 SSE,也可能在末尾缺少空行,因此以「块边界」为准做收尾。 */ export interface SseEvent { event?: string data: string } export async function* parseSse( stream: ReadableStream ): AsyncGenerator { const reader = stream.getReader() const decoder = new TextDecoder() let buffer = '' try { for (;;) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) let index: number // 事件之间以空行分隔,兼容 \r\n 与 \n while ((index = findBoundary(buffer)) !== -1) { const raw = buffer.slice(0, index) buffer = buffer.slice(index).replace(/^(\r?\n){2}/, '') const parsed = parseBlock(raw) if (parsed) yield parsed } } // 收尾:流结束时可能残留最后一个事件 buffer += decoder.decode() const tail = parseBlock(buffer) if (tail) yield tail } finally { reader.releaseLock() } } function findBoundary(buffer: string): number { const a = buffer.indexOf('\n\n') const b = buffer.indexOf('\r\n\r\n') if (a === -1) return b if (b === -1) return a return Math.min(a, b) } function parseBlock(block: string): SseEvent | null { const lines = block.split(/\r?\n/) const dataLines: string[] = [] let event: string | undefined for (const line of lines) { if (!line || line.startsWith(':')) continue if (line.startsWith('event:')) { event = line.slice(6).trim() } else if (line.startsWith('data:')) { dataLines.push(line.slice(5).replace(/^ /, '')) } } if (dataLines.length === 0) return null return { event, data: dataLines.join('\n') } }