@sproutboat/runtime 0.19.0 → 0.19.2

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,17 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.19.2
4
+
5
+ ### Patch Changes
6
+
7
+ - bc3fd25: `Response.body` and `Request.body` are `ReadableStream`s only from compatibility date `2026-10-21`. 0.19.0 changed this for every app, which broke handlers that read `request.body` as a string, such as `JSON.parse(request.body)` in the CLI's own D1 example. An app with an earlier `compatibility_date` keeps the raw body, as before. To opt in, move `compatibility_date` to `2026-10-21` or later and rebuild.
8
+
9
+ ## 0.19.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 6676ead: `env.BUCKET.put(key, request.body)`, `env.KV.put(key, request.body)` and `fetch(url, { body: request.body })` store and send the request body again. Since 0.19.0, `body` is a `ReadableStream`, and these took its string form. Incoming request bodies are also read as the bytes they are, so `arrayBuffer()`, `blob()`, `body` and KV `put` no longer UTF-8 encode a non-ASCII upload twice. R2 `put` accepts bytes as well.
14
+
3
15
  ## 0.19.0
4
16
 
5
17
  ### Minor Changes
package/WEB_STREAMS.md CHANGED
@@ -20,8 +20,10 @@ 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. `body` is a ReadableStream for every body type,
24
- as in Fetch: a stream body itself, otherwise a stream made once over the bytes.
23
+ their chunks must be bytes, as in workerd. From compatibility date 2026-10-21, `body` is a
24
+ ReadableStream for every body type, as in Fetch: a stream body itself, otherwise a stream made
25
+ once over the bytes. Before that date it is the value as given, as apps written for earlier
26
+ releases read it (`JSON.parse(request.body)`).
25
27
  BYOB buffer detachment requires transfer support from the underlying engine;
26
28
  the polyfill cannot supply that engine primitive. Full Web Platform Tests
27
29
  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.19.0",
3
+ "version": "0.19.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -4045,7 +4045,14 @@ function __sbFromUtf8(bytes) {
4045
4045
  // #232 — a fetch() request body for the wire: text as-is, and an ArrayBuffer or
4046
4046
  // any view as its bytes (one char per byte) flagged bodyBinary, instead of
4047
4047
  // String(view), which sent "0,1,2,...".
