evOo-local/packages/core/src/response-bytes.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

54 lines
1.5 KiB
TypeScript

/** Read a response within a byte budget, releasing failed streams without waiting for cancellation.
* Callers own header validation and wrap lazy read operations in their request deadline.
*/
export async function readBoundedResponseBytes(
response: Response,
options: {
maxBytes: number;
tooLargeMessage: string;
read: <T>(operation: () => Promise<T>) => Promise<T>;
},
): Promise<Uint8Array> {
const { maxBytes, tooLargeMessage, read } = options;
if (!response.body) {
const buffer = await read(() => response.arrayBuffer());
if (buffer.byteLength > maxBytes) throw new Error(tooLargeMessage);
return new Uint8Array(buffer);
}
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
try {
for (;;) {
const { done, value } = await read(() => reader.read());
if (done) break;
if (!value?.byteLength) continue;
total += value.byteLength;
if (total > maxBytes) throw new Error(tooLargeMessage);
chunks.push(value);
}
} catch (error) {
try {
void Promise.resolve(reader.cancel()).catch(() => undefined);
} catch {
// Best-effort release, including injected streams that throw synchronously.
}
throw error;
} finally {
try {
reader.releaseLock();
} catch {
// Already cancelled or released.
}
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return bytes;
}