@sproutboat/runtime 0.35.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,97 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ // AsyncLocalStorage (design 02, milestone 2): the store follows the control flow across
7
+ // awaits, promise reactions, timers, intervals, microtasks and ctx.waitUntil, and many
8
+ // requests interleaved in one process each see only their own. The expected values are what
9
+ // workerd (nodejs_compat) answers for test/fixtures/als-app.js.
10
+ const cli = process.env.SB_CLI;
11
+ if (!cli) {
12
+ console.log("skipped: set SB_CLI to a sproutboat-cli checkout linked to this checkout (it builds the sprouts)");
13
+ process.exit(0);
14
+ }
15
+
16
+ function freePort(): number {
17
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
18
+ const port = listener.port;
19
+ listener.stop(true);
20
+ return port;
21
+ }
22
+
23
+ const workdir = mkdtempSync(join(tmpdir(), "sb-native-als-"));
24
+ let server: ReturnType<typeof Bun.spawn> | undefined;
25
+ try {
26
+ const project = join(workdir, "app");
27
+ mkdirSync(join(project, "src"), { recursive: true });
28
+ writeFileSync(join(project, "src/index.js"), readFileSync(new URL("./fixtures/als-app.js", import.meta.url)));
29
+ writeFileSync(
30
+ join(project, "sproutboat.jsonc"),
31
+ JSON.stringify({ name: "als", main: "src/index.js", compatibility_date: process.env.SB_COMPAT ?? "2026-11-01" }),
32
+ );
33
+ const built = Bun.spawnSync(["bun", join(cli, "src/main.ts"), "build", "--standalone", "--target", "host"], {
34
+ cwd: project,
35
+ stdout: "inherit",
36
+ stderr: "inherit",
37
+ });
38
+ assert.equal(built.exitCode, 0, "the CLI could not build the sprout");
39
+ const port = freePort();
40
+ server = Bun.spawn([join(project, "dist/als")], {
41
+ cwd: project,
42
+ env: { ...process.env, PORT: String(port), SB_DATA_DIR: join(project, "data") },
43
+ stdout: "ignore",
44
+ stderr: "inherit",
45
+ });
46
+ const base = `http://127.0.0.1:${port}`;
47
+ let ready = false;
48
+ for (let attempt = 0; attempt < 100 && !ready; attempt++) {
49
+ try {
50
+ await fetch(`${base}/none`, { signal: AbortSignal.timeout(500) });
51
+ ready = true;
52
+ } catch {
53
+ if (server.exitCode !== null) break;
54
+ await Bun.sleep(100);
55
+ }
56
+ }
57
+ assert(ready, "the standalone app did not start");
58
+ const get = async (path: string) => (await fetch(`${base}${path}`)).json();
59
+
60
+ assert.deepEqual(await get("/basic"), [
61
+ null,
62
+ "a",
63
+ "b",
64
+ null,
65
+ ["a", "x"],
66
+ null,
67
+ ["threw", "boom", null],
68
+ ["args", 1, 2],
69
+ 7,
70
+ ]);
71
+ const steps = (id: string) => [id, id, id, id, id, id, id, `q${id}`, `i${id}`, `i${id}`, `c${id}`, `s${id}`];
72
+ assert.deepEqual(await get("/await?id=one&d=2"), steps("one"));
73
+ assert.deepEqual(await get("/bind"), ["captured", "captured", "snap", null, "captured"]);
74
+ assert.deepEqual(await get("/nested-async?d=2"), ["outer", "inner", "outer", "outer"]);
75
+ assert.deepEqual(await get("/leak?d=2"), [null, null]);
76
+ await (await fetch(`${base}/wu?id=wu1&d=20`)).text();
77
+ await Bun.sleep(100);
78
+ assert.equal(await (await fetch(`${base}/wu-read`)).text(), "wu1");
79
+ console.log(
80
+ "run, exit and snapshot behave as workerd's; the store follows awaits, timers, intervals, microtasks and waitUntil",
81
+ );
82
+
83
+ // Many requests at once, each with its own store and its own delays.
84
+ const ids = Array.from({ length: 60 }, (_, i) => `req${i}`);
85
+ const answers = await Promise.all(ids.map((id, i) => get(`/await?id=${id}&d=${1 + (i % 7)}`)));
86
+ ids.forEach((id, i) => assert.deepEqual(answers[i], steps(id), `request ${id} saw another request's store`));
87
+ const nested = await Promise.all(ids.map((id, i) => get(`/nested-async?id=${id}&d=${1 + (i % 5)}`)));
88
+ nested.forEach((answer) => assert.deepEqual(answer, ["outer", "inner", "outer", "outer"]));
89
+ assert.equal(server.exitCode, null, "the server exited");
90
+ console.log("60 interleaved requests each saw only their own store at every await");
91
+ } finally {
92
+ if (server) {
93
+ server.kill(9);
94
+ await server.exited;
95
+ }
96
+ rmSync(workdir, { recursive: true, force: true });
97
+ }
@@ -0,0 +1,87 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { drive, startUpstream } from "./concurrent-driver";
6
+
7
+ // Concurrent requests in one process (design 02, milestone 1). A standalone binary serves many
8
+ // requests at once: they interleave on timers, request and response body streams, KV, D1 and
9
+ // in-worker WebSockets, and each reply must hold only its own request's values (headers, body,
10
+ // cookie, env, ctx, cf, errors, ctx.waitUntil results). The same app and driver run against a
11
+ // sproutd worker (test/concurrency-smoke.py, which serves up to 8 requests per process).
12
+ const cli = process.env.SB_CLI;
13
+ if (!cli) {
14
+ console.log("skipped: set SB_CLI to a sproutboat-cli checkout linked to this checkout (it builds the sprouts)");
15
+ process.exit(0);
16
+ }
17
+
18
+ function freePort(): number {
19
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
20
+ const port = listener.port;
21
+ listener.stop(true);
22
+ return port;
23
+ }
24
+
25
+ const workdir = mkdtempSync(join(tmpdir(), "sb-native-concurrent-"));
26
+ const upstream = startUpstream();
27
+ let server: ReturnType<typeof Bun.spawn> | undefined;
28
+ try {
29
+ const project = join(workdir, "app");
30
+ mkdirSync(join(project, "src"), { recursive: true });
31
+ writeFileSync(join(project, "src/index.js"), readFileSync(new URL("./fixtures/concurrent-app.js", import.meta.url)));
32
+ writeFileSync(
33
+ join(project, "sproutboat.jsonc"),
34
+ JSON.stringify({
35
+ name: "conc",
36
+ main: "src/index.js",
37
+ compatibility_date: process.env.SB_COMPAT ?? "2026-11-01",
38
+ vars: { GREETING: "hi" },
39
+ kv_namespaces: ["KV"],
40
+ d1_databases: ["DB"],
41
+ }),
42
+ );
43
+ const built = Bun.spawnSync(["bun", join(cli, "src/main.ts"), "build", "--standalone", "--target", "host"], {
44
+ cwd: project,
45
+ stdout: "inherit",
46
+ stderr: "inherit",
47
+ });
48
+ assert.equal(built.exitCode, 0, "the CLI could not build the sprout");
49
+ const port = freePort();
50
+ server = Bun.spawn([join(project, "dist/conc")], {
51
+ cwd: project,
52
+ env: { ...process.env, PORT: String(port), SB_EGRESS_ALLOW: "127.0.0.1", SB_DATA_DIR: join(project, "data") },
53
+ stdout: "ignore",
54
+ stderr: "ignore",
55
+ });
56
+ const base = `http://127.0.0.1:${port}`;
57
+ let ready = false;
58
+ for (let attempt = 0; attempt < 100 && !ready; attempt++) {
59
+ try {
60
+ await fetch(`${base}/ping`, { signal: AbortSignal.timeout(500) });
61
+ ready = true;
62
+ } catch {
63
+ if (server.exitCode !== null) break;
64
+ await Bun.sleep(100);
65
+ }
66
+ }
67
+ assert(ready, "the standalone app did not start");
68
+
69
+ const { requests, failures } = await drive({
70
+ base,
71
+ upstream: `http://127.0.0.1:${upstream.port}`,
72
+ requests: Number(process.env.SB_CONC_REQUESTS ?? 40),
73
+ rounds: Number(process.env.SB_CONC_ROUNDS ?? 5),
74
+ clientSockets: false,
75
+ store: true,
76
+ });
77
+ assert.equal(failures.length, 0, `cross-request leaks or failures:\n${failures.slice(0, 20).join("\n")}`);
78
+ assert.equal(server.exitCode, null, "the server exited");
79
+ console.log(`Standalone: ${requests} interleaved requests, no value crossed between them`);
80
+ } finally {
81
+ if (server) {
82
+ server.kill(9);
83
+ await server.exited;
84
+ }
85
+ upstream.stop(true);
86
+ rmSync(workdir, { recursive: true, force: true });
87
+ }
@@ -0,0 +1,156 @@
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
+ // Per-request CPU (design 02): x-sb-cpu-ms counts the thread CPU time of the slices in which one
9
+ // request ran. A spinner interleaved with requests that wait is charged for its own work, the
10
+ // waiters for theirs, whatever order they run in. An async handler reports it from compatibility
11
+ // date 2026-11-02; before that only a handler that answers synchronously does.
12
+ const handler = `export default {async fetch(request) {
13
+ const url = new URL(request.url);
14
+ const ms = Number(url.searchParams.get("ms") || 0);
15
+ const sleep = (n) => new Promise((resolve) => setTimeout(resolve, n));
16
+ // Work, not wall time: a loaded machine deschedules the process, and CPU time must not follow wall time.
17
+ const spin = (n) => { let x = 1; for (let i = 0; i < n * 40000; i++) x = (x * 1.0000001 + i) % 1000003; return x; };
18
+ if (url.pathname === "/spin") {
19
+ await sleep(5);
20
+ spin(ms / 2);
21
+ await sleep(5);
22
+ spin(ms / 2);
23
+ return new Response("spun");
24
+ }
25
+ if (url.pathname === "/wait") {
26
+ await sleep(ms);
27
+ return new Response("waited");
28
+ }
29
+ if (url.pathname === "/sync") {
30
+ spin(ms);
31
+ return new Response("sync");
32
+ }
33
+ return new Response("none", { status: 404 });
34
+ }};`;
35
+
36
+ function freePort(): number {
37
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
38
+ const port = listener.port;
39
+ listener.stop(true);
40
+ return port;
41
+ }
42
+
43
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-cpu-requests-"));
44
+ const servers: ReturnType<typeof Bun.spawn>[] = [];
45
+ async function build(date: string): Promise<number> {
46
+ const porffor = await ensurePorffor();
47
+ const node = Bun.which("node");
48
+ if (!node) throw new Error("native cpu test needs node on PATH");
49
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
50
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
51
+ const generated = join(workdir, `cpu-${date}.generated.js`);
52
+ const binary = join(workdir, `cpu-${date}`);
53
+ await writeFile(
54
+ generated,
55
+ wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, broker), undefined, undefined, undefined, date),
56
+ );
57
+ const compile = Bun.spawn(
58
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
59
+ {
60
+ cwd: workdir,
61
+ env: process.env,
62
+ stdout: "pipe",
63
+ stderr: "pipe",
64
+ },
65
+ );
66
+ const [exitCode, stdout, stderr] = await Promise.all([
67
+ compile.exited,
68
+ new Response(compile.stdout).text(),
69
+ new Response(compile.stderr).text(),
70
+ ]);
71
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
72
+ const port = freePort();
73
+ const server = Bun.spawn([binary], {
74
+ cwd: workdir,
75
+ env: { ...process.env, PORT: String(port) },
76
+ stdout: "ignore",
77
+ stderr: "inherit",
78
+ });
79
+ servers.push(server);
80
+ for (let attempt = 0; attempt < 50; attempt++) {
81
+ try {
82
+ await fetch(`http://127.0.0.1:${port}/none`, { signal: AbortSignal.timeout(500) });
83
+ return port;
84
+ } catch {
85
+ if (server.exitCode !== null) break;
86
+ await Bun.sleep(100);
87
+ }
88
+ }
89
+ throw new Error("compiled handler did not start");
90
+ }
91
+
92
+ try {
93
+ const port = await build("2026-11-02");
94
+ const cpu = async (path: string) => {
95
+ const response = await fetch(`http://127.0.0.1:${port}${path}`);
96
+ await response.text();
97
+ const header = response.headers.get("x-sb-cpu-ms");
98
+ assert(header !== null, `${path} has no x-sb-cpu-ms`);
99
+ return Number(header);
100
+ };
101
+
102
+ const alone = await cpu("/spin?ms=200");
103
+ assert(alone > 10, `a spinner alone reports only ${alone} ms`);
104
+ const sync = await cpu("/sync?ms=200");
105
+ assert(
106
+ sync > alone * 0.5 && sync < alone * 2,
107
+ `a synchronous spinner of the same work reports ${sync} ms, the async one ${alone}`,
108
+ );
109
+ const idle = await cpu("/wait?ms=50");
110
+ assert(idle < alone / 4 && idle < 30, `a request that only waits reports ${idle} ms (a spinner: ${alone})`);
111
+
112
+ // Together: the spinner next to eight waiters, repeated so the slices interleave differently.
113
+ for (let round = 0; round < 3; round++) {
114
+ const results = await Promise.all([
115
+ cpu("/spin?ms=200"),
116
+ ...Array.from({ length: 8 }, (_, i) => cpu(`/wait?ms=${150 + i * 20}`)),
117
+ ]);
118
+ const [spinner, ...waiters] = results;
119
+ assert(
120
+ spinner > alone * 0.5 && spinner < alone * 2,
121
+ `round ${round}: the spinner reports ${spinner} ms, alone it did ${alone}`,
122
+ );
123
+ for (const waiter of waiters)
124
+ assert(
125
+ waiter < alone / 4 && waiter < 60,
126
+ `round ${round}: a waiter was charged ${waiter} ms (the spinner: ${spinner})`,
127
+ );
128
+ }
129
+ // Two spinners at once are each charged their own work, not both.
130
+ const [a, b] = await Promise.all([cpu("/spin?ms=200"), cpu("/spin?ms=200")]);
131
+ assert(
132
+ a > alone * 0.5 && a < alone * 1.6 && b > alone * 0.5 && b < alone * 1.6,
133
+ `two spinners report ${a} and ${b} ms, one alone ${alone}`,
134
+ );
135
+ console.log(`per-request CPU holds under interleaving (a spinner ${alone.toFixed(0)} ms, waiters a few ms)`);
136
+
137
+ // Before 2026-11-02 an async handler still reports nothing.
138
+ const oldPort = await build("2026-11-01");
139
+ const old = await fetch(`http://127.0.0.1:${oldPort}/spin?ms=20`);
140
+ await old.text();
141
+ assert.equal(old.headers.get("x-sb-cpu-ms"), null, "an async handler is stamped before 2026-11-02");
142
+ const oldSync = await fetch(`http://127.0.0.1:${oldPort}/sync?ms=20`);
143
+ await oldSync.text();
144
+ assert.equal(
145
+ oldSync.headers.get("x-sb-cpu-ms"),
146
+ null,
147
+ "this handler is async, so it is not stamped before 2026-11-02",
148
+ );
149
+ console.log("before 2026-11-02 an async handler is not stamped");
150
+ } finally {
151
+ for (const server of servers) {
152
+ server.kill(9);
153
+ await server.exited;
154
+ }
155
+ await rm(workdir, { recursive: true, force: true });
156
+ }
package/test/native-d1.ts CHANGED
@@ -373,6 +373,77 @@ int main(void) {
373
373
  (e: Error) => e.message === 'D1_EXEC_ERROR: Error in line 2: SELEC 2: near "SELEC": syntax error',
374
374
  );
