@sproutboat/runtime 0.13.2 → 0.13.4
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/package.json +1 -1
- package/src/native-fetch-prelude.js +8 -2
- package/src/r2-object.test.js +2 -1
- package/src/transport-broker.js +16 -0
- package/src/transport-embedded.js +5 -0
- package/src/wrap.ts +13 -3
- package/test/native-utf8.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.13.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5bf4ea6: The handler bundle runs in its own function scope inside the generated module (part of baronunread/sproutboat#238). It used to be spliced into the prelude's top level, and Porffor resolves top-level names module-wide, so a bundle with its own top-level `const URL = ...` (uuid exports one) replaced the `URL` class the prelude extends and the binary died at startup with `Cannot get property of null`. Static imports from a direct-ESM handler stay at module top level.
|
|
8
|
+
|
|
9
|
+
## 0.13.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 733bd9e: Outbound `fetch()` and service-binding responses keep binary bodies intact (baronunread/sproutboat#232). `arrayBuffer()` and `bytes()` on a fetched response used to go through Porffor's `text()`, which the runtime overrides to decode UTF-8, so binary bodies collapsed; they now read the raw bytes. Through the broker, `fetch` and `service.fetch` sent over a v1 frame now get the upstream body as raw bytes in the frame's binary section instead of `TextDecoder` output. v0 frames, which already-deployed sprouts use, are unchanged, and a new sprout talking to an older broker falls back to the previous text behaviour.
|
|
14
|
+
|
|
3
15
|
## 0.13.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -655,6 +655,12 @@ function __sbRawBodyResponse(body, init) {
|
|
|
655
655
|
return decoded;
|
|
656
656
|
};
|
|
657
657
|
resp.json = () => JSON.parse(resp.text());
|
|
658
|
+
// #232 — Porffor's arrayBuffer() goes through text(), which decodes UTF-8
|
|
659
|
+
// above and so collapsed a binary body's multi-byte runs. The body already
|
|
660
|
+
// is the bytes, one char per byte. blob() stays text-based: Porffor's Blob
|
|
661
|
+
// only holds text and has no arrayBuffer() to read bytes back out of.
|
|
662
|
+
resp.arrayBuffer = () => __sbBufFrom(body);
|
|
663
|
+
resp.bytes = () => new Uint8Array(__sbBufFrom(body));
|
|
658
664
|
}
|
|
659
665
|
return resp;
|
|
660
666
|
}
|
|
@@ -1584,7 +1590,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
1584
1590
|
if (__sbIsFn(opts.headers.forEach)) opts.headers.forEach((v, k) => headers.push([k, v]));
|
|
1585
1591
|
else for (const k in opts.headers) headers.push([k, opts.headers[k]]);
|
|
1586
1592
|
}
|
|
1587
|
-
const r =
|
|
1593
|
+
const r = globalThis.__sbFetchUpstream("service.fetch", {
|
|
1588
1594
|
binding,
|
|
1589
1595
|
url,
|
|
1590
1596
|
method: opts.method || "GET",
|
|
@@ -1611,7 +1617,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
1611
1617
|
if (__sbIsFn(opts.headers.forEach)) opts.headers.forEach((v, k) => headers.push([k, v]));
|
|
1612
1618
|
else for (const k in opts.headers) headers.push([k, opts.headers[k]]);
|
|
1613
1619
|
}
|
|
1614
|
-
const r =
|
|
1620
|
+
const r = globalThis.__sbFetchUpstream("fetch", {
|
|
1615
1621
|
url,
|
|
1616
1622
|
method: opts.method || "GET",
|
|
1617
1623
|
headers,
|
package/src/r2-object.test.js
CHANGED
|
@@ -12,7 +12,8 @@ import { readFileSync } from "node:fs";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
const src = readFileSync(fileURLToPath(new URL("./native-fetch-prelude.js", import.meta.url)), "utf8");
|
|
15
|
-
|
|
15
|
+
// From __sbBufFrom (raw-body arrayBuffer, #232) through __sbFromUtf8.
|
|
16
|
+
const utf8Start = src.indexOf("function __sbBufFrom(");
|
|
16
17
|
const rawStart = src.indexOf("function __sbRawBodyResponse(");
|
|
17
18
|
const utf8End = rawStart;
|
|
18
19
|
const rawEnd = src.indexOf("function __sbEqCt(");
|
package/src/transport-broker.js
CHANGED
|
@@ -391,6 +391,22 @@ globalThis.__sbR2MultipartPut = function (bucket, key, uploadId, partNumber, bod
|
|
|
391
391
|
return reply;
|
|
392
392
|
};
|
|
393
393
|
|
|
394
|
+
/**
|
|
395
|
+
* #232 — outbound / service fetch over a v1 frame, so the body arrives as the
|
|
396
|
+
* upstream's raw bytes (one char per byte) instead of UTF-8-decoded text. A
|
|
397
|
+
* broker that predates this answers v1 fetch with the body still in its JSON:
|
|
398
|
+
* re-encode that to bytes so it matches today's behaviour rather than breaking.
|
|
399
|
+
*/
|
|
400
|
+
globalThis.__sbFetchUpstream = function (op, msg) {
|
|
401
|
+
const req = { v: 1, id: ++__sbReqId, token: __sbEnv("SB_BROKER_TOKEN"), op };
|
|
402
|
+
for (const k in msg) req[k] = msg[k];
|
|
403
|
+
const reply = JSON.parse(__sbCallBin(JSON.stringify(req), ""));
|
|
404
|
+
if (reply.ok === false) throw new Error("sproutboat " + op + ": " + (reply.error || "failed"));
|
|
405
|
+
const bytes = __sbTakeBin();
|
|
406
|
+
reply.body = reply.body != null ? __sbToBytes(reply.body) : bytes;
|
|
407
|
+
return reply;
|
|
408
|
+
};
|
|
409
|
+
|
|
394
410
|
/** Static asset metadata stays JSON; the response body uses the v1 byte tail. */
|
|
395
411
|
globalThis.__sbAssetsGet = function (path) {
|
|
396
412
|
const token = __sbEnv("SB_BROKER_TOKEN");
|
|
@@ -2603,6 +2603,11 @@ globalThis.__sbR2MultipartPut = function (bucket, key, uploadId, partNumber, bod
|
|
|
2603
2603
|
return { ok: true, part: { partNumber, etag: reply.etag } };
|
|
2604
2604
|
};
|
|
2605
2605
|
|
|
2606
|
+
// #232 — the C HTTP client already hands back raw bytes, one char per byte.
|
|
2607
|
+
globalThis.__sbFetchUpstream = function (op, msg) {
|
|
2608
|
+
return __sbRpc(op, msg);
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2606
2611
|
globalThis.__sbAssetsGet = function (path) {
|
|
2607
2612
|
return __sbEmbeddedDispatch({ op: "assets.get", path });
|
|
2608
2613
|
};
|
package/src/wrap.ts
CHANGED
|
@@ -228,11 +228,21 @@ export function wrapNativeFetchHandler(
|
|
|
228
228
|
? `__sbStartLocalTriggers(__sbHandlers, ${JSON.stringify(bindings)});\n`
|
|
229
229
|
: "";
|
|
230
230
|
|
|
231
|
+
// baronunread/sproutboat#238: the handler runs in its own function scope.
|
|
232
|
+
// Spliced in at the prelude's top level, a bundle's own top-level
|
|
233
|
+
// `const URL = ...` (uuid exports one) replaced the URL class for the whole
|
|
234
|
+
// module, prelude included, and the binary died at startup. Porffor resolves
|
|
235
|
+
// a top-level name module-wide, so a block is not enough; a function is.
|
|
236
|
+
// Static imports (a direct-ESM handler, no bundler in front) are only legal
|
|
237
|
+
// at module top level, so they stay outside the function.
|
|
238
|
+
const importPattern = /^import\s(?:[\s\S]*?\sfrom\s*)?["'][^"'\n]+["'];?[ \t]*$/gm;
|
|
239
|
+
const imports = (neutralised.match(importPattern) ?? []).join("\n");
|
|
240
|
+
const body = neutralised.replace(importPattern, "");
|
|
231
241
|
return (
|
|
232
242
|
`${prelude}\n${nativeTransferAbi}${compat}${env}${versionMetadataLine}${wire}` +
|
|
233
|
-
`${
|
|
234
|
-
`${registerDO}${triggers}` +
|
|
235
|
-
`export default {\n port: ${port},\n fetch(request) { return __sbEntry(
|
|
243
|
+
`${imports ? `${imports}\n` : ""}const __sbUserHandlers = (() => {\n${body}\n` +
|
|
244
|
+
`${registerDO}${triggers}return __sbHandlers;\n})();\n` +
|
|
245
|
+
`export default {\n port: ${port},\n fetch(request) { return __sbEntry(__sbUserHandlers, request); }\n};\n`
|
|
236
246
|
);
|
|
237
247
|
}
|
|
238
248
|
|
package/test/native-utf8.ts
CHANGED
|
@@ -12,7 +12,10 @@ import { EMPTY_BINDINGS, preludePath, TRANSPORT_MARKER, transportPath, wrapNativ
|
|
|
12
12
|
// two hot paths (KV, D1) and a non-ASCII SQLite error message.
|
|
13
13
|
const text = "/café/日本/😀/x";
|
|
14
14
|
|
|
15
|
+
// #238: the handler's own top-level \`URL\` (uuid exports one) must not replace
|
|
16
|
+
// the URL class the prelude extends; the handler runs in its own scope.
|
|
15
17
|
const handler = `
|
|
18
|
+
const URL = "shadowed";
|
|
16
19
|
export default {
|
|
17
20
|
async fetch() {
|
|
18
21
|
await env.KV.put("k", ${JSON.stringify(text)});
|
|
@@ -22,7 +25,7 @@ export default {
|
|
|
22
25
|
const d1 = (await env.DB.prepare("SELECT v FROM t").first()).v;
|
|
23
26
|
let error = "";
|
|
24
27
|
try { await env.DB.prepare("SELECT * FROM tabellà").first(); } catch (e) { error = String(e.message); }
|
|
25
|
-
return Response.json({ kv, kvLength: kv.length, d1, d1Length: d1.length, error });
|
|
28
|
+
return Response.json({ kv, kvLength: kv.length, d1, d1Length: d1.length, error, url: URL });
|
|
26
29
|
},
|
|
27
30
|
};
|
|
28
31
|
`;
|
|
@@ -96,13 +99,21 @@ try {
|
|
|
96
99
|
assert.equal(response.status, 200);
|
|
97
100
|
// SAFETY: the handler above is the only producer of this body and always
|
|
98
101
|
// returns exactly these five fields; deepEqual below checks every one.
|
|
99
|
-
const body = (await response.json()) as {
|
|
102
|
+
const body = (await response.json()) as {
|
|
103
|
+
kv: string;
|
|
104
|
+
kvLength: number;
|
|
105
|
+
d1: string;
|
|
106
|
+
d1Length: number;
|
|
107
|
+
error: string;
|
|
108
|
+
url: string;
|
|
109
|
+
};
|
|
100
110
|
assert.deepEqual(
|
|
101
111
|
{ kv: body.kv, kvLength: body.kvLength, d1: body.d1, d1Length: body.d1Length },
|
|
102
112
|
{ kv: text, kvLength: text.length, d1: text, d1Length: text.length },
|
|
103
113
|
);
|
|
104
114
|
assert.match(body.error, /tabellà/);
|
|
105
|
-
|
|
115
|
+
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");
|
|
106
117
|
} finally {
|
|
107
118
|
if (server) {
|
|
108
119
|
server.kill(9);
|