Files
STCourses/packages/feature-ai/src/providers/sse.ts
T

45 lines
1.6 KiB
TypeScript

/** Minimal Server-Sent Events reader: yields one {event, data} per blank-line-terminated record. */
export async function* readSse(body: ReadableStream<Uint8Array>): AsyncIterable<{ event: string; data: string }> {
const decoder = new TextDecoder();
let buf = "";
let event = "message";
let data: string[] = [];
const flushLine = function* (line: string): Generator<{ event: string; data: string }> {
if (line === "") {
if (data.length) yield { event, data: data.join("\n") };
event = "message";
data = [];
return;
}
if (line.startsWith(":")) return;
const i = line.indexOf(":");
const field = i === -1 ? line : line.slice(0, i);
let value = i === -1 ? "" : line.slice(i + 1);
if (value.startsWith(" ")) value = value.slice(1);
if (field === "event") event = value;
else if (field === "data") data.push(value);
};
for await (const chunk of body as unknown as AsyncIterable<Uint8Array>) {
buf += decoder.decode(chunk, { stream: true });
let nl: number;
while ((nl = buf.search(/\r\n|\n|\r/)) !== -1) {
const line = buf.slice(0, nl);
buf = buf.slice(nl + (buf.startsWith("\r\n", nl) ? 2 : 1));
yield* flushLine(line);
}
}
buf += decoder.decode();
if (buf) yield* flushLine(buf);
yield* flushLine("");
}
export async function errorMessage(res: Response): Promise<string> {
const text = await res.text().catch(() => "");
try {
const j = JSON.parse(text);
const m = j?.error?.message ?? j?.message ?? j?.error;
if (typeof m === "string") return `${res.status}: ${m}`;
} catch {}
return `${res.status}: ${text.slice(0, 300) || res.statusText}`;
}