@sproutboat/runtime 0.16.0 → 0.17.1
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 +68 -0
- package/WEB_STREAMS.md +7 -3
- package/package.json +1 -1
- package/src/asset-rules.test.js +125 -0
- package/src/egress.test.ts +248 -5
- package/src/gen-streams.ts +4 -5
- package/src/native-fetch-prelude.js +776 -112
- package/src/streams-entry.js +17 -3
- package/src/transport-broker.js +12 -0
- package/src/transport-embedded.js +403 -71
- package/test/native-bodies.ts +136 -0
- package/test/native-errors.ts +101 -0
- package/test/native-events.ts +102 -0
- package/test/native-iterators.ts +6 -1
- package/test/native-json.ts +102 -0
- package/test/native-raw-body.ts +118 -0
- package/test/native-request.ts +101 -0
- package/test/native-response-defaults.ts +98 -0
- package/test/native-streaming.ts +117 -0
- package/test/native-url.ts +145 -0
- package/test/native-waituntil.ts +107 -0
- package/test/native-web-apis.ts +110 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { ensurePorffor } from "../../toolchain/src/acquire";
|
|
6
|
+
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
|
+
|
|
8
|
+
// Web API behaviour the W2 conformance suite found wrong, compared with Bun: Headers
|
|
9
|
+
// (combined get, sorted iteration, getSetCookie, trimming, validation), TextDecoder (astral,
|
|
10
|
+
// replacement, fatal, BOM, stream), URLSearchParams init forms, atob/btoa checks, Unicode
|
|
11
|
+
// case mapping, queueMicrotask and IDN hosts.
|
|
12
|
+
const CASES = [
|
|
13
|
+
`(() => { const h = new Headers(); h.append("B", " 2 "); h.append("a", "1"); h.append("a", "x"); return [h.get("a"), [...h].map((p) => p.join("=")).join(";")].join("|"); })()`,
|
|
14
|
+
`(() => { const h = new Headers(); h.append("set-cookie", "a=1"); h.append("set-cookie", "b=2"); return [h.getSetCookie().join("+"), [...h].length].join("|"); })()`,
|
|
15
|
+
`(() => { try { new Headers({ "bad name": "1" }); return "no throw"; } catch (e) { return e.name; } })()`,
|
|
16
|
+
`(() => { try { new Headers({ a: "x\\ny" }); return "no throw"; } catch (e) { return e.name; } })()`,
|
|
17
|
+
`new TextDecoder().decode(new Uint8Array([0x61, 0xf0, 0x9f, 0x98, 0x80, 0xff, 0x62]))`,
|
|
18
|
+
`(() => { try { new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([0xc3])); return "no throw"; } catch (e) { return e.name; } })()`,
|
|
19
|
+
`(() => { const d = new TextDecoder(); return d.decode(new Uint8Array([0xef, 0xbb, 0xbf, 0xe2, 0x82]), { stream: true }) + d.decode(new Uint8Array([0xac])); })()`,
|
|
20
|
+
`[new URLSearchParams({ a: "1", b: "x y" }).toString(), new URLSearchParams([["c", "3"]]).toString(), new URLSearchParams(new URLSearchParams("d=4")).toString()].join("|")`,
|
|
21
|
+
`(() => { try { atob("@@"); return "no throw"; } catch (e) { return e.name + ":" + atob(" aGVs bG8= "); } })()`,
|
|
22
|
+
`(() => { try { btoa("€"); return "no throw"; } catch (e) { return e.name; } })()`,
|
|
23
|
+
`["ÄÖÜ", "ΑΒΓ", "дж", "Ā", "Ωmega"].map((s) => s.toLowerCase() + s.toUpperCase()).join(",")`,
|
|
24
|
+
`new Promise((resolve) => { const o = []; setTimeout(() => o.push("t"), 0); queueMicrotask(() => o.push("m")); setTimeout(() => resolve(o.join()), 50); })`,
|
|
25
|
+
`new URL("http://münchen.test/x").host`,
|
|
26
|
+
`crypto.subtle.digest("SHA-1", new TextEncoder().encode("abc")).then((b) => Array.from(new Uint8Array(b), (x) => x.toString(16).padStart(2, "0")).join(""))`,
|
|
27
|
+
`crypto.subtle.importKey("raw", new TextEncoder().encode("key"), { name: "HMAC", hash: "SHA-1" }, true, ["sign"]).then(async (k) => [k.algorithm.hash.name, k.algorithm.length, Array.from(new Uint8Array(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode("msg"))), (x) => x.toString(16).padStart(2, "0")).join(""), new Uint8Array(await crypto.subtle.exportKey("raw", k)).length].join())`,
|
|
28
|
+
`crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-512" }, true, ["sign"]).then(async (k) => [k.type, k.algorithm.length, (await crypto.subtle.exportKey("raw", k)).byteLength].join())`, `(async () => { const collect = async (rs) => { const r = rs.getReader(); const out = []; for (;;) { const { value, done } = await r.read(); if (done) return out; out.push(value); } }; const bytes = new ReadableStream({ start(c) { c.enqueue(new Uint8Array([0x61, 0xe2])); c.enqueue(new Uint8Array([0x82, 0xac, 0xf0, 0x9f])); c.enqueue(new Uint8Array([0x98, 0x80])); c.close(); } }); const text = new ReadableStream({ start(c) { c.enqueue("a\\uD83D"); c.enqueue("\\uDE00b"); c.close(); } }); return (await collect(bytes.pipeThrough(new TextDecoderStream()))).join("|") + ";" + (await collect(text.pipeThrough(new TextEncoderStream()))).map((u) => Array.from(u).join(",")).join("|"); })()`,
|
|
29
|
+
];
|
|
30
|
+
const handler = `export default {async fetch() {
|
|
31
|
+
const cases = [
|
|
32
|
+
${CASES.map((c) => ` () => (${c}),`).join("\n")}
|
|
33
|
+
];
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const run of cases) {
|
|
36
|
+
try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
|
|
37
|
+
}
|
|
38
|
+
return new Response(JSON.stringify(out));
|
|
39
|
+
}};`;
|
|
40
|
+
const want = await Promise.all(CASES.map(async (c) => { try { return String(await new Function("return " + c)()); } catch (e) { return "THROWS " + (e instanceof Error ? e.message : String(e)); } }));
|
|
41
|
+
|
|
42
|
+
function freePort(): number {
|
|
43
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
44
|
+
const port = listener.port;
|
|
45
|
+
listener.stop(true);
|
|
46
|
+
return port;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-web-apis-"));
|
|
50
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
51
|
+
try {
|
|
52
|
+
const porffor = await ensurePorffor();
|
|
53
|
+
const node = Bun.which("node");
|
|
54
|
+
if (!node) throw new Error("native streams test needs node on PATH");
|
|
55
|
+
|
|
56
|
+
const [core, broker] = await Promise.all([
|
|
57
|
+
readFile(preludePath, "utf8"),
|
|
58
|
+
readFile(transportPath("broker"), "utf8"),
|
|
59
|
+
]);
|
|
60
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
61
|
+
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
62
|
+
const generated = join(workdir, "web-apis.generated.js");
|
|
63
|
+
const binary = join(workdir, "web-apis-native");
|
|
64
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
65
|
+
await rm(binary, { force: true });
|
|
66
|
+
|
|
67
|
+
const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
|
|
68
|
+
cwd: workdir,
|
|
69
|
+
env: process.env,
|
|
70
|
+
stdout: "pipe",
|
|
71
|
+
stderr: "pipe",
|
|
72
|
+
});
|
|
73
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
74
|
+
compile.exited,
|
|
75
|
+
new Response(compile.stdout).text(),
|
|
76
|
+
new Response(compile.stderr).text(),
|
|
77
|
+
]);
|
|
78
|
+
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
79
|
+
|
|
80
|
+
const port = freePort();
|
|
81
|
+
server = Bun.spawn([binary], {
|
|
82
|
+
cwd: workdir,
|
|
83
|
+
env: { ...process.env, PORT: String(port) },
|
|
84
|
+
stdout: "ignore",
|
|
85
|
+
stderr: "inherit",
|
|
86
|
+
});
|
|
87
|
+
let ready = false;
|
|
88
|
+
for (let attempt = 0; attempt < 50 && !ready; attempt++) {
|
|
89
|
+
try {
|
|
90
|
+
await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
91
|
+
ready = true;
|
|
92
|
+
} catch {
|
|
93
|
+
if (server.exitCode !== null) break;
|
|
94
|
+
await Bun.sleep(100);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
assert(ready, "compiled handler did not start");
|
|
98
|
+
// SAFETY: the handler above is the only producer of this body: a JSON array with one
|
|
99
|
+
// string per case; the comparison below reports any entry that differs.
|
|
100
|
+
const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
|
|
101
|
+
const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
|
|
102
|
+
assert.deepEqual(diffs, [], `Web API results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
|
|
103
|
+
console.log(`Native Web APIs match Bun in all ${CASES.length} cases`);
|
|
104
|
+
} finally {
|
|
105
|
+
if (server) {
|
|
106
|
+
server.kill(9);
|
|
107
|
+
await server.exited;
|
|
108
|
+
}
|
|
109
|
+
await rm(workdir, { recursive: true, force: true });
|
|
110
|
+
}
|