@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 +12 -0
- package/WEB_STREAMS.md +4 -2
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +14 -1
- package/test/native-bodies.ts +1 -1
- package/test/native-request.ts +5 -4
- package/test/native-utf8.ts +13 -4
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
|
|
24
|
-
as in Fetch: a stream body itself, otherwise a stream made
|
|
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
|
@@ -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
|
-
|
|
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);
|
package/test/native-bodies.ts
CHANGED
|
@@ -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-
|
|
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"], {
|
package/test/native-request.ts
CHANGED
|
@@ -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
|
-
//
|
|
23
|
-
|
|
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, "
|
|
98
|
-
console.log(`Native Request copies match Bun in all ${CASES.length} cases;
|
|
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);
|
package/test/native-utf8.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|