wasm: openUrl takes Headers, aborts sibling requests on failure, checks parallel
- opts.headers is read as fetch reads it (a Headers, [name, value] pairs or a plain object); it was spread as an object, which silently dropped a Headers instance (a common way to pass Authorization). A caller's Range is not sent. - When one range request of a batch fails, the others in flight are aborted (one AbortController per batch, its signal passed to fetch) and no new ones start; the first failure is the error. The workers used to go on issuing requests nobody waited for. - parallel must be an integer from 1 to 1024 (openUrl) or a positive integer (fetchRanges): a non-number gave NaN workers, so none ran and fetchRanges returned nothing. Tests (test.mjs): headers as object, Headers and pairs reach the fetch; bad parallel values are option errors; fetchRanges with a 500 on the third of 20 ranges at parallel 3 starts 3 requests and aborts the 2 in flight. Before: the Headers case sent no header, 17 requests started after the failure with none aborted, and parallel "x" returned. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -203,17 +203,61 @@ async function remoteTests() {
|
||||
eq(counted.stats().requests, server.requests, "requests counted");
|
||||
eq(counted.stats().bytesFetched, server.bytes, "bytes counted");
|
||||
|
||||
// A custom fetch is used for every request, with the caller's headers.
|
||||
let calls = 0;
|
||||
const seen = new Set();
|
||||
const viaCustom = await pkg.openUrl(`${base}/fix/fixture.h5`, {
|
||||
blockSize: 4096,
|
||||
headers: { "X-Test": "1" },
|
||||
fetch: (url, init) => { calls++; seen.add(init.headers["X-Test"]); return fetch(url, init); },
|
||||
});
|
||||
eq((await viaCustom.read("/sensors/temp")).data[0], 21.5, "custom fetch values");
|
||||
eq(calls, viaCustom.stats().requests, "custom fetch calls");
|
||||
eq([...seen], ["1"], "headers passed");
|
||||
// A custom fetch is used for every request, with the caller's headers,
|
||||
// given in any form fetch takes (a caller's Range is not sent).
|
||||
for (const headers of [{ "X-Test": "1" }, new Headers({ "X-Test": "1", Range: "bytes=0-0" }), [["X-Test", "1"]]]) {
|
||||
let calls = 0;
|
||||
const seen = new Set();
|
||||
const viaCustom = await pkg.openUrl(`${base}/fix/fixture.h5`, {
|
||||
blockSize: 4096,
|
||||
headers,
|
||||
fetch: (url, init) => {
|
||||
calls++;
|
||||
const h = new Headers(init.headers);
|
||||
seen.add(`${h.get("X-Test")} ${h.get("Range").startsWith("bytes=0-0") ? "caller's range" : "ours"}`);
|
||||
return fetch(url, init);
|
||||
},
|
||||
});
|
||||
const kind = headers.constructor.name;
|
||||
eq((await viaCustom.read("/sensors/temp")).data[0], 21.5, `custom fetch values (${kind})`);
|
||||
eq(calls, viaCustom.stats().requests, `custom fetch calls (${kind})`);
|
||||
eq([...seen], ["1 ours"], `headers passed (${kind})`);
|
||||
}
|
||||
|
||||
// parallel must be a positive integer.
|
||||
for (const parallel of [0, -1, 1.5, "4", NaN, 5000]) {
|
||||
await fails(() => pkg.openUrl(`${base}/fix/fixture.h5`, { parallel }), /parallel/, `parallel ${String(parallel)}`);
|
||||
}
|
||||
|
||||
// When one range request fails, the others in flight are aborted and no
|
||||
// more start (fetchRanges, called directly: remote.js is in the package).
|
||||
{
|
||||
const snippets = join(pkgDir, "snippets");
|
||||
const dir = readdirSync(snippets).find((d) => existsSync(join(snippets, d, "js", "remote.js")));
|
||||
const remote = await import(pathToFileURL(join(snippets, dir, "js", "remote.js")));
|
||||
let started = 0;
|
||||
const aborted = [];
|
||||
const slowFetch = (url, init) => {
|
||||
const k = started++;
|
||||
if (k === 2) return Promise.resolve(new Response(null, { status: 500 }));
|
||||
return new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => resolve(new Response(new Uint8Array(10), { status: 206 })), 200);
|
||||
init.signal?.addEventListener("abort", () => {
|
||||
clearTimeout(t);
|
||||
aborted.push(k);
|
||||
reject(new DOMException("aborted", "AbortError"));
|
||||
});
|
||||
});
|
||||
};
|
||||
const ranges = Array.from({ length: 20 }, (_, i) => [i * 10, i * 10 + 10]).flat();
|
||||
await fails(() => remote.fetchRanges("http://x.invalid/f.h5", ranges, { fetch: slowFetch, parallel: 3 }, null, 200),
|
||||
/HTTP 500/, "a failed range request is the error");
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
eq(started, 3, "no request starts after a failure");
|
||||
eq(aborted.sort(), [0, 1], "requests in flight are aborted");
|
||||
await fails(() => remote.fetchRanges("http://x.invalid/f.h5", ranges, { fetch: slowFetch, parallel: "x" }, null, 200),
|
||||
/parallel must be a positive integer/, "fetchRanges checks parallel");
|
||||
}
|
||||
|
||||
// Calls in flight at once share the cache (a 1 KiB budget: nothing is
|
||||
// evicted while any of them runs) and each gets its own answer.
|
||||
|
||||
Reference in New Issue
Block a user