@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 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. A body given as a string, bytes or a Blob is
24
- still exposed as that value, not as a ReadableStream.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -6,9 +6,10 @@ const ingress = src.slice(
6
6
  src.indexOf("function __sbDecodeIngress("),
7
7
  src.indexOf("globalThis.__sbEntry ="),
8
8
  );
9
- if (!decoder || !ingress) throw new Error("Ingress decoder source not found");
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 && res.body;
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.body !== "" ? req.body : null;
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.body instanceof ReadableStream;
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.body;
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.body);
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.body;
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
- return JSON.parse(request.body == null ? "{}" : String(request.body));
5995
+ const body = __sbRawBody(request);
5996
+ return JSON.parse(body == null ? "{}" : String(body));
5990
5997
  } catch {
5991
5998
  return {};
5992
5999
  }
@@ -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(),
@@ -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
- console.log(`Native Request copies match Bun in all ${CASES.length} cases`);
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);