feat: Phase 3 — 前端交互功能完成
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// SSE (Server-Sent Events) client for POST-based streaming endpoints
|
||||
// Works with fetch() + ReadableStream since EventSource only supports GET
|
||||
const SSE = {
|
||||
/**
|
||||
* POST to a streaming endpoint and handle SSE events.
|
||||
* @param {string} url - The endpoint URL
|
||||
* @param {object} body - JSON request body
|
||||
* @param {object} handlers - Map of event name → callback(data)
|
||||
* Special handlers:
|
||||
* 'error' - called on error events or fetch failures
|
||||
* 'done' - called when stream ends
|
||||
* 'start' - called when stream starts (first event)
|
||||
* @returns {object} controller with abort() method
|
||||
*/
|
||||
async post(url, body, handlers = {}) {
|
||||
const controller = new AbortController();
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const errText = await resp.text();
|
||||
let msg = `HTTP ${resp.status}`;
|
||||
try {
|
||||
const errJson = JSON.parse(errText);
|
||||
msg = errJson.error || errJson.message || msg;
|
||||
} catch (_) {
|
||||
msg = errText || msg;
|
||||
}
|
||||
if (handlers.error) handlers.error({ message: msg });
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let currentEvent = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
currentEvent = line.slice(7).trim();
|
||||
} else if (line.startsWith('data: ')) {
|
||||
const raw = line.slice(6);
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch (_) {
|
||||
data = raw;
|
||||
}
|
||||
|
||||
// Call the matching handler
|
||||
if (currentEvent && handlers[currentEvent]) {
|
||||
handlers[currentEvent](data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream ended
|
||||
if (handlers.done) handlers.done();
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') return;
|
||||
if (handlers.error) handlers.error({ message: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
return { abort: () => controller.abort() };
|
||||
}
|
||||
};
|
||||
|
||||
window.SSE = SSE;
|
||||
Reference in New Issue
Block a user