chore: merge with web UI project

This commit is contained in:
Li Xin
2025-04-17 12:02:23 +08:00
parent 3aebb67e2b
commit fd7a803753
58 changed files with 10290 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
export interface StreamEvent {
type: string;
data: object;
}

View File

@@ -0,0 +1,70 @@
import { type StreamEvent } from "./StreamEvent";
export async function* fetchStream<T extends StreamEvent>(
url: string,
init: RequestInit,
): AsyncIterable<T> {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
},
...init,
});
if (response.status !== 200) {
throw new Error(`Failed to fetch from ${url}: ${response.status}`);
}
// Read from response body, event by event. An event always ends with a '\n\n'.
const reader = response.body
?.pipeThrough(new TextDecoderStream())
.getReader();
if (!reader) {
throw new Error("Response body is not readable");
}
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += value;
while (true) {
const index = buffer.indexOf("\n\n");
if (index === -1) {
break;
}
const chunk = buffer.slice(0, index);
buffer = buffer.slice(index + 2);
const event = parseEvent<T>(chunk);
if (event) {
yield event;
}
}
}
}
function parseEvent<T extends StreamEvent>(chunk: string) {
let resultType = "message";
let resultData: object | null = null;
for (const line of chunk.split("\n")) {
const pos = line.indexOf(": ");
if (pos === -1) {
continue;
}
const key = line.slice(0, pos);
const value = line.slice(pos + 2);
if (key === "event") {
resultType = value;
} else if (key === "data") {
resultData = JSON.parse(value);
}
}
if (resultType === "message" && resultData === null) {
return undefined;
}
return {
type: resultType,
data: resultData,
} as T;
}

View File

@@ -0,0 +1,2 @@
export * from "./fetch-stream";
export * from "./StreamEvent";