@sproutboat/runtime 0.36.0 → 0.37.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.
@@ -0,0 +1,114 @@
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
+ // DOMException in a compiled handler, compared with Bun. Porffor ships a stub global that
9
+ // cannot be constructed; the prelude replaces it (found while testing third-party packages).
10
+ const CASES = [
11
+ `(() => { const e = new DOMException("m", "AbortError"); return [e.name, e.message, e.code, e instanceof Error, e instanceof DOMException].join(); })()`,
12
+ `(() => { const e = new DOMException(); return [e.name, JSON.stringify(e.message), e.code].join(); })()`,
13
+ `(() => { const e = new DOMException("m", { name: "TimeoutError", cause: "c" }); return [e.name, e.code, e.cause].join(); })()`,
14
+ `(() => { const e = new DOMException("x", "CustomName"); return [e.name, e.code].join(); })()`,
15
+ `[DOMException.ABORT_ERR, DOMException.TIMEOUT_ERR, DOMException.DATA_CLONE_ERR, DOMException.NOT_SUPPORTED_ERR, DOMException.INVALID_STATE_ERR].join()`,
16
+ `(() => { const r = AbortSignal.abort().reason; return [r instanceof DOMException, r.name, r.code].join(); })()`,
17
+ `(() => { const c = new AbortController(); c.abort(); try { c.signal.throwIfAborted(); return "no throw"; } catch (e) { return [e instanceof DOMException, e.name].join(); } })()`,
18
+ `(() => { try { throw new DOMException("boom", "InvalidStateError"); } catch (e) { return String(e); } })()`,
19
+ ];
20
+ const handler = `export default {async fetch() {
21
+ const cases = [
22
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
23
+ ];
24
+ const out = [];
25
+ for (const run of cases) {
26
+ try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
27
+ }
28
+ return new Response(JSON.stringify(out));
29
+ }};`;
30
+ const want = await Promise.all(
31
+ CASES.map(async (c) => {
32
+ try {
33
+ return String(await new Function("return " + c)());
34
+ } catch (e) {
35
+ return "THROWS " + (e instanceof Error ? e.message : String(e));
36
+ }
37
+ }),
38
+ );
39
+
40
+ function freePort(): number {
41
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
42
+ const port = listener.port;
43
+ listener.stop(true);
44
+ return port;
45
+ }
46
+
47
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-domexception-"));
48
+ let server: ReturnType<typeof Bun.spawn> | undefined;
49
+ try {
50
+ const porffor = await ensurePorffor();
51
+ const node = Bun.which("node");
52
+ if (!node) throw new Error("native streams test needs node on PATH");
53
+
54
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
55
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
56
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
57
+ const generated = join(workdir, "domexception.generated.js");
58
+ const binary = join(workdir, "domexception-native");
59
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
60
+ await rm(binary, { force: true });
61
+
62
+ const compile = Bun.spawn(
63
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
64
+ {
65
+ cwd: workdir,
66
+ env: process.env,
67
+ stdout: "pipe",
68
+ stderr: "pipe",
69
+ },
70
+ );
71
+ const [exitCode, stdout, stderr] = await Promise.all([
72
+ compile.exited,
73
+ new Response(compile.stdout).text(),
74
+ new Response(compile.stderr).text(),
75
+ ]);
76
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
77
+
78
+ const port = freePort();
79
+ server = Bun.spawn([binary], {
80
+ cwd: workdir,
81
+ env: { ...process.env, PORT: String(port) },
82
+ stdout: "ignore",
83
+ stderr: "inherit",
84
+ });
85
+ let ready = false;
86
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
87
+ try {
88
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
89
+ ready = true;
90
+ } catch {
91
+ if (server.exitCode !== null) break;
92
+ await Bun.sleep(100);
93
+ }
94
+ }
95
+ assert(ready, "compiled handler did not start");
96
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
97
+ // string per case; the comparison below reports any entry that differs.
98
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
99
+ const diffs = CASES.flatMap((c, i) =>
100
+ got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
101
+ );
102
+ assert.deepEqual(
103
+ diffs,
104
+ [],
105
+ `DOMException results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
106
+ );
107
+ console.log(`Native DOMException matches Bun in all ${CASES.length} cases`);
108
+ } finally {
109
+ if (server) {
110
+ server.kill(9);
111
+ await server.exited;
112
+ }
113
+ await rm(workdir, { recursive: true, force: true });
114
+ }
@@ -0,0 +1,117 @@
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
+ // crypto.getRandomValues on wider integer views, in a compiled handler, compared with Bun. It filled only the
9
+ // low byte of each element (found while testing third-party packages).
10
+ const CASES = [
11
+ `(() => { const b = new Uint32Array(256); crypto.getRandomValues(b); return [b.some((x) => x > 65535), b.some((x) => x > 16777215), b.length].join(); })()`,
12
+ `(() => { const b = new Uint16Array(512); crypto.getRandomValues(b); return b.some((x) => x > 255); })()`,
13
+ `(() => { const b = new BigUint64Array(64); crypto.getRandomValues(b); return b.some((x) => x > 4294967295n); })()`,
14
+ `(() => { const b = new Int32Array(256); crypto.getRandomValues(b); return b.some((x) => x < 0); })()`,
15
+ `(() => { const c = new Array(37).fill(0); const b = new Uint32Array(1); for (let i = 0; i < 3700; i++) { crypto.getRandomValues(b); c[Math.floor((b[0] / 2 ** 32) * 37)]++; } return c.filter((n) => n > 0).length > 30; })()`,
16
+ `(() => { const buf = new Uint8Array(16); const view = new Uint32Array(buf.buffer, 4, 2); crypto.getRandomValues(view); return [buf.slice(0, 4).every((x) => x === 0), buf.slice(12).every((x) => x === 0), buf.slice(4, 12).some((x) => x > 0)].join(); })()`,
17
+ `(() => { const b = new Uint8Array(8); return crypto.getRandomValues(b) === b; })()`,
18
+ `(() => { try { crypto.getRandomValues(new Float32Array(2)); return "no throw"; } catch (e) { return e.name; } })()`,
19
+ `(() => { try { crypto.getRandomValues(new DataView(new ArrayBuffer(4))); return "no throw"; } catch (e) { return e.name; } })()`,
20
+ `(() => { try { crypto.getRandomValues([1, 2]); return "no throw"; } catch (e) { return e.name; } })()`,
21
+ `(() => { const b = new Uint8Array(0); return crypto.getRandomValues(b).length; })()`,
22
+ ];
23
+ const handler = `export default {async fetch() {
24
+ const cases = [
25
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
26
+ ];
27
+ const out = [];
28
+ for (const run of cases) {
29
+ try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
30
+ }
31
+ return new Response(JSON.stringify(out));
32
+ }};`;
33
+ const want = await Promise.all(
34
+ CASES.map(async (c) => {
35
+ try {
36
+ return String(await new Function("return " + c)());
37
+ } catch (e) {
38
+ return "THROWS " + (e instanceof Error ? e.message : String(e));
39
+ }
40
+ }),
41
+ );
42
+
43
+ function freePort(): number {
44
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
45
+ const port = listener.port;
46
+ listener.stop(true);
47
+ return port;
48
+ }
49
+
50
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-getrandomvalues-"));
51
+ let server: ReturnType<typeof Bun.spawn> | undefined;
52
+ try {
53
+ const porffor = await ensurePorffor();
54
+ const node = Bun.which("node");
55
+ if (!node) throw new Error("native streams test needs node on PATH");
56
+
57
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
58
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
59
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
60
+ const generated = join(workdir, "getrandomvalues.generated.js");
61
+ const binary = join(workdir, "getrandomvalues-native");
62
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
63
+ await rm(binary, { force: true });
64
+
65
+ const compile = Bun.spawn(
66
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
67
+ {
68
+ cwd: workdir,
69
+ env: process.env,
70
+ stdout: "pipe",
71
+ stderr: "pipe",
72
+ },
73
+ );
74
+ const [exitCode, stdout, stderr] = await Promise.all([
75
+ compile.exited,
76
+ new Response(compile.stdout).text(),
77
+ new Response(compile.stderr).text(),
78
+ ]);
79
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
80
+
81
+ const port = freePort();
82
+ server = Bun.spawn([binary], {
83
+ cwd: workdir,
84
+ env: { ...process.env, PORT: String(port) },
85
+ stdout: "ignore",
86
+ stderr: "inherit",
87
+ });
88
+ let ready = false;
89
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
90
+ try {
91
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
92
+ ready = true;
93
+ } catch {
94
+ if (server.exitCode !== null) break;
95
+ await Bun.sleep(100);
96
+ }
97
+ }
98
+ assert(ready, "compiled handler did not start");
99
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
100
+ // string per case; the comparison below reports any entry that differs.
101
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
102
+ const diffs = CASES.flatMap((c, i) =>
103
+ got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
104
+ );
105
+ assert.deepEqual(
106
+ diffs,
107
+ [],
108
+ `getRandomValues results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
109
+ );
110
+ console.log(`Native getRandomValues matches Bun in all ${CASES.length} cases`);
111
+ } finally {
112
+ if (server) {
113
+ server.kill(9);
114
+ await server.exited;
115
+ }
116
+ await rm(workdir, { recursive: true, force: true });
117
+ }
@@ -0,0 +1,123 @@
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
+ // TextDecoder encodings (UTF-16LE/BE, windows-1252), Blob.stream and Response.url in a compiled handler, compared with Bun
9
+ // (found while testing third-party packages).
10
+ const CASES = [
11
+ `new TextDecoder("utf-16le").decode(new Uint8Array([104, 0, 105, 0, 0x3d, 0xd8, 0x00, 0xde]))`,
12
+ `new TextDecoder("utf-16be").decode(new Uint8Array([0, 104, 0, 105]))`,
13
+ `JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([0xff, 0xfe, 65, 0])))`,
14
+ `JSON.stringify(new TextDecoder("utf-16le", { ignoreBOM: true }).decode(new Uint8Array([0xff, 0xfe, 65, 0])))`,
15
+ `JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([0x00, 0xd8, 65, 0])))`,
16
+ `JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([65, 0, 66])))`,
17
+ `(() => { try { new TextDecoder("utf-16le", { fatal: true }).decode(new Uint8Array([0x00, 0xd8])); return "no throw"; } catch (e) { return e.name; } })()`,
18
+ `(() => { const d = new TextDecoder("utf-16le"); return [d.decode(new Uint8Array([104]), { stream: true }), d.decode(new Uint8Array([0, 105]), { stream: true }), d.decode(new Uint8Array([0x3d, 0xd8]), { stream: true }), d.decode(new Uint8Array([0x00, 0xde]))].join("|"); })()`,
19
+ `new TextDecoder("windows-1252").decode(new Uint8Array([0x80, 0x41, 0xe9, 0x9f, 0xff, 0x7f]))`,
20
+ `JSON.stringify(new TextDecoder("windows-1252").decode(new Uint8Array([0x81, 0x8d, 0x8f, 0x90, 0x9d])))`,
21
+ `[new TextDecoder("latin1").encoding, new TextDecoder("ISO-8859-1").encoding, new TextDecoder(" ascii ").encoding, new TextDecoder("utf-16").encoding, new TextDecoder("UTF8").encoding, new TextDecoder("unicodefffe").encoding].join()`,
22
+ `(() => { try { new TextDecoder("not-an-encoding"); return "no throw"; } catch (e) { return e.name; } })()`,
23
+ `new TextDecoder("utf-8").decode(new Uint8Array([0xe2, 0x82, 0xac]))`,
24
+ `new Response(new Blob(["abc", new Uint8Array([100, 101])]).stream()).text()`,
25
+ `(async () => { const r = new Blob(["hello"]).stream().getReader(); const a = await r.read(); const b = await r.read(); return [a.done, a.value.length, b.done].join(); })()`,
26
+ `JSON.stringify([new Response("x").url, new Response("x").redirected, new Response(null, { status: 429 }).url + "|"])`,
27
+ `(async () => { const r = new Blob([]).stream().getReader(); const a = await r.read(); return a.done; })()`,
28
+ ];
29
+ const handler = `export default {async fetch() {
30
+ const cases = [
31
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
32
+ ];
33
+ const out = [];
34
+ for (const run of cases) {
35
+ try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
36
+ }
37
+ return new Response(JSON.stringify(out));
38
+ }};`;
39
+ const want = await Promise.all(
40
+ CASES.map(async (c) => {
41
+ try {
42
+ return String(await new Function("return " + c)());
43
+ } catch (e) {
44
+ return "THROWS " + (e instanceof Error ? e.message : String(e));
45
+ }
46
+ }),
47
+ );
48
+
49
+ function freePort(): number {
50
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
51
+ const port = listener.port;
52
+ listener.stop(true);
53
+ return port;
54
+ }
55
+
56
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-textdecoder-encodings-"));
57
+ let server: ReturnType<typeof Bun.spawn> | undefined;
58
+ try {
59
+ const porffor = await ensurePorffor();
60
+ const node = Bun.which("node");
61
+ if (!node) throw new Error("native streams test needs node on PATH");
62
+
63
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
64
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
65
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
66
+ const generated = join(workdir, "textdecoder-encodings.generated.js");
67
+ const binary = join(workdir, "textdecoder-encodings-native");
68
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
69
+ await rm(binary, { force: true });
70
+
71
+ const compile = Bun.spawn(
72
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
73
+ {
74
+ cwd: workdir,
75
+ env: process.env,
76
+ stdout: "pipe",
77
+ stderr: "pipe",
78
+ },
79
+ );
80
+ const [exitCode, stdout, stderr] = await Promise.all([
81
+ compile.exited,
82
+ new Response(compile.stdout).text(),
83
+ new Response(compile.stderr).text(),
84
+ ]);
85
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
86
+
87
+ const port = freePort();
88
+ server = Bun.spawn([binary], {
89
+ cwd: workdir,
90
+ env: { ...process.env, PORT: String(port) },
91
+ stdout: "ignore",
92
+ stderr: "inherit",
93
+ });
94
+ let ready = false;
95
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
96
+ try {
97
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
98
+ ready = true;
99
+ } catch {
100
+ if (server.exitCode !== null) break;
101
+ await Bun.sleep(100);
102
+ }
103
+ }
104
+ assert(ready, "compiled handler did not start");
105
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
106
+ // string per case; the comparison below reports any entry that differs.
107
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
108
+ const diffs = CASES.flatMap((c, i) =>
109
+ got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
110
+ );
111
+ assert.deepEqual(
112
+ diffs,
113
+ [],
114
+ `TextDecoder, Blob.stream and Response.url results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
115
+ );
116
+ console.log(`Native TextDecoder encodings, Blob.stream and Response.url match Bun in all ${CASES.length} cases`);
117
+ } finally {
118
+ if (server) {
119
+ server.kill(9);
120
+ await server.exited;
121
+ }
122
+ await rm(workdir, { recursive: true, force: true });
123
+ }