Files

92 lines
3.5 KiB
JavaScript
Raw Permalink Normal View History

2026-06-18 23:37:19 +08:00
// 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' },
credentials: 'same-origin',
2026-06-18 23:37:19 +08:00
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 = '';
let doneReceived = false;
2026-06-18 23:37:19 +08:00
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;
}
// Track explicit done event from backend
if (currentEvent === 'done') doneReceived = true;
2026-06-18 23:37:19 +08:00
// Call the matching handler
if (currentEvent && handlers[currentEvent]) {
handlers[currentEvent](data);
}
}
}
}
// Stream ended — only fire done if backend didn't send an explicit one
if (handlers.done && !doneReceived) handlers.done();
2026-06-18 23:37:19 +08:00
} catch (err) {
if (err.name === 'AbortError') return;
if (handlers.error) handlers.error({ message: err.message });
}
};
run();
return { abort: () => controller.abort() };
}
};
window.SSE = SSE;