@sproutboat/runtime 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/WEB_STREAMS.md +2 -2
- package/package.json +1 -1
- package/src/ingress-utf8.test.js +3 -2
- package/src/native-fetch-prelude.js +15 -8
- package/test/native-bodies.ts +7 -0
- package/test/native-request.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4274bef: `Response.body` and `Request.body` are `ReadableStream`s for every body type, as in the Fetch spec and workerd. Before, they held the value given (a string, bytes or a `Blob`). `res.body.getReader()`, `res.body.pipeThrough(...)` and `new Response(other.body)` now work on any response, including the incoming request. The value as given stays internal, so responses are still written with a fixed length unless the body really is a stream. A body whose stream has been locked can no longer be read with `text()` and the other readers, which throw a `TypeError`.
|
|
8
|
+
|
|
3
9
|
## 0.18.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/WEB_STREAMS.md
CHANGED
|
@@ -20,8 +20,8 @@ runtime links no zlib. Compression writes one fixed-Huffman block per chunk as i
|
|
|
20
20
|
(close to zlib on text: 252 bytes against 221 for 22.5 KB); decompression inflates all
|
|
21
21
|
block types once the input ends and checks the CRC-32 or Adler-32. Response and Request
|
|
22
22
|
bodies that are ReadableStreams are read by text(), json(), arrayBuffer() and blob(), and
|
|
23
|
-
their chunks must be bytes, as in workerd.
|
|
24
|
-
|
|
23
|
+
their chunks must be bytes, as in workerd. `body` is a ReadableStream for every body type,
|
|
24
|
+
as in Fetch: a stream body itself, otherwise a stream made once over the bytes.
|
|
25
25
|
BYOB buffer detachment requires transfer support from the underlying engine;
|
|
26
26
|
the polyfill cannot supply that engine primitive. Full Web Platform Tests
|
|
27
27
|
conformance has not been established for Porffor.
|
package/package.json
CHANGED
package/src/ingress-utf8.test.js
CHANGED
|
@@ -6,9 +6,10 @@ const ingress = src.slice(
|
|
|
6
6
|
src.indexOf("function __sbDecodeIngress("),
|
|
7
7
|
src.indexOf("globalThis.__sbEntry ="),
|
|
8
8
|
);
|
|
9
|
-
|
|
9
|
+
const rawBody = src.slice(src.indexOf("function __sbRawBody("), src.indexOf("function __sbHasStreamBody("));
|
|
10
|
+
if (!decoder || !ingress || !rawBody) throw new Error("Ingress decoder source not found");
|
|
10
11
|
// oxlint-disable-next-line no-function-constructor -- Exercise the text-only runtime prelude.
|
|
11
|
-
const load = new Function(`${decoder}\n${ingress}\nreturn __sbDecodeIngress;`);
|
|
12
|
+
const load = new Function(`${decoder}\n${rawBody}\n${ingress}\nreturn __sbDecodeIngress;`);
|
|
12
13
|
const decode = load();
|
|
13
14
|
const raw = (text) =>
|
|
14
15
|
Array.from(new TextEncoder().encode(text), (byte) => String.fromCharCode(byte)).join("");
|
|
@@ -3092,7 +3092,7 @@ function __sbCpuMs() {
|
|
|
3092
3092
|
function __sbTagCpu(res, t0) {
|
|
3093
3093
|
const cpu = __sbCpuMs() - t0;
|
|
3094
3094
|
try {
|
|
3095
|
-
const body = res
|
|
3095
|
+
const body = __sbRawBody(res);
|
|
3096
3096
|
if (__sbIsStr(body) && !res.headers.has("set-cookie")) {
|
|
3097
3097
|
const headers = {};
|
|
3098
3098
|
res.headers.forEach(function (value, name) {
|
|
@@ -5189,7 +5189,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
5189
5189
|
url,
|
|
5190
5190
|
method: opts.method || "GET",
|
|
5191
5191
|
headers,
|
|
5192
|
-
...__sbRequestBody(opts.body),
|
|
5192
|
+
...__sbRequestBody(opts instanceof Request ? __sbRawBody(opts) : opts.body),
|
|
5193
5193
|
});
|
|
5194
5194
|
const respHeaders = new Headers();
|
|
5195
5195
|
for (let j = 0; j < (r.headers || []).length; j++) respHeaders.set(r.headers[j][0], r.headers[j][1]);
|
|
@@ -5221,7 +5221,7 @@ globalThis.__sbInstallFetch = function () {
|
|
|
5221
5221
|
if (__sbIsFn(source.forEach)) source.forEach((v, k) => headers.push([k, v]));
|
|
5222
5222
|
else for (const k in source) headers.push([k, source[k]]);
|
|
5223
5223
|
}
|
|
5224
|
-
const body = opts.body !== undefined ? opts.body : req && req
|
|
5224
|
+
const body = opts.body !== undefined && !(opts instanceof Request) ? opts.body : req && __sbRawBody(req) !== "" ? __sbRawBody(req) : null;
|
|
5225
5225
|
const method = opts.method || (req && req.method) || "GET";
|
|
5226
5226
|
const msg = { url, method, headers, ...__sbRequestBody(body) };
|
|
5227
5227
|
const respond = (r) => {
|
|
@@ -5647,8 +5647,14 @@ async function __sbCollectBody(stream) {
|
|
|
5647
5647
|
}
|
|
5648
5648
|
return out;
|
|
5649
5649
|
}
|
|
5650
|
+
// The body as given (a string, bytes or a ReadableStream), not the stream the Fetch-spec
|
|
5651
|
+
// body getter makes over it; plain objects keep their body as is.
|
|
5652
|
+
function __sbRawBody(message) {
|
|
5653
|
+
if (message == null) return null;
|
|
5654
|
+
return message._bodyInit !== undefined ? message._bodyInit : message.body;
|
|
5655
|
+
}
|
|
5650
5656
|
function __sbHasStreamBody(res) {
|
|
5651
|
-
return res != null && res
|
|
5657
|
+
return res != null && __sbRawBody(res) instanceof ReadableStream;
|
|
5652
5658
|
}
|
|
5653
5659
|
|
|
5654
5660
|
// B19: on a host that streams (a standalone sprout's native server defines sb_stream_*), a
|
|
@@ -5697,7 +5703,7 @@ function __sbStreamingSupported() {
|
|
|
5697
5703
|
return __sbStreaming === 1;
|
|
5698
5704
|
}
|
|
5699
5705
|
function __sbStreamResponse(response) {
|
|
5700
|
-
const stream = response
|
|
5706
|
+
const stream = __sbRawBody(response);
|
|
5701
5707
|
const id = __sbStreamOp("o", "0", "");
|
|
5702
5708
|
response.headers.set("x-sb-stream", id);
|
|
5703
5709
|
response.body = "";
|
|
@@ -5733,7 +5739,7 @@ async function __sbFetchWithDrain(res, tasks) {
|
|
|
5733
5739
|
r = res && __sbIsFn(res.then) ? await res : res;
|
|
5734
5740
|
if (__sbHasStreamBody(r)) {
|
|
5735
5741
|
if (__sbStreamingSupported()) __sbStreamResponse(r);
|
|
5736
|
-
else r.body = await __sbCollectBody(r
|
|
5742
|
+
else r.body = await __sbCollectBody(__sbRawBody(r));
|
|
5737
5743
|
}
|
|
5738
5744
|
} catch (error) {
|
|
5739
5745
|
// #179 — a rejected handler promise must not take the process down with it.
|
|
@@ -5794,7 +5800,7 @@ async function __sbQueueWithDrain(result, tasks) {
|
|
|
5794
5800
|
// handing the request to user code. Request.text() in Porffor otherwise treats
|
|
5795
5801
|
// the raw bytes as already-decoded characters and corrupts Unicode/HMAC input.
|
|
5796
5802
|
function __sbDecodeIngress(request) {
|
|
5797
|
-
const raw = request
|
|
5803
|
+
const raw = __sbRawBody(request);
|
|
5798
5804
|
if (raw == null) return;
|
|
5799
5805
|
let decoded = null;
|
|
5800
5806
|
request.text = function () {
|
|
@@ -5986,7 +5992,8 @@ async function __sbRunQueueBatch(handlers, body, ctx) {
|
|
|
5986
5992
|
|
|
5987
5993
|
function __sbReadJson(request) {
|
|
5988
5994
|
try {
|
|
5989
|
-
|
|
5995
|
+
const body = __sbRawBody(request);
|
|
5996
|
+
return JSON.parse(body == null ? "{}" : String(body));
|
|
5990
5997
|
} catch {
|
|
5991
5998
|
return {};
|
|
5992
5999
|
}
|
package/test/native-bodies.ts
CHANGED
|
@@ -18,6 +18,13 @@ const handler = `export default {async fetch(request) {
|
|
|
18
18
|
if (path === "/stream-strings") return new Response(new ReadableStream({ start(c) { c.enqueue("text"); c.close(); } }));
|
|
19
19
|
if (path === "/form") { const f = new FormData(); f.append("a", "1"); f.append("f", new Blob(["xy"]), "n.txt"); return new Response(f); }
|
|
20
20
|
const cases = [
|
|
21
|
+
// body is a ReadableStream for every body type (Fetch), and readers still work.
|
|
22
|
+
() => [new Response("hi").body instanceof ReadableStream, new Response(new Uint8Array([1])).body instanceof ReadableStream, new Response(null).body, new Request("http://a.test", { method: "POST", body: "q" }).body instanceof ReadableStream].join(","),
|
|
23
|
+
async () => new Response(new Response("abc").body).text(),
|
|
24
|
+
async () => { const r = new Response("x"); const b = r.body; return (b === r.body) + ":" + (await r.text()); },
|
|
25
|
+
async () => { const r = new Response("x"); r.body.getReader(); try { await r.text(); return "no throw"; } catch (e) { return e.name; } },
|
|
26
|
+
async () => { const reader = new Response("chunk").body.getReader(); const { value, done } = await reader.read(); return new TextDecoder().decode(value) + ":" + done + ":" + (await reader.read()).done; },
|
|
27
|
+
async () => { const r = new Response("cloned"); return (await r.clone().text()) + "|" + (await r.text()); },
|
|
21
28
|
// HTMLRewriter, against Bun's (lol-html, as in workerd): the W2 cases and more.
|
|
22
29
|
() => typeof HTMLRewriter,
|
|
23
30
|
() => new HTMLRewriter().on("h1", { element(e) { e.setInnerContent("x"); } }).transform(new Response("<h1>a</h1><p>b</p>")).text(),
|
package/test/native-request.ts
CHANGED
|
@@ -18,7 +18,9 @@ const CASES = [
|
|
|
18
18
|
`new Request(new URL("http://a.test/u")).url`,
|
|
19
19
|
`new Request("http://a.test/x", { method: "post" }).method`,
|
|
20
20
|
];
|
|
21
|
-
const handler = `export default {async fetch() {
|
|
21
|
+
const handler = `export default {async fetch(request) {
|
|
22
|
+
// The incoming request: its body is a ReadableStream, and text() still reads it.
|
|
23
|
+
if (request.method === "POST") return new Response((request.body instanceof ReadableStream) + ":" + (await request.text()));
|
|
22
24
|
const cases = [
|
|
23
25
|
${CASES.map((c) => ` () => (${c}),`).join("\n")}
|
|
24
26
|
];
|
|
@@ -91,7 +93,9 @@ try {
|
|
|
91
93
|
const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
|
|
92
94
|
const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
|
|
93
95
|
assert.deepEqual(diffs, [], `Request copies differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
|
|
94
|
-
|
|
96
|
+
const posted = await (await fetch(`http://127.0.0.1:${port}/`, { method: "POST", body: "posted é" })).text();
|
|
97
|
+
assert.equal(posted, "true:posted é", "the incoming request body is a stream and text() reads it");
|
|
98
|
+
console.log(`Native Request copies match Bun in all ${CASES.length} cases; the incoming body is a stream`);
|
|
95
99
|
} finally {
|
|
96
100
|
if (server) {
|
|
97
101
|
server.kill(9);
|