@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.
@@ -0,0 +1,101 @@
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
+ // new Request(request, init) and fetch-style Request copies, compared with Bun: the copy
9
+ // keeps method, URL, headers and body, and init overrides them field by field (W1).
10
+ const CASES = [
11
+ `new Request(new Request("http://a.test/x", { method: "PUT" })).method`,
12
+ `new Request(new Request("http://a.test/x?q=1")).url`,
13
+ `new Request(new Request("http://a.test/x", { method: "PUT" }), { method: "DELETE" }).method`,
14
+ `new Request(new Request("http://a.test/x", { headers: { a: "1" } })).headers.get("a")`,
15
+ `[...new Request(new Request("http://a.test/x", { headers: { a: "1" } }), { headers: { b: "2" } }).headers].join(";")`,
16
+ `new Request(new Request("http://a.test/x", { method: "POST", body: "hi" })).text()`,
17
+ `new Request(new Request("http://a.test/x", { method: "POST", body: "hi" }), { body: "yo" }).text()`,
18
+ `new Request(new URL("http://a.test/u")).url`,
19
+ `new Request("http://a.test/x", { method: "post" }).method`,
20
+ ];
21
+ const handler = `export default {async fetch() {
22
+ const cases = [
23
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
24
+ ];
25
+ const out = [];
26
+ for (const run of cases) {
27
+ try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
28
+ }
29
+ return new Response(JSON.stringify(out));
30
+ }};`;
31
+ 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)); } }));
32
+
33
+ function freePort(): number {
34
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
35
+ const port = listener.port;
36
+ listener.stop(true);
37
+ return port;
38
+ }
39
+
40
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-request-"));
41
+ let server: ReturnType<typeof Bun.spawn> | undefined;
42
+ try {
43
+ const porffor = await ensurePorffor();
44
+ const node = Bun.which("node");
45
+ if (!node) throw new Error("native streams test needs node on PATH");
46
+
47
+ const [core, broker] = await Promise.all([
48
+ readFile(preludePath, "utf8"),
49
+ readFile(transportPath("broker"), "utf8"),
50
+ ]);
51
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
52
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
53
+ const generated = join(workdir, "request.generated.js");
54
+ const binary = join(workdir, "request-native");
55
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
56
+ await rm(binary, { force: true });
57
+
58
+ const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
59
+ cwd: workdir,
60
+ env: process.env,
61
+ stdout: "pipe",
62
+ stderr: "pipe",
63
+ });
64
+ const [exitCode, stdout, stderr] = await Promise.all([
65
+ compile.exited,
66
+ new Response(compile.stdout).text(),
67
+ new Response(compile.stderr).text(),
68
+ ]);
69
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
70
+
71
+ const port = freePort();
72
+ server = Bun.spawn([binary], {
73
+ cwd: workdir,
74
+ env: { ...process.env, PORT: String(port) },
75
+ stdout: "ignore",
76
+ stderr: "inherit",
77
+ });
78
+ let ready = false;
79
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
80
+ try {
81
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
82
+ ready = true;
83
+ } catch {
84
+ if (server.exitCode !== null) break;
85
+ await Bun.sleep(100);
86
+ }
87
+ }
88
+ assert(ready, "compiled handler did not start");
89
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
90
+ // string per case; the comparison below reports any entry that differs.
91
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
92
+ const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
93
+ assert.deepEqual(diffs, [], `Request copies differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
94
+ console.log(`Native Request copies match Bun in all ${CASES.length} cases`);
95
+ } finally {
96
+ if (server) {
97
+ server.kill(9);
98
+ await server.exited;
99
+ }
100
+ await rm(workdir, { recursive: true, force: true });
101
+ }
@@ -0,0 +1,98 @@
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
+ // What workerd answers for the same handler (checked 2026-10-19, workerd 2026-10-16).
9
+ const expected = {
10
+ "/text": { type: "text/plain;charset=UTF-8" },
11
+ "/typed": { type: "text/html" },
12
+ "/json": { type: "application/json" },
13
+ "/null": { type: null },
14
+ "/bytes": { type: null },
15
+ "/redirect": { type: null, location: "https://example.com/" },
16
+ "/throw": { type: null },
17
+ } satisfies Record<string, { type: string | null; location?: string }>;
18
+ const handler = `export default {fetch(request) {
19
+ const path = new URL(request.url).pathname;
20
+ if (path === "/text") return new Response("hello");
21
+ if (path === "/typed") return new Response("<p>hi</p>", { headers: { "content-type": "text/html" } });
22
+ if (path === "/json") return Response.json({ a: 1 });
23
+ if (path === "/null") return new Response(null, { status: 204 });
24
+ if (path === "/bytes") return new Response(new Uint8Array([1, 2, 3]));
25
+ if (path === "/redirect") return Response.redirect("https://example.com", 301);
26
+ if (path === "/throw") throw new Error("boom");
27
+ return new Response("not found", { status: 404 });
28
+ }};`;
29
+
30
+ function freePort(): number {
31
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
32
+ const port = listener.port;
33
+ listener.stop(true);
34
+ return port;
35
+ }
36
+
37
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-response-defaults-"));
38
+ let server: ReturnType<typeof Bun.spawn> | undefined;
39
+ try {
40
+ const porffor = await ensurePorffor();
41
+ const node = Bun.which("node");
42
+ if (!node) throw new Error("native streams test needs node on PATH");
43
+
44
+ const [core, broker] = await Promise.all([
45
+ readFile(preludePath, "utf8"),
46
+ readFile(transportPath("broker"), "utf8"),
47
+ ]);
48
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
49
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
50
+ const generated = join(workdir, "response.generated.js");
51
+ const binary = join(workdir, "response-native");
52
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
53
+ await rm(binary, { force: true });
54
+
55
+ const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
56
+ cwd: workdir,
57
+ env: process.env,
58
+ stdout: "pipe",
59
+ stderr: "pipe",
60
+ });
61
+ const [exitCode, stdout, stderr] = await Promise.all([
62
+ compile.exited,
63
+ new Response(compile.stdout).text(),
64
+ new Response(compile.stderr).text(),
65
+ ]);
66
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
67
+
68
+ const port = freePort();
69
+ server = Bun.spawn([binary], {
70
+ cwd: workdir,
71
+ env: { ...process.env, PORT: String(port) },
72
+ stdout: "ignore",
73
+ stderr: "inherit",
74
+ });
75
+ let ready = false;
76
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
77
+ try {
78
+ await fetch(`http://127.0.0.1:${port}/text`, { signal: AbortSignal.timeout(500) });
79
+ ready = true;
80
+ } catch {
81
+ if (server.exitCode !== null) break;
82
+ await Bun.sleep(100);
83
+ }
84
+ }
85
+ assert(ready, "compiled handler did not start");
86
+ for (const [path, want] of Object.entries(expected)) {
87
+ const response = await fetch(`http://127.0.0.1:${port}${path}`, { redirect: "manual" });
88
+ assert.equal(response.headers.get("content-type"), want.type, `${path} content-type`);
89
+ if ("location" in want) assert.equal(response.headers.get("location"), want.location, `${path} location`);
90
+ }
91
+ console.log("Native Response defaults match workerd: text/plain for strings, application/json, normalized redirects");
92
+ } finally {
93
+ if (server) {
94
+ server.kill(9);
95
+ await server.exited;
96
+ }
97
+ await rm(workdir, { recursive: true, force: true });
98
+ }
@@ -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
+ // A ReadableStream response body is streamed (B19): each chunk reaches the client when the
9
+ // handler produces it, over chunked encoding, with the handler's headers and no internal
10
+ // marker; a client that goes away does not take the server down.
11
+ const handler = `export default {async fetch(request) {
12
+ const path = new URL(request.url).pathname;
13
+ const enc = new TextEncoder();
14
+ if (path === "/slow") {
15
+ let n = 0;
16
+ return new Response(new ReadableStream({ async pull(c) {
17
+ if (n === 3) { c.close(); return; }
18
+ if (n > 0) await new Promise((r) => setTimeout(r, 300));
19
+ c.enqueue(enc.encode("chunk" + n + "\\n")); n++;
20
+ } }), { headers: { "content-type": "text/event-stream" } });
21
+ }
22
+ if (path === "/forever") {
23
+ let n = 0;
24
+ return new Response(new ReadableStream({ async pull(c) { await new Promise((r) => setTimeout(r, 50)); c.enqueue(enc.encode("tick " + n++ + "\\n")); } }));
25
+ }
26
+ return new Response("ok");
27
+ }};`;
28
+
29
+ function freePort(): number {
30
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
31
+ const port = listener.port;
32
+ listener.stop(true);
33
+ return port;
34
+ }
35
+
36
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-streaming-"));
37
+ let server: ReturnType<typeof Bun.spawn> | undefined;
38
+ try {
39
+ const porffor = await ensurePorffor();
40
+ const node = Bun.which("node");
41
+ if (!node) throw new Error("native streaming test needs node on PATH");
42
+
43
+ const [core, broker] = await Promise.all([
44
+ readFile(preludePath, "utf8"),
45
+ readFile(transportPath("broker"), "utf8"),
46
+ ]);
47
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
48
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
49
+ const generated = join(workdir, "streaming.generated.js");
50
+ const binary = join(workdir, "streaming-native");
51
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude, undefined, undefined, undefined, "2026-10-01"));
52
+ await rm(binary, { force: true });
53
+
54
+ const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
55
+ cwd: workdir,
56
+ env: process.env,
57
+ stdout: "pipe",
58
+ stderr: "pipe",
59
+ });
60
+ const [exitCode, stdout, stderr] = await Promise.all([
61
+ compile.exited,
62
+ new Response(compile.stdout).text(),
63
+ new Response(compile.stderr).text(),
64
+ ]);
65
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
66
+
67
+ const port = freePort();
68
+ server = Bun.spawn([binary], {
69
+ cwd: workdir,
70
+ env: { ...process.env, PORT: String(port) },
71
+ stdout: "ignore",
72
+ stderr: "inherit",
73
+ });
74
+ let ready = false;
75
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
76
+ try {
77
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
78
+ ready = true;
79
+ } catch {
80
+ if (server.exitCode !== null) break;
81
+ await Bun.sleep(100);
82
+ }
83
+ }
84
+ assert(ready, "compiled handler did not start");
85
+
86
+ const t0 = performance.now();
87
+ const res = await fetch(`http://127.0.0.1:${port}/slow`);
88
+ assert.equal(res.headers.get("content-type"), "text/event-stream");
89
+ assert.equal(res.headers.get("x-sb-stream"), null, "the internal stream marker is never sent");
90
+ const reader = res.body!.getReader();
91
+ const decoder = new TextDecoder();
92
+ const arrivals: { ms: number; text: string }[] = [];
93
+ for (;;) {
94
+ const { value, done } = await reader.read();
95
+ if (done) break;
96
+ arrivals.push({ ms: performance.now() - t0, text: decoder.decode(value) });
97
+ }
98
+ assert.equal(arrivals.map((a) => a.text).join(""), "chunk0\nchunk1\nchunk2\n");
99
+ // Buffered, everything would arrive together after ~600 ms.
100
+ assert(arrivals[0].ms < 250, `the first chunk took ${Math.round(arrivals[0].ms)} ms`);
101
+ assert(arrivals[arrivals.length - 1].ms - arrivals[0].ms > 400, "chunks arrived together, not as produced");
102
+
103
+ const abort = new AbortController();
104
+ const forever = await fetch(`http://127.0.0.1:${port}/forever`, { signal: abort.signal });
105
+ assert.equal(decoder.decode((await forever.body!.getReader().read()).value), "tick 0\n");
106
+ abort.abort();
107
+ await Bun.sleep(300);
108
+ assert.equal(server.exitCode, null, "a client that went away took the server down");
109
+ assert.equal(await (await fetch(`http://127.0.0.1:${port}/`)).text(), "ok");
110
+ console.log(`Native streamed body: chunks at ${arrivals.map((a) => Math.round(a.ms)).join(", ")} ms`);
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,145 @@
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
+ // URL as WHATWG defines it (Bun's URL is the reference): base URLs, setters on every
9
+ // part, hash, default ports, dot segments, percent-encoding, and searchParams writing
10
+ // back into the URL. Each case is an expression over U, the URL class under test.
11
+ const CASES = [
12
+ `new U("https://Example.COM:443/a/./b/../c?x=1#frag").href`,
13
+ `new U("http://example.com:80").href`,
14
+ `new U("http://example.com:8080/p").host`,
15
+ `new U("/path?q=1", "https://a.com/base/x").href`,
16
+ `new U("rel", "https://a.com/base/x").href`,
17
+ `new U("../up", "https://a.com/a/b/c").href`,
18
+ `new U("?q", "https://a.com/p?old#h").href`,
19
+ `new U("#h2", "https://a.com/p?old#h").href`,
20
+ `new U("//other.com/x", "https://a.com/").href`,
21
+ `new U("./", "https://a.com/a/b").href`,
22
+ `new U("..", "https://a.com/a/b/").href`,
23
+ `new U("x", new U("https://a.com/d/")).href`,
24
+ `(() => { const u = new U("https://foo.example.com/api.js"); u.hostname = "example.com"; return u.href; })()`,
25
+ `(() => { const u = new U("https://a.com/"); u.port = "8080"; return u.href; })()`,
26
+ `(() => { const u = new U("https://a.com:8080/"); u.port = "443"; return u.href; })()`,
27
+ `(() => { const u = new U("https://a.com:8080/"); u.port = ""; return u.href; })()`,
28
+ `(() => { const u = new U("https://a.com/x"); u.protocol = "http"; return u.href; })()`,
29
+ `(() => { const u = new U("https://a.com/x"); u.protocol = "http:"; u.port = "80"; return u.href; })()`,
30
+ `(() => { const u = new U("https://a.com/x"); u.host = "b.com:9"; return u.href; })()`,
31
+ `(() => { const u = new U("https://a.com/x"); u.search = "a=1&b=2"; return u.href; })()`,
32
+ `(() => { const u = new U("https://a.com/x?a=1"); u.search = ""; return u.href; })()`,
33
+ `(() => { const u = new U("https://a.com/x"); u.hash = "top"; return u.href + " " + u.hash; })()`,
34
+ `(() => { const u = new U("https://a.com/x#top"); u.hash = ""; return u.href; })()`,
35
+ `(() => { const u = new U("https://a.com/x"); u.pathname = "new path"; return u.href; })()`,
36
+ `(() => { const u = new U("https://a.com/x?a=1"); u.searchParams.set("b", "two words"); return u.href; })()`,
37
+ `(() => { const u = new U("https://a.com/x?a=1&a=2"); u.searchParams.delete("a"); return u.href; })()`,
38
+ `(() => { const u = new U("https://a.com/x?b=2&a=1"); u.searchParams.sort(); return u.href; })()`,
39
+ `(() => { const u = new U("https://a.com/x?a=1"); const p = u.searchParams; u.search = "?z=9"; return p.get("z") + " " + p.get("a"); })()`,
40
+ `(() => { const u = new U("https://a.com/x"); u.searchParams.append("q", "a+b&c=d!'()~*"); return u.search; })()`,
41
+ `new U("https://a.com/x?a=1").searchParams.get("a")`,
42
+ `new U("http://u:p@h.com/").username + " " + new U("http://u:p@h.com/").password`,
43
+ `new U("http://u:p@h.com/").href`,
44
+ `new U("https://a.com/p").origin`,
45
+ `new U("http://[::1]:8080/").hostname + " " + new U("http://[::1]:8080/").port`,
46
+ `new U("http://[::1]:8080/").host`,
47
+ `U.canParse("nope") + " " + U.canParse("https://a.com") + " " + U.canParse("/x", "https://a.com")`,
48
+ `(() => { try { new U("nope"); return "no throw"; } catch (e) { return e instanceof TypeError; } })()`,
49
+ `(() => { try { new U("https://"); return "no throw"; } catch (e) { return e instanceof TypeError; } })()`,
50
+ `JSON.stringify({ u: new U("https://a.com") })`,
51
+ `new U("https://a.com").pathname`,
52
+ `new U("mailto:x@y.com").pathname + " " + new U("mailto:x@y.com").protocol + " " + new U("mailto:x@y.com").origin`,
53
+ `String(new U("https://a.com/a b"))`,
54
+ `new U("https://a.com/é?q=ü#ä").href`,
55
+ `new U("https://a.com/x?q=it's").search`,
56
+ `new U("HTTPS://A.COM/X").href`,
57
+ `new U("https://a.com?").href`,
58
+ `new U("https://a.com#").href`,
59
+ `new U("https://a.com/%2e/x").pathname`,
60
+ `(() => { const u = new U("https://a.com/x"); u.href = "http://b.com/y?z"; return u.host + " " + u.pathname + " " + u.search; })()`,
61
+ `new U("https://a.com/x?a=1#f").toString()`,
62
+ ];
63
+
64
+ const handler = `export default {fetch() {
65
+ const U = URL;
66
+ const cases = [
67
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
68
+ ];
69
+ const out = [];
70
+ for (const run of cases) {
71
+ try { out.push(String(run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
72
+ }
73
+ return new Response(JSON.stringify(out));
74
+ }};`;
75
+ const want = CASES.map((c) => String(new Function("U", "return " + c)(URL)));
76
+
77
+ function freePort(): number {
78
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
79
+ const port = listener.port;
80
+ listener.stop(true);
81
+ return port;
82
+ }
83
+
84
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-url-"));
85
+ let server: ReturnType<typeof Bun.spawn> | undefined;
86
+ try {
87
+ const porffor = await ensurePorffor();
88
+ const node = Bun.which("node");
89
+ if (!node) throw new Error("native streams test needs node on PATH");
90
+
91
+ const [core, broker] = await Promise.all([
92
+ readFile(preludePath, "utf8"),
93
+ readFile(transportPath("broker"), "utf8"),
94
+ ]);
95
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
96
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
97
+ const generated = join(workdir, "url.generated.js");
98
+ const binary = join(workdir, "url-native");
99
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
100
+ await rm(binary, { force: true });
101
+
102
+ const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
103
+ cwd: workdir,
104
+ env: process.env,
105
+ stdout: "pipe",
106
+ stderr: "pipe",
107
+ });
108
+ const [exitCode, stdout, stderr] = await Promise.all([
109
+ compile.exited,
110
+ new Response(compile.stdout).text(),
111
+ new Response(compile.stderr).text(),
112
+ ]);
113
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
114
+
115
+ const port = freePort();
116
+ server = Bun.spawn([binary], {
117
+ cwd: workdir,
118
+ env: { ...process.env, PORT: String(port) },
119
+ stdout: "ignore",
120
+ stderr: "inherit",
121
+ });
122
+ let ready = false;
123
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
124
+ try {
125
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
126
+ ready = true;
127
+ } catch {
128
+ if (server.exitCode !== null) break;
129
+ await Bun.sleep(100);
130
+ }
131
+ }
132
+ assert(ready, "compiled handler did not start");
133
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
134
+ // string per case; the comparison below reports any entry that differs.
135
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
136
+ const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
137
+ assert.deepEqual(diffs, [], `URL differs from WHATWG in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
138
+ console.log(`Native URL matches WHATWG URL in all ${CASES.length} cases: base URLs, setters, hash, ports, dot segments, encoding, searchParams`);
139
+ } finally {
140
+ if (server) {
141
+ server.kill(9);
142
+ await server.exited;
143
+ }
144
+ await rm(workdir, { recursive: true, force: true });
145
+ }
@@ -0,0 +1,107 @@
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
+ // ctx.waitUntil work runs after the response, as in workerd: the response does not wait
9
+ // for it, the work still finishes, and its 25 s cap leaves no timer behind once it is done.
10
+ const handler = `export default {async fetch(request, env, ctx) {
11
+ const path = new URL(request.url).pathname;
12
+ if (path === "/later") {
13
+ globalThis.__done = "pending";
14
+ ctx.waitUntil(new Promise((resolve) => setTimeout(() => { globalThis.__done = "done"; resolve(); }, 400)));
15
+ return new Response("scheduled");
16
+ }
17
+ if (path === "/state") return new Response(String(globalThis.__done));
18
+ if (path === "/cron") return new Response(String(globalThis.__cron));
19
+ return new Response("ok");
20
+ },
21
+ async scheduled(event, env, ctx) {
22
+ ctx.waitUntil(new Promise((resolve) => setTimeout(() => { globalThis.__cron = event.cron; resolve(); }, 50)));
23
+ }};`;
24
+
25
+ function freePort(): number {
26
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
27
+ const port = listener.port;
28
+ listener.stop(true);
29
+ return port;
30
+ }
31
+
32
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-waituntil-"));
33
+ let server: ReturnType<typeof Bun.spawn> | undefined;
34
+ try {
35
+ const porffor = await ensurePorffor();
36
+ const node = Bun.which("node");
37
+ if (!node) throw new Error("native streams test needs node on PATH");
38
+
39
+ const [core, broker] = await Promise.all([
40
+ readFile(preludePath, "utf8"),
41
+ readFile(transportPath("broker"), "utf8"),
42
+ ]);
43
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
44
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
45
+ const generated = join(workdir, "waituntil.generated.js");
46
+ const binary = join(workdir, "waituntil-native");
47
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude, undefined, undefined, undefined, "2026-10-01"));
48
+ await rm(binary, { force: true });
49
+
50
+ const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
51
+ cwd: workdir,
52
+ env: process.env,
53
+ stdout: "pipe",
54
+ stderr: "pipe",
55
+ });
56
+ const [exitCode, stdout, stderr] = await Promise.all([
57
+ compile.exited,
58
+ new Response(compile.stdout).text(),
59
+ new Response(compile.stderr).text(),
60
+ ]);
61
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
62
+
63
+ const port = freePort();
64
+ server = Bun.spawn([binary], {
65
+ cwd: workdir,
66
+ env: { ...process.env, PORT: String(port), SB_BROKER_TOKEN: "native-test-token" },
67
+ stdout: "ignore",
68
+ stderr: "inherit",
69
+ });
70
+ let ready = false;
71
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
72
+ try {
73
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
74
+ ready = true;
75
+ } catch {
76
+ if (server.exitCode !== null) break;
77
+ await Bun.sleep(100);
78
+ }
79
+ }
80
+ assert(ready, "compiled handler did not start");
81
+ const t0 = performance.now();
82
+ const first = await (await fetch(`http://127.0.0.1:${port}/later`)).text();
83
+ const elapsed = performance.now() - t0;
84
+ assert.equal(first, "scheduled");
85
+ assert(elapsed < 300, `the response waited ${Math.round(elapsed)} ms for its waitUntil work`);
86
+ assert.equal(await (await fetch(`http://127.0.0.1:${port}/state`)).text(), "pending");
87
+ await Bun.sleep(700);
88
+ assert.equal(await (await fetch(`http://127.0.0.1:${port}/state`)).text(), "done");
89
+ // A trigger answers 204 with no body. new Response("", { status: 204 }) throws under the
90
+ // Fetch-spec Response, and the prelude used it: the first cron or alarm took the server down.
91
+ const cron = await fetch(`http://127.0.0.1:${port}/`, {
92
+ method: "POST",
93
+ headers: { "x-sb-trigger": "scheduled", "x-sb-token": "native-test-token", "content-type": "application/json" },
94
+ body: JSON.stringify({ cron: "*/5 * * * *", scheduledTime: Date.now() }),
95
+ });
96
+ assert.equal(cron.status, 204);
97
+ assert.equal(await cron.text(), "");
98
+ assert.equal(await (await fetch(`http://127.0.0.1:${port}/cron`)).text(), "*/5 * * * *");
99
+ assert.equal(server.exitCode, null, "the server exited after a scheduled trigger");
100
+ console.log(`Native waitUntil runs after the response (${Math.round(elapsed)} ms) and completes; scheduled() answers 204`);
101
+ } finally {
102
+ if (server) {
103
+ server.kill(9);
104
+ await server.exited;
105
+ }
106
+ await rm(workdir, { recursive: true, force: true });
107
+ }