136 lines
3.9 KiB
JavaScript
136 lines
3.9 KiB
JavaScript
export const AGENT_CONTEXT_DIAGNOSTICS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
export const AGENT_CONTEXT_DIAGNOSTICS_REQUEST_TIMEOUT_MS = 30_000;
|
|
|
|
export class AgentContextDiagnosticsFetchError extends Error {
|
|
constructor(code, message) {
|
|
super(message);
|
|
this.name = "AgentContextDiagnosticsFetchError";
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
function timeoutError() {
|
|
return new AgentContextDiagnosticsFetchError(
|
|
"agent_context_diagnostics_request_timed_out",
|
|
"Agent context diagnostics request timed out.",
|
|
);
|
|
}
|
|
|
|
function responseTooLargeError() {
|
|
return new AgentContextDiagnosticsFetchError(
|
|
"agent_context_diagnostics_response_too_large",
|
|
"Agent context diagnostics response exceeded the 1 MiB limit.",
|
|
);
|
|
}
|
|
|
|
function cancelUnlockedBody(response) {
|
|
if (!response.body || response.body.locked) return;
|
|
void response.body.cancel().catch(() => undefined);
|
|
}
|
|
|
|
async function readBoundedResponseText(response, signal) {
|
|
const declaredLength = response.headers.get("content-length")?.trim() ?? "";
|
|
if (/^\d+$/.test(declaredLength)) {
|
|
const declaredBytes = Number(declaredLength);
|
|
if (
|
|
!Number.isSafeInteger(declaredBytes)
|
|
|| declaredBytes > AGENT_CONTEXT_DIAGNOSTICS_RESPONSE_MAX_BYTES
|
|
) {
|
|
cancelUnlockedBody(response);
|
|
throw responseTooLargeError();
|
|
}
|
|
}
|
|
|
|
if (!response.body) return "";
|
|
|
|
const reader = response.body.getReader();
|
|
const chunks = [];
|
|
let bytesRead = 0;
|
|
const cancelOnAbort = () => {
|
|
void reader.cancel().catch(() => undefined);
|
|
};
|
|
signal.addEventListener("abort", cancelOnAbort, { once: true });
|
|
|
|
try {
|
|
while (true) {
|
|
if (signal.aborted) throw timeoutError();
|
|
const chunk = await reader.read();
|
|
if (chunk.done) break;
|
|
bytesRead += chunk.value.byteLength;
|
|
if (bytesRead > AGENT_CONTEXT_DIAGNOSTICS_RESPONSE_MAX_BYTES) {
|
|
void reader.cancel().catch(() => undefined);
|
|
throw responseTooLargeError();
|
|
}
|
|
chunks.push(chunk.value);
|
|
}
|
|
} catch (error) {
|
|
if (signal.aborted) throw timeoutError();
|
|
throw error;
|
|
} finally {
|
|
signal.removeEventListener("abort", cancelOnAbort);
|
|
reader.releaseLock();
|
|
}
|
|
|
|
const bytes = new Uint8Array(bytesRead);
|
|
let offset = 0;
|
|
for (const chunk of chunks) {
|
|
bytes.set(chunk, offset);
|
|
offset += chunk.byteLength;
|
|
}
|
|
return new TextDecoder().decode(bytes);
|
|
}
|
|
|
|
function boundedDeadlineAt(requestedDeadlineAtMs) {
|
|
const now = Date.now();
|
|
const latestAllowedDeadline = now + AGENT_CONTEXT_DIAGNOSTICS_REQUEST_TIMEOUT_MS;
|
|
const requested = Number(requestedDeadlineAtMs);
|
|
if (!Number.isFinite(requested)) return now;
|
|
return Math.min(requested, latestAllowedDeadline);
|
|
}
|
|
|
|
/**
|
|
* Electron's IPC fetch must consume the body in the main process. Keep that
|
|
* buffering under the renderer's absolute diagnostics deadline and fixed cap.
|
|
*/
|
|
export async function fetchAgentContextDiagnosticsResponse(
|
|
fetchImpl,
|
|
url,
|
|
init,
|
|
requestedDeadlineAtMs,
|
|
) {
|
|
const deadlineAtMs = boundedDeadlineAt(requestedDeadlineAtMs);
|
|
const remainingMs = deadlineAtMs - Date.now();
|
|
if (remainingMs <= 0) throw timeoutError();
|
|
|
|
const controller = new AbortController();
|
|
let timeoutId = null;
|
|
const deadline = new Promise((_, reject) => {
|
|
timeoutId = setTimeout(() => {
|
|
controller.abort();
|
|
reject(timeoutError());
|
|
}, remainingMs);
|
|
});
|
|
|
|
try {
|
|
const response = await Promise.race([
|
|
fetchImpl(url, { ...init, redirect: "error", signal: controller.signal }),
|
|
deadline,
|
|
]);
|
|
const body = await Promise.race([
|
|
readBoundedResponseText(response, controller.signal),
|
|
deadline,
|
|
]);
|
|
return {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: Array.from(response.headers.entries()),
|
|
body,
|
|
};
|
|
} catch (error) {
|
|
if (controller.signal.aborted) throw timeoutError();
|
|
throw error;
|
|
} finally {
|
|
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
}
|
|
}
|