(options: FetchSSEOptions)
| 27 | * Promise 在流结束时 resolve,出错时 reject。 |
| 28 | */ |
| 29 | export async function fetchSSE(options: FetchSSEOptions): Promise<void> { |
| 30 | const { url, method = 'POST', headers = {}, body, signal, onEvent } = options |
| 31 | |
| 32 | const resp = await fetchWithAuth(url, { |
| 33 | method, |
| 34 | headers: { 'Content-Type': 'application/json', ...headers }, |
| 35 | body: body != null ? JSON.stringify(body) : undefined, |
| 36 | signal, |
| 37 | }) |
| 38 | |
| 39 | if (!resp.ok) { |
| 40 | const text = await resp.text().catch(() => '') |
| 41 | throw new Error(`SSE request failed: HTTP ${resp.status} ${text}`) |
| 42 | } |
| 43 | |
| 44 | if (!resp.body) { |
| 45 | throw new Error('SSE response has no body') |
| 46 | } |
| 47 | |
| 48 | const reader = resp.body.getReader() |
| 49 | const decoder = new TextDecoder() |
| 50 | let buffer = '' |
| 51 | let currentEvent = '' |
| 52 | |
| 53 | while (true) { |
| 54 | const { done, value } = await reader.read() |
| 55 | if (done) break |
| 56 | |
| 57 | buffer += decoder.decode(value, { stream: true }) |
| 58 | const lines = buffer.split('\n') |
| 59 | buffer = lines.pop() || '' |
| 60 | |
| 61 | for (const line of lines) { |
| 62 | if (line.startsWith('event: ')) { |
| 63 | currentEvent = line.slice(7).trim() |
| 64 | } else if (line.startsWith('data: ')) { |
| 65 | onEvent({ event: currentEvent, data: line.slice(6) }) |
| 66 | currentEvent = '' |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | if (buffer.trim()) { |
| 72 | if (buffer.startsWith('data: ')) { |
| 73 | onEvent({ event: currentEvent, data: buffer.slice(6) }) |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * 便捷方法:发起 SSE 请求,按 event type 分发解析后的 JSON。 |
no test coverage detected