@sproutboat/runtime 0.15.0 → 0.17.0
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 +70 -0
- package/README.md +6 -0
- package/WEB_STREAMS.md +37 -0
- package/WEB_STREAMS_LICENSE +22 -0
- 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 +34 -0
- package/src/native-fetch-prelude.js +3058 -70
- package/src/streams-entry.js +19 -0
- package/src/transport-broker.js +12 -0
- package/src/transport-embedded.js +403 -71
- package/test/native-base64.ts +89 -0
- 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-model-resolver.ts +100 -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-streams.ts +107 -0
- package/test/native-timers.ts +89 -0
- package/test/native-url.ts +145 -0
- package/test/native-waituntil.ts +92 -0
- package/test/native-web-apis.ts +110 -0
|
@@ -0,0 +1,118 @@
|
|
|
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 {
|
|
7
|
+
preludePath,
|
|
8
|
+
TRANSPORT_MARKER,
|
|
9
|
+
transportPath,
|
|
10
|
+
wrapNativeFetchHandler,
|
|
11
|
+
} from "../src/wrap";
|
|
12
|
+
|
|
13
|
+
// fetch() hands a response's bytes back as a raw body (x-sb-raw-body, #176), and
|
|
14
|
+
// they can arrive as a UTF-16 string holding one byte per unit: JSON.parse of the
|
|
15
|
+
// transport's reply makes one. Returning that response unchanged must send the bytes.
|
|
16
|
+
const handler = `export default {fetch() {
|
|
17
|
+
const bytes = JSON.parse('"caf\\u00e9 \\u00ff ok"');
|
|
18
|
+
return new Response(bytes, { headers: { "x-sb-raw-body": "1", "content-type": "application/octet-stream" } });
|
|
19
|
+
}};`;
|
|
20
|
+
const want = [0x63, 0x61, 0x66, 0xe9, 0x20, 0xff, 0x20, 0x6f, 0x6b];
|
|
21
|
+
|
|
22
|
+
function freePort(): number {
|
|
23
|
+
const listener = Bun.listen({
|
|
24
|
+
hostname: "127.0.0.1",
|
|
25
|
+
port: 0,
|
|
26
|
+
socket: { data() {} },
|
|
27
|
+
});
|
|
28
|
+
const port = listener.port;
|
|
29
|
+
listener.stop(true);
|
|
30
|
+
return port;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-raw-body-"));
|
|
34
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
35
|
+
try {
|
|
36
|
+
const porffor = await ensurePorffor();
|
|
37
|
+
const node = Bun.which("node");
|
|
38
|
+
if (!node) throw new Error("native streams test needs node on PATH");
|
|
39
|
+
|
|
40
|
+
const [core, broker] = await Promise.all([
|
|
41
|
+
readFile(preludePath, "utf8"),
|
|
42
|
+
readFile(transportPath("broker"), "utf8"),
|
|
43
|
+
]);
|
|
44
|
+
assert(
|
|
45
|
+
core.includes(TRANSPORT_MARKER),
|
|
46
|
+
"runtime prelude is missing its transport marker",
|
|
47
|
+
);
|
|
48
|
+
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
49
|
+
const generated = join(workdir, "raw.generated.js");
|
|
50
|
+
const binary = join(workdir, "raw-native");
|
|
51
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
52
|
+
await rm(binary, { force: true });
|
|
53
|
+
|
|
54
|
+
const compile = Bun.spawn(
|
|
55
|
+
[
|
|
56
|
+
node,
|
|
57
|
+
resolve(porffor, "runtime/index.js"),
|
|
58
|
+
"native",
|
|
59
|
+
generated,
|
|
60
|
+
"-o",
|
|
61
|
+
binary,
|
|
62
|
+
"-s",
|
|
63
|
+
"-O0",
|
|
64
|
+
],
|
|
65
|
+
{
|
|
66
|
+
cwd: workdir,
|
|
67
|
+
env: process.env,
|
|
68
|
+
stdout: "pipe",
|
|
69
|
+
stderr: "pipe",
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
73
|
+
compile.exited,
|
|
74
|
+
new Response(compile.stdout).text(),
|
|
75
|
+
new Response(compile.stderr).text(),
|
|
76
|
+
]);
|
|
77
|
+
assert.equal(
|
|
78
|
+
exitCode,
|
|
79
|
+
0,
|
|
80
|
+
`Porffor native compile failed:\n${stderr}\n${stdout}`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const port = freePort();
|
|
84
|
+
server = Bun.spawn([binary], {
|
|
85
|
+
cwd: workdir,
|
|
86
|
+
env: { ...process.env, PORT: String(port) },
|
|
87
|
+
stdout: "ignore",
|
|
88
|
+
stderr: "inherit",
|
|
89
|
+
});
|
|
90
|
+
let ready = false;
|
|
91
|
+
for (let attempt = 0; attempt < 50 && !ready; attempt++) {
|
|
92
|
+
try {
|
|
93
|
+
await fetch(`http://127.0.0.1:${port}/`, {
|
|
94
|
+
signal: AbortSignal.timeout(500),
|
|
95
|
+
});
|
|
96
|
+
ready = true;
|
|
97
|
+
} catch {
|
|
98
|
+
if (server.exitCode !== null) break;
|
|
99
|
+
await Bun.sleep(100);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
assert(ready, "compiled handler did not start");
|
|
103
|
+
const response = await fetch(`http://127.0.0.1:${port}/`);
|
|
104
|
+
const got = [...new Uint8Array(await response.arrayBuffer())];
|
|
105
|
+
assert.deepEqual(got, want, "raw body bytes");
|
|
106
|
+
assert.equal(
|
|
107
|
+
response.headers.get("x-sb-raw-body"),
|
|
108
|
+
null,
|
|
109
|
+
"the marker never reaches the client",
|
|
110
|
+
);
|
|
111
|
+
console.log("A raw UTF-16 body is sent as its bytes");
|
|
112
|
+
} finally {
|
|
113
|
+
if (server) {
|
|
114
|
+
server.kill(9);
|
|
115
|
+
await server.exited;
|
|
116
|
+
}
|
|
117
|
+
await rm(workdir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
@@ -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,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
|
+
const expected = { chunks: ["a", "b"], transformed: ["X", "Y"], first: { value: "hello", done: false },
|
|
9
|
+
done: true, locked: false, lockError: true, cancelReason: "stop", error: "broken",
|
|
10
|
+
tee: [[1, 2], [1, 2]], pulled: [3, 4], pending: "later", bytes: [7, 8], byob: [9, 10] };
|
|
11
|
+
const handler = `export default { async fetch(request) {
|
|
12
|
+
if (new URL(request.url).pathname === "/null") return new Response(null);
|
|
13
|
+
const stream = new ReadableStream({start(c){c.enqueue("hello");c.close();}});
|
|
14
|
+
const reader = stream.getReader(); let lockError = false;
|
|
15
|
+
try { stream.getReader(); } catch { lockError = true; }
|
|
16
|
+
const first = await reader.read(); const done = (await reader.read()).done;
|
|
17
|
+
reader.releaseLock();
|
|
18
|
+
const chunks = []; await ReadableStream.from(["a", "b"]).pipeTo(new WritableStream({write(x){chunks.push(x);}}));
|
|
19
|
+
const transformed = [];
|
|
20
|
+
await ReadableStream.from(["x", "y"]).pipeThrough(new TransformStream({transform(x,c){c.enqueue(x.toUpperCase());}}))
|
|
21
|
+
.pipeTo(new WritableStream({write(x){transformed.push(x);}}));
|
|
22
|
+
let cancelReason; await new ReadableStream({cancel(reason){cancelReason=reason;}}).cancel("stop");
|
|
23
|
+
let error; const broken = new ReadableStream({start(c){c.error(new Error("broken"));}}).getReader();
|
|
24
|
+
try { await broken.read(); } catch(e) { error=e.message; }
|
|
25
|
+
const branches=ReadableStream.from([1,2]).tee(); const tee=[[],[]];
|
|
26
|
+
await Promise.all(branches.map((branch,i)=>branch.pipeTo(new WritableStream({write(x){tee[i].push(x);}}))));
|
|
27
|
+
let value=3; const pulled=[];
|
|
28
|
+
await new ReadableStream({pull(c){if(value===5)c.close();else c.enqueue(value++);}})
|
|
29
|
+
.pipeTo(new WritableStream({write(x){pulled.push(x);}}));
|
|
30
|
+
let controller; const pendingReader=new ReadableStream({start(c){controller=c;}}).getReader();
|
|
31
|
+
const read=pendingReader.read(); controller.enqueue("later");controller.close();const pending=(await read).value;
|
|
32
|
+
const byteReader=new ReadableStream({type:"bytes",start(c){c.enqueue(new Uint8Array([7,8]));c.close();}}).getReader();
|
|
33
|
+
const bytes=Array.from((await byteReader.read()).value);
|
|
34
|
+
const byobReader=new ReadableStream({type:"bytes",start(c){c.enqueue(new Uint8Array([9,10]));c.close();}}).getReader({mode:"byob"});
|
|
35
|
+
const byob=Array.from((await byobReader.read(new Uint8Array(2))).value);
|
|
36
|
+
return Response.json({bytes,byob,chunks, transformed, first, done, locked:stream.locked,lockError,cancelReason,error,tee,pulled,pending});
|
|
37
|
+
} };`;
|
|
38
|
+
|
|
39
|
+
function freePort(): number {
|
|
40
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
41
|
+
const port = listener.port;
|
|
42
|
+
listener.stop(true);
|
|
43
|
+
return port;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-streams-"));
|
|
47
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
48
|
+
try {
|
|
49
|
+
const porffor = await ensurePorffor();
|
|
50
|
+
const node = Bun.which("node");
|
|
51
|
+
if (!node) throw new Error("native streams test needs node on PATH");
|
|
52
|
+
|
|
53
|
+
const [core, broker] = await Promise.all([
|
|
54
|
+
readFile(preludePath, "utf8"),
|
|
55
|
+
readFile(transportPath("broker"), "utf8"),
|
|
56
|
+
]);
|
|
57
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
58
|
+
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
59
|
+
const generated = join(workdir, "streams.generated.js");
|
|
60
|
+
const binary = join(workdir, "streams-native");
|
|
61
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
62
|
+
await rm(binary, { force: true });
|
|
63
|
+
|
|
64
|
+
const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
|
|
65
|
+
cwd: workdir,
|
|
66
|
+
env: process.env,
|
|
67
|
+
stdout: "pipe",
|
|
68
|
+
stderr: "pipe",
|
|
69
|
+
});
|
|
70
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
71
|
+
compile.exited,
|
|
72
|
+
new Response(compile.stdout).text(),
|
|
73
|
+
new Response(compile.stderr).text(),
|
|
74
|
+
]);
|
|
75
|
+
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
76
|
+
|
|
77
|
+
const port = freePort();
|
|
78
|
+
server = Bun.spawn([binary], {
|
|
79
|
+
cwd: workdir,
|
|
80
|
+
env: { ...process.env, PORT: String(port) },
|
|
81
|
+
stdout: "ignore",
|
|
82
|
+
stderr: "inherit",
|
|
83
|
+
});
|
|
84
|
+
let response: Response | undefined;
|
|
85
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
86
|
+
try {
|
|
87
|
+
response = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
88
|
+
break;
|
|
89
|
+
} catch {
|
|
90
|
+
if (server.exitCode !== null) break;
|
|
91
|
+
await Bun.sleep(100);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
assert(response, "compiled streams handler did not start");
|
|
95
|
+
assert.equal(response.status, 200);
|
|
96
|
+
assert.deepEqual(await response.json(), expected);
|
|
97
|
+
const empty = await fetch(`http://127.0.0.1:${port}/null`);
|
|
98
|
+
assert.equal(empty.status, 200);
|
|
99
|
+
assert.equal(await empty.text(), "");
|
|
100
|
+
console.log("Native Web Streams pass reads, locks, cancellation, errors, piping, transforms, tee and pulls");
|
|
101
|
+
} finally {
|
|
102
|
+
if (server) {
|
|
103
|
+
server.kill(9);
|
|
104
|
+
await server.exited;
|
|
105
|
+
}
|
|
106
|
+
await rm(workdir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
const expected = { fired: 7, cancelled: false, ticks: 2 };
|
|
9
|
+
const handler = `export default {async fetch() {
|
|
10
|
+
let cancelled = false;
|
|
11
|
+
const timer = globalThis.setTimeout(() => { cancelled = true; }, 1);
|
|
12
|
+
globalThis.clearTimeout(timer);
|
|
13
|
+
const fired = await new Promise(resolve => globalThis.setTimeout(resolve, 1, 7));
|
|
14
|
+
let ticks = 0;
|
|
15
|
+
await new Promise(resolve => {
|
|
16
|
+
const interval = globalThis.setInterval(() => {
|
|
17
|
+
ticks++;
|
|
18
|
+
if (ticks === 2) { globalThis.clearInterval(interval); resolve(); }
|
|
19
|
+
}, 1);
|
|
20
|
+
});
|
|
21
|
+
return Response.json({fired, cancelled, ticks});
|
|
22
|
+
}};`;
|
|
23
|
+
|
|
24
|
+
function freePort(): number {
|
|
25
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
26
|
+
const port = listener.port;
|
|
27
|
+
listener.stop(true);
|
|
28
|
+
return port;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-timers-"));
|
|
32
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
33
|
+
try {
|
|
34
|
+
const porffor = await ensurePorffor();
|
|
35
|
+
const node = Bun.which("node");
|
|
36
|
+
if (!node) throw new Error("native timers test needs node on PATH");
|
|
37
|
+
|
|
38
|
+
const [core, broker] = await Promise.all([
|
|
39
|
+
readFile(preludePath, "utf8"),
|
|
40
|
+
readFile(transportPath("broker"), "utf8"),
|
|
41
|
+
]);
|
|
42
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
43
|
+
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
44
|
+
const generated = join(workdir, "timers.generated.js");
|
|
45
|
+
const binary = join(workdir, "timers-native");
|
|
46
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
47
|
+
await rm(binary, { force: true });
|
|
48
|
+
|
|
49
|
+
const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
|
|
50
|
+
cwd: workdir,
|
|
51
|
+
env: process.env,
|
|
52
|
+
stdout: "pipe",
|
|
53
|
+
stderr: "pipe",
|
|
54
|
+
});
|
|
55
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
56
|
+
compile.exited,
|
|
57
|
+
new Response(compile.stdout).text(),
|
|
58
|
+
new Response(compile.stderr).text(),
|
|
59
|
+
]);
|
|
60
|
+
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
61
|
+
|
|
62
|
+
const port = freePort();
|
|
63
|
+
server = Bun.spawn([binary], {
|
|
64
|
+
cwd: workdir,
|
|
65
|
+
env: { ...process.env, PORT: String(port) },
|
|
66
|
+
stdout: "ignore",
|
|
67
|
+
stderr: "inherit",
|
|
68
|
+
});
|
|
69
|
+
let response: Response | undefined;
|
|
70
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
response = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
73
|
+
break;
|
|
74
|
+
} catch {
|
|
75
|
+
if (server.exitCode !== null) break;
|
|
76
|
+
await Bun.sleep(100);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
assert(response, "compiled timers handler did not start");
|
|
80
|
+
assert.equal(response.status, 200);
|
|
81
|
+
assert.deepEqual(await response.json(), expected);
|
|
82
|
+
console.log("Native global timers preserve callbacks, arguments, cancellation and intervals");
|
|
83
|
+
} finally {
|
|
84
|
+
if (server) {
|
|
85
|
+
server.kill(9);
|
|
86
|
+
await server.exited;
|
|
87
|
+
}
|
|
88
|
+
await rm(workdir, { recursive: true, force: true });
|
|
89
|
+
}
|