refactor(sse): declare the ArrayBuffer backing on media byte producers (#8665)

Six declarations spell a byte buffer as bare `Buffer` / `Uint8Array`. Without
its type argument that widens to `ArrayBufferLike`, which also admits
`SharedArrayBuffer` — so the value is rejected at every Web API boundary it is
actually passed to: `BodyInit` for `new Response(...)` and `BlobPart` for
`new Blob([...])`.

Every one of them is already ArrayBuffer-backed at runtime. `hexToBytes()`
allocates with `new Uint8Array(len)`; `synthesizeGtts()` and `pcmToWav()` return
`Buffer.concat(...)`; `fetchRemoteImage()` returns
`Buffer.from(await response.arrayBuffer())`; `readPageResponseBody()` returns
`Buffer.from(body)`, which copies. The declarations were simply less specific
than the values, so this states what the code already guarantees.

Same fix #8533 applied to the multipart and gRPC-web bodies.

Fixes 5 of the 208 `tsc -p open-sse/tsconfig.json` diagnostics with no new ones:
3 in audioSpeech.ts (MiniMax hex, gTTS, Vertex Gemini TTS), 1 in
imageGeneration.ts (Topaz Blob upload) and 1 in browserBackedChat.ts.

Refs #8484
This commit is contained in:
backryun
2026-07-28 05:24:23 +09:00
committed by GitHub
parent 809e6d1880
commit 116d583972
6 changed files with 92 additions and 7 deletions

View File

@@ -198,7 +198,7 @@ async function synthesizeGttsChunk(
export async function synthesizeGtts(
input: GttsSynthInput,
fetchImpl: FetchLike = fetch
): Promise<Buffer> {
): Promise<Buffer<ArrayBuffer>> {
const lang = normalizeGttsLang(input.lang);
const tld = (typeof input.tld === "string" && input.tld.trim()) || DEFAULT_TLD;
const chunks = chunkGttsText(input.text);

View File

@@ -131,7 +131,12 @@ async function vertexError(res: Response): Promise<VertexHttpError> {
}
/** Wrap raw little-endian 16-bit PCM mono samples in a minimal WAV container. */
export function pcmToWav(pcm: Buffer, sampleRate = 24000, channels = 1, bitsPerSample = 16): Buffer {
export function pcmToWav(
pcm: Buffer,
sampleRate = 24000,
channels = 1,
bitsPerSample = 16
): Buffer<ArrayBuffer> {
const blockAlign = (channels * bitsPerSample) / 8;
const byteRate = sampleRate * blockAlign;
const header = Buffer.alloc(44);
@@ -190,7 +195,7 @@ function extractText(data: unknown): string {
export async function vertexGenerateSpeech(
credentials: VertexMediaCredentials,
options: { model: string; input: string; voice?: string }
): Promise<{ audio: Buffer; contentType: string }> {
): Promise<{ audio: Buffer<ArrayBuffer>; contentType: string }> {
const auth = await resolveVertexAuth(credentials);
const { url, headers } = buildModelRequest(auth, options.model, "generateContent");
const payload = {

View File

@@ -618,7 +618,7 @@ async function handleXiaomiMimoSpeech(providerConfig, body, modelId, token, cred
* `base_resp.status_code` (0 = success).
* Port of decolua/9router#1043 by toanalien <toanalien@gmail.com>.
*/
function hexToBytes(audioHex) {
function hexToBytes(audioHex): Uint8Array<ArrayBuffer> {
const clean = typeof audioHex === "string" ? audioHex.trim() : "";
if (!clean) throw new Error("MiniMax TTS returned no audio");
if (clean.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(clean)) {
@@ -684,7 +684,7 @@ async function handleMinimaxSpeech(providerConfig, body, modelId, token) {
}
const audioField = (data.data as Record<string, unknown> | undefined)?.audio;
let bytes: Uint8Array;
let bytes: Uint8Array<ArrayBuffer>;
try {
bytes = hexToBytes(audioField);
} catch (err) {

View File

@@ -502,7 +502,7 @@ export function __resetBrowserPoolMetricsForTest(): void {
export async function readPageResponseBody(
response: import("playwright").Response
): Promise<{ status: number; headers: Record<string, string>; body: Buffer }> {
): Promise<{ status: number; headers: Record<string, string>; body: Buffer<ArrayBuffer> }> {
const headers: Record<string, string> = {};
for (const [name, value] of Object.entries(response.headers())) {
headers[name] = value;

View File

@@ -39,7 +39,7 @@ export interface RemoteImageFetchOptions {
}
export interface RemoteImageFetchResult {
buffer: Buffer;
buffer: Buffer<ArrayBuffer>;
contentType: string;
url: string;
}

View File

@@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import test from "node:test";
import { pcmToWav } from "../../open-sse/executors/vertexMedia.ts";
import { synthesizeGtts } from "../../open-sse/executors/gtts.ts";
import { readPageResponseBody } from "../../open-sse/services/browserPool.ts";
import { fetchRemoteImage } from "@/shared/network/remoteImageFetch";
/**
* Each producer below is handed straight to a Web API that only accepts an
* `ArrayBuffer`-backed view — `new Response(body)` (BodyInit) or
* `new Blob([part])` (BlobPart). A `Buffer` / `Uint8Array` written without its
* type argument widens to `ArrayBufferLike`, which also admits
* `SharedArrayBuffer` and is therefore rejected at that boundary.
*
* The existing tests for these functions assert their *contents* — the RIFF
* header, the concatenated chunks, the decoded base64. None assert the backing,
* which is the single property the narrowed return types exist to guarantee and
* the one a plausible refactor to a shared or pooled allocation would break.
*
* So each case asserts both: the backing itself, and that the value really is
* accepted by the Web API it is passed to in production.
*/
test("pcmToWav returns an ArrayBuffer-backed WAV that new Response accepts", () => {
const wav = pcmToWav(Buffer.from([0x01, 0x02, 0x03, 0x04]), 24000);
assert.ok(wav.buffer instanceof ArrayBuffer, "WAV buffer must not be SharedArrayBuffer-backed");
// open-sse/handlers/audioSpeech.ts hands this to `new Response(audio, …)`.
assert.doesNotThrow(() => new Response(wav));
});
test("synthesizeGtts returns an ArrayBuffer-backed buffer that new Response accepts", async () => {
// Same batchexecute envelope shape gtts-provider.test.ts builds; "aGk=" is
// base64 for "hi".
const inner = JSON.stringify(["aGk=", null, null, null, null, null, []]);
const outer = JSON.stringify([["wrb.fr", "jQ1olc", inner, null, null, null, "generic"]]);
const fetchImpl = async () =>
new Response(`)]}'\n\n${outer.length}\n${outer}\n`, { status: 200 });
const audio = await synthesizeGtts({ text: "hi", lang: "en" }, fetchImpl);
assert.ok(audio.buffer instanceof ArrayBuffer, "gTTS audio must not be SharedArrayBuffer-backed");
assert.doesNotThrow(() => new Response(audio));
});
test("fetchRemoteImage returns an ArrayBuffer-backed buffer that new Blob accepts", async () => {
const result = await fetchRemoteImage("https://cdn.example.com/image.png", {
fetchImpl: async () =>
new Response(new Uint8Array([1, 2, 3]), {
status: 200,
headers: { "content-type": "image/png" },
}),
guard: "public-only",
lookup: async () => [{ address: "203.0.113.5", family: 4 }],
});
assert.ok(result.buffer.buffer instanceof ArrayBuffer, "image bytes must be ArrayBuffer-backed");
// open-sse/handlers/imageGeneration.ts wraps this in `new Blob([…])` for the
// Topaz multipart upload.
assert.doesNotThrow(() => new Blob([result.buffer], { type: "image/png" }));
});
test("readPageResponseBody returns an ArrayBuffer-backed body", async () => {
const fakeResponse = {
status: () => 200,
headers: () => ({ "content-type": "application/json" }),
body: async () => Buffer.from('{"ok":true}'),
};
const captured = await readPageResponseBody(
fakeResponse as unknown as Parameters<typeof readPageResponseBody>[0]
);
assert.equal(captured.status, 200);
assert.equal(captured.body.toString(), '{"ok":true}');
// browserBackedChat.ts assigns this into a `Buffer.alloc(0)`-seeded local
// that it later returns as the response body.
assert.ok(captured.body.buffer instanceof ArrayBuffer);
});