4048
+ // A body stream the runtime made over a value (Response/Request body): that value. Bindings
4049
+ // that take a body at once (R2 and KV put, outbound fetch) read it without draining the
4050
+ // stream, so put(key, request.body) works as in Workers. Other values pass through.
4051
+ function __sbUnwrapBody(value) {
4052
+ return value != null && value._sbBody !== undefined ? value._sbBody : value;
4053
+ }
4048
4054
  function __sbRequestBody(body) {
4055
+ body = __sbUnwrapBody(body);
4049
4056
  if (body == null) return { body: null };
4050
4057
  if (!__sbIsStr(body) && body.byteLength !== undefined) {
4051
4058
  const bytes = body.buffer !== undefined ? new Uint8Array(body.buffer, body.byteOffset, body.byteLength) : body;
@@ -4966,6 +4973,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
4966
4973
  return type === "json" ? JSON.parse(text) : text;
4967
4974
  },
4968
4975
  put(key, value, options) {
4976
+ value = __sbUnwrapBody(value);
4969
4977
  const msg = { ns, key: String(key), value: "" };
4970
4978
  if (value != null && !__sbIsStr(value) && value.byteLength !== undefined) {
4971
4979
  // ArrayBuffer or any ArrayBufferView: its bytes, not String(view) ("0,1,2,...").
@@ -5011,12 +5019,15 @@ globalThis.__sbInstallBindings = function (target, bindings) {
5011
5019
  const name = bindings.r2[i];
5012
5020
  target[name] = {
5013
5021
  put(key, value, options) {
5022
+ value = __sbUnwrapBody(value);
5014
5023
  const o = options || {};
5015
5024
  // #56 — the body goes out of band where the transport allows it.
5025
+ // Bytes go in as bytes: a byte view or the bytes of a body stream (request.body).
5026
+ const raw = value == null ? "" : !__sbIsStr(value) && value.byteLength !== undefined ? __sbToBytes(value.buffer !== undefined ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : value) : String(value);
5016
5027
  return __sbR2Put(
5017
5028
  name,
5018
5029
  String(key),
5019
- value == null ? "" : String(value),
5030
+ raw,
5020
5031
  o.httpMetadata || {},
5021
5032
  o.customMetadata || {},
5022
5033
  ).object;
@@ -5802,6 +5813,8 @@ async function __sbQueueWithDrain(result, tasks) {
5802
5813
  function __sbDecodeIngress(request) {
5803
5814
  const raw = __sbRawBody(request);
5804
5815
  if (raw == null) return;
5816
+ // Raw bytes, one character each: arrayBuffer(), blob() and body read them as bytes.
5817
+ request._bodyLatin1 = true;
5805
5818
  let decoded = null;
5806
5819
  request.text = function () {
5807
5820
  if (decoded === null) decoded = __sbFromUtf8(raw);
@@ -92,7 +92,7 @@ try {
92
92
  const prelude = core.replace(TRANSPORT_MARKER, broker);
93
93
  const generated = join(workdir, "bodies.generated.js");
94
94
  const binary = join(workdir, "bodies-native");
95
- await writeFile(generated, wrapNativeFetchHandler(handler, prelude, undefined, undefined, undefined, "2026-10-01"));
95
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude, undefined, undefined, undefined, "2026-10-21"));
96
96
  await rm(binary, { force: true });
97
97
 
98
98
  const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
@@ -19,8 +19,9 @@ const CASES = [
19
19
  `new Request("http://a.test/x", { method: "post" }).method`,
20
20
  ];
21
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
+ // Before compatibility date 2026-10-21 (this build has the baseline date) the incoming body is
23
+ // the raw value, as apps written against earlier releases read it; text() decodes it.
24
+ if (request.method === "POST") return new Response((request.body instanceof ReadableStream) + ":" + (typeof request.body) + ":" + (await request.text()));
24
25
  const cases = [
25
26
  ${CASES.map((c) => ` () => (${c}),`).join("\n")}
26
27
  ];
@@ -94,8 +95,8 @@ try {
94
95
  const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
95
96
  assert.deepEqual(diffs, [], `Request copies differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
96
97
  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`);
98
+ assert.equal(posted, "false:string:posted é", "before 2026-10-21 the incoming body stays the raw value");
99
+ console.log(`Native Request copies match Bun in all ${CASES.length} cases; an app before 2026-10-21 keeps its raw body`);
99
100
  } finally {
100
101
  if (server) {
101
102
  server.kill(9);
@@ -17,7 +17,14 @@ const text = "/café/日本/😀/x";
17
17
  const handler = `
18
18
  const URL = "shadowed";
19
19
  export default {
20
- async fetch() {
20
+ async fetch(request) {
21
+ // put(key, request.body) and fetch(url, { body: request.body }) are the Workers idiom:
22
+ // body is a ReadableStream, and the bindings take the value it was made from.
23
+ if (request.method === "POST") {
24
+ env.UPLOADS.put("upload", request.body);
25
+ await env.KV.put("upload", request.body);
26
+ return Response.json({ r2: env.UPLOADS.get("upload").text(), kv: await env.KV.get("upload"), stream: await new Response(request.body).text() });
27
+ }
21
28
  await env.KV.put("k", ${JSON.stringify(text)});
22
29
  const kv = await env.KV.get("k");
23
30
  await env.DB.prepare("CREATE TABLE t (v TEXT)").run();
@@ -38,7 +45,7 @@ function freePort(): number {
38
45
  }
39
46
 
40
47
  const workdir = await mkdtemp(join(tmpdir(), "sb-native-utf8-"));
41
- const bindings = { ...EMPTY_BINDINGS, kv: ["KV"], d1: ["DB"] };
48
+ const bindings = { ...EMPTY_BINDINGS, kv: ["KV"], d1: ["DB"], r2: ["UPLOADS"] };
42
49
  const broker = createBroker({
43
50
  db: ":memory:",
44
51
  token: "native-utf8",
@@ -58,7 +65,7 @@ try {
58
65
  assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
59
66
  const generated = join(workdir, "utf8.generated.js");
60
67
  const binary = join(workdir, "utf8-native");
61
- await writeFile(generated, wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, transport), {}, bindings));
68
+ await writeFile(generated, wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, transport), {}, bindings, undefined, "2026-10-21"));
62
69
 
63
70
  const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
64
71
  cwd: workdir,
@@ -113,7 +120,9 @@ try {
113
120
  );
114
121
  assert.match(body.error, /tabellà/);
115
122
  assert.equal(body.url, "shadowed");
116
- console.log("Non-ASCII KV, D1 and error text round-trip through the broker; a handler-level URL stays scoped");
123
+ const stored = await (await fetch(`http://127.0.0.1:${port}/`, { method: "POST", body: "uploaded é" })).json();
124
+ assert.deepEqual(stored, { r2: "uploaded é", kv: "uploaded é", stream: "uploaded é" }, "put(key, request.body) stores the request body, and body reads as UTF-8");
125
+ console.log("Non-ASCII KV, D1 and error text round-trip through the broker; a handler-level URL stays scoped; put(key, request.body) stores the body");
117
126
  } finally {
118
127
  if (server) {
119
128
  server.kill(9);