70 lines
1.9 KiB
TypeScript
70 lines
1.9 KiB
TypeScript
/**
|
||
* 极简 SSE(text/event-stream)解析器。
|
||
*
|
||
* 只依赖 Web 标准的 ReadableStream,可直接消费 fetch 响应体。
|
||
* 上游既可能是标准 SSE,也可能在末尾缺少空行,因此以「块边界」为准做收尾。
|
||
*/
|
||
|
||
export interface SseEvent {
|
||
event?: string
|
||
data: string
|
||
}
|
||
|
||
export async function* parseSse(
|
||
stream: ReadableStream<Uint8Array>
|
||
): AsyncGenerator<SseEvent, void, void> {
|
||
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') }
|
||
}
|