fix: streaming reliability, timeout, memory leaks, and retry improvements
- Fix streaming timeout: use AbortController for connection-only timeout instead of AbortSignal.timeout() that kills active SSE streams - Fix fetchViaCurl: stream body via ReadableStream instead of buffering entire response in memory - Fix JWT/aichat race condition: add Promise dedup to prevent concurrent bootstrap calls (10 requests = 1 bootstrap, not 10) - Fix ACTIVE_READERS memory leak: auto-remove readers on stream completion - Fix WebSocket backpressure: log warning when client buffer exceeds 1MB - Add SSE heartbeat/keepalive: send ': keepalive' every 15s to prevent LB/proxy timeout during AI thinking - Fix SSELineBuffer: graceful overflow handling (warn + discard instead of throwing error that crashes stream) - Fix transformStream tight loop: yield to event loop after each chunk to prevent starvation - Fix fetchViaCurl process cleanup: use SIGKILL + proper timeout cleanup - Add retry on 502/504: retry transient server errors before returning to caller (both fetchWithRetry and fetchWithSessionRetry)
This commit is contained in:
@@ -576,12 +576,37 @@ function transformStream(
|
||||
const encoder = new TextEncoder();
|
||||
const lineBuffer = new SSELineBuffer();
|
||||
|
||||
// SSE keepalive: send a comment every 15s to prevent LB/proxy timeout
|
||||
let keepaliveTimer: ReturnType<typeof setInterval> | null = null;
|
||||
const KEEPALIVE_INTERVAL_MS = 15_000;
|
||||
|
||||
function startKeepalive(controller: ReadableStreamDefaultController) {
|
||||
if (keepaliveTimer) return;
|
||||
keepaliveTimer = setInterval(() => {
|
||||
try {
|
||||
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
||||
} catch {
|
||||
// Stream already closed
|
||||
if (keepaliveTimer) clearInterval(keepaliveTimer);
|
||||
}
|
||||
}, KEEPALIVE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopKeepalive() {
|
||||
if (keepaliveTimer) {
|
||||
clearInterval(keepaliveTimer);
|
||||
keepaliveTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
return new ReadableStream({
|
||||
async pull(controller) {
|
||||
try {
|
||||
startKeepalive(controller);
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
stopKeepalive();
|
||||
// Flush remaining text after stream ends
|
||||
const remaining = lineBuffer.flush();
|
||||
if (remaining.length > 0) {
|
||||
@@ -612,8 +637,12 @@ function transformStream(
|
||||
controller.enqueue(encoder.encode(line + "\n\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// Yield to event loop after each chunk to prevent starvation
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
} catch (err) {
|
||||
stopKeepalive();
|
||||
if (isDevMode()) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
@@ -630,6 +659,10 @@ function transformStream(
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
stopKeepalive();
|
||||
reader.cancel();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user