Fix terminal log follow and wrapping

This commit is contained in:
npc0-hue
2026-08-11 00:16:27 +08:00
parent 75a2c5aac5
commit 82ee28522c
5 changed files with 88 additions and 29 deletions
+38 -17
View File
@@ -791,11 +791,12 @@ async function responseError(response: Response): Promise<PlatformApiError> {
class FetchServerSentEventStream implements PlatformEventStream {
onerror: ((event: Event) => void) | null = null;
private lastEventId = "";
private readonly controller = new AbortController();
private readonly listeners = new Map<string, Set<(event: MessageEvent) => void>>();
constructor(private readonly url: string, private readonly sessionToken: string) {
void this.connect();
void this.connectLoop();
}
addEventListener(type: string, listener: (event: MessageEvent) => void): void {
@@ -813,17 +814,21 @@ class FetchServerSentEventStream implements PlatformEventStream {
this.listeners.clear();
}
private async connect(): Promise<void> {
try {
const response = await fetch(this.url, { method: "GET", credentials: "include", headers: { Accept: "text/event-stream", Authorization: `Bearer ${this.sessionToken}` }, signal: this.controller.signal });
if (!response.ok || !response.body) {
throw new Error(`event stream failed: ${response.status}`);
}
await this.read(response.body);
} catch {
if (!this.controller.signal.aborted) {
private async connectLoop(): Promise<void> {
while (!this.controller.signal.aborted) {
try {
const headers = new Headers({ Accept: "text/event-stream", Authorization: `Bearer ${this.sessionToken}` });
if (this.lastEventId) headers.set("Last-Event-ID", this.lastEventId);
const response = await fetch(this.url, { method: "GET", credentials: "include", headers, signal: this.controller.signal });
if (!response.ok || !response.body) {
throw new Error(`event stream failed: ${response.status}`);
}
await this.read(response.body);
} catch {
if (this.controller.signal.aborted) return;
this.onerror?.(new Event("error"));
}
await this.reconnectPause();
}
}
@@ -853,23 +858,39 @@ class FetchServerSentEventStream implements PlatformEventStream {
if (field === "id") eventId = value;
if (field === "data") dataLines.push(value);
};
while (!this.controller.signal.aborted) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
lines.forEach(processLine);
try {
while (!this.controller.signal.aborted) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
lines.forEach(processLine);
}
buffer += decoder.decode();
if (buffer) processLine(buffer);
processLine("");
} finally {
reader.releaseLock();
}
}
private dispatch(type: string, data: string, lastEventId: string): void {
if (lastEventId) this.lastEventId = lastEventId;
const event = new MessageEvent(type, { data, lastEventId });
this.listeners.get(type)?.forEach((listener) => listener(event));
if (type !== "message") {
this.listeners.get("message")?.forEach((listener) => listener(event));
}
}
private reconnectPause(): Promise<void> {
if (this.controller.signal.aborted) return Promise.resolve();
return new Promise((resolve) => {
const timeout = globalThis.setTimeout(resolve, 1000);
this.controller.signal.addEventListener("abort", () => { globalThis.clearTimeout(timeout); resolve(); }, { once: true });
});
}
}
function safeValidationMessage(apiError?: ApiErrorResponse | null): string {