375
375
  assert.equal(ops.__sbD1Run(ex, "SELECT count(*) AS n FROM x", [], false).results[0].n, 2, "exec rolled back");
376
+ // From 2026-11-03 (parity): D1's text, with the failing statement's own offset (counted from its first
377
+ // non-space character) and SQLite's result code.
378
+ const parityError = (lines: string[]) => {
379
+ try {
380
+ ops.__sbD1ExecLines(ex, lines, true);
381
+ } catch (e) {
382
+ return e instanceof Error ? e.message : String(e);
383
+ }
384
+ return "did not throw";
385
+ };
386
+ const parityCases: [string[], string][] = [
387
+ [
388
+ ["INSERT INTO x VALUES (3); SELEC 2"],
389
+ 'Error in line 1: INSERT INTO x VALUES (3); SELEC 2: near "SELEC": syntax error at offset 0: SQLITE_ERROR',
390
+ ],
391
+ [["SELEC 2"], 'Error in line 1: SELEC 2: near "SELEC": syntax error at offset 0: SQLITE_ERROR'],
392
+ [["SELECT 1;", " SELEC 3"], 'Error in line 2: SELEC 3: near "SELEC": syntax error at offset 2: SQLITE_ERROR'],
393
+ [["/*c*/ SELEC 1"], 'Error in line 1: /*c*/ SELEC 1: near "SELEC": syntax error at offset 6: SQLITE_ERROR'],
394
+ [["SELECT * FROM nope"], "Error in line 1: SELECT * FROM nope: no such table: nope: SQLITE_ERROR"],
395
+ [["SELECT 1,"], "Error in line 1: SELECT 1,: incomplete input: SQLITE_ERROR"],
396
+ ];
397
+ for (const [lines, text] of parityCases) assert.equal(parityError(lines), "D1_EXEC_ERROR: " + text);
398
+ ops.__sbD1ExecLines(ex, ["CREATE TABLE ux (a UNIQUE)", "INSERT INTO ux VALUES (1)"], true);
399
+ assert.equal(
400
+ parityError(["INSERT INTO ux VALUES (1)"]),
401
+ "D1_EXEC_ERROR: Error in line 1: INSERT INTO ux VALUES (1): UNIQUE constraint failed: ux.a: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_UNIQUE)",
402
+ );
403
+ // From 2026-11-03 (multi): a statement with more behind it runs them all, in one savepoint, and answers with the last.
404
+ const mdb = join(work, "data/d1/MULTI.sqlite");
405
+ const multi = (sql: string, params: unknown[] = []) => ops.__sbD1Run(mdb, sql, params, true, true);
406
+ const multiError = (sql: string, params: unknown[] = []) => {
407
+ try {
408
+ multi(sql, params);
409
+ } catch (e) {
410
+ return e instanceof Error ? e.message : String(e);
411
+ }
412
+ return "did not throw";
413
+ };
414
+ multi("CREATE TABLE m (id INTEGER PRIMARY KEY, name TEXT, v TEXT UNIQUE)");
415
+ const two = multi("INSERT INTO m (id) VALUES (7); INSERT INTO m (id) VALUES (8)");
416
+ assert.deepEqual([two.meta.changes, two.meta.last_row_id, two.rows.length], [2, 8, 0]);
417
+ assert.equal(multi("INSERT INTO m (id) VALUES (10); UPDATE m SET name = 'x'").meta.changes, 4);
418
+ assert.deepEqual(multi("SELECT 1 AS a; SELECT 2 AS b").columns, ["b"]);
419
+ assert.deepEqual(multi("SELECT 1; SELECT ? AS p", [5]).rows, [[5]]);
420
+ assert.equal(multi("INSERT INTO m (id) VALUES (30); INSERT INTO m (id) VALUES (31); ").meta.changes, 2);
421
+ const mcount = () => ops.__sbD1Run(mdb, "SELECT count(*) FROM m", [], true, true).rows[0][0];
422
+ const mbefore = mcount();
423
+ assert.equal(
424
+ multiError("INSERT INTO m (id) VALUES (40); garbage"),
425
+ 'D1_ERROR: near "garbage": syntax error at offset 0: SQLITE_ERROR',
426
+ );
427
+ assert.match(
428
+ multiError("INSERT INTO m (id, v) VALUES (41, 'a'); INSERT INTO m (id, v) VALUES (42, 'a')"),
429
+ /^D1_ERROR: UNIQUE constraint failed: m\.v: SQLITE_CONSTRAINT/,
430
+ );
431
+ assert.equal(
432
+ multiError("INSERT INTO m (id) VALUES (?); INSERT INTO m (id) VALUES (?)", [50, 51]),
433
+ "D1_ERROR: When executing multiple SQL statements in a single call, only the last statement can have parameters.",
434
+ );
435
+ assert.equal(multiError("SELECT 1;;"), "D1_ERROR: SQL code did not contain a statement.");
436
+ assert.equal(multiError("SELECT 1; -- c"), "D1_ERROR: SQL code did not contain a statement.");
437
+ assert.equal(
438
+ multiError("SELECT 1; SELECT ? AS p", [1, 2]),
439
+ "D1_ERROR: Wrong number of parameter bindings for SQL query.",
440
+ );
441
+ assert.equal(mcount(), mbefore, "a failing script left nothing behind");
442
+ assert.equal(
443
+ ops.__sbD1Run(mdb, "INSERT INTO m (id) VALUES (60); INSERT INTO m (id) VALUES (61)", [], true, false).meta.changes,
444
+ 1,
445
+ "without multi only the first runs",
446
+ );
376
447
  const db2 = join(work, "data/d1/OPS.sqlite");
377
448
  ops.__sbD1Run(db2, "CREATE TABLE p (id INTEGER PRIMARY KEY)", [], false);
378
449
  ops.__sbD1Run(db2, "CREATE TABLE c (id INTEGER PRIMARY KEY, p INTEGER REFERENCES p(id))", [], false);
@@ -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
+ }