@sproutboat/runtime 0.42.2 → 0.44.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 +16 -0
- package/package.json +1 -1
- package/src/egress.test.ts +76 -0
- package/src/native-fetch-prelude.js +164 -14
- package/src/transport-embedded.js +78 -7
- package/test/native-base64.ts +12 -26
- package/test/native-bodies.ts +19 -26
- package/test/native-byte-text.ts +3 -19
- package/test/native-compile.ts +163 -0
- package/test/native-cpu-requests.ts +3 -20
- package/test/native-cpu-stamp.ts +3 -20
- package/test/native-crypto.ts +3 -22
- package/test/native-domexception.ts +3 -21
- package/test/native-errors.ts +12 -24
- package/test/native-events.ts +21 -25
- package/test/native-fetch-request.ts +11 -20
- package/test/native-getrandomvalues.ts +3 -21
- package/test/native-headers.ts +3 -21
- package/test/native-http.ts +10 -20
- package/test/native-iterators.ts +24 -27
- package/test/native-json-strings.ts +3 -21
- package/test/native-json.ts +15 -25
- package/test/native-jwt.ts +3 -22
- package/test/native-model-resolver.ts +5 -24
- package/test/native-r2-transfer.ts +101 -0
- package/test/native-raw-body.ts +7 -53
- package/test/native-request-log.ts +197 -0
- package/test/native-request-streaming.ts +3 -19
- package/test/native-request.ts +24 -26
- package/test/native-response-defaults.ts +4 -22
- package/test/native-service-bindings.ts +53 -27
- package/test/native-sockets-broker.ts +3 -20
- package/test/native-streaming.ts +4 -22
- package/test/native-streams.ts +22 -25
- package/test/native-subtle.ts +3 -21
- package/test/native-textdecoder-encodings.ts +3 -21
- package/test/native-timers.ts +4 -22
- package/test/native-unhandled-rejection.ts +3 -20
- package/test/native-url.ts +15 -25
- package/test/native-utf8.ts +15 -21
- package/test/native-waituntil.ts +7 -23
- package/test/native-web-apis.ts +3 -22
- package/test/native-websockets.ts +3 -19
package/test/native-raw-body.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
preludePath,
|
|
8
|
-
TRANSPORT_MARKER,
|
|
9
|
-
transportPath,
|
|
10
|
-
wrapNativeFetchHandler,
|
|
11
|
-
} from "../src/wrap";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
|
+
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
12
7
|
|
|
13
8
|
// fetch() hands a response's bytes back as a raw body (x-sb-raw-body, #176), and
|
|
14
9
|
// they can arrive as a UTF-16 string holding one byte per unit: JSON.parse of the
|
|
@@ -33,52 +28,15 @@ function freePort(): number {
|
|
|
33
28
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-raw-body-"));
|
|
34
29
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
35
30
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
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
|
-
);
|
|
31
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
32
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
48
33
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
49
34
|
const generated = join(workdir, "raw.generated.js");
|
|
50
35
|
const binary = join(workdir, "raw-native");
|
|
51
36
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
52
37
|
await rm(binary, { force: true });
|
|
53
38
|
|
|
54
|
-
|
|
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
|
-
);
|
|
39
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
82
40
|
|
|
83
41
|
const port = freePort();
|
|
84
42
|
server = Bun.spawn([binary], {
|
|
@@ -103,11 +61,7 @@ try {
|
|
|
103
61
|
const response = await fetch(`http://127.0.0.1:${port}/`);
|
|
104
62
|
const got = [...new Uint8Array(await response.arrayBuffer())];
|
|
105
63
|
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
|
-
);
|
|
64
|
+
assert.equal(response.headers.get("x-sb-raw-body"), null, "the marker never reaches the client");
|
|
111
65
|
console.log("A raw UTF-16 body is sent as its bytes");
|
|
112
66
|
} finally {
|
|
113
67
|
if (server) {
|
|
@@ -0,0 +1,197 @@
|
|
|
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 } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
|
+
import { EMPTY_BINDINGS, preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
|
+
import { isSafeInteger, isString, jsonObject, parseJsonValue, type JsonObject } from "../../wire/src/json";
|
|
8
|
+
|
|
9
|
+
// #268 — SB_LOG=json makes a standalone binary write one JSON line per request and one per uncaught
|
|
10
|
+
// error to stderr: correlated by id, bounded, with no bodies, headers, query strings or secret values.
|
|
11
|
+
// Without it the output is the plain text it always was.
|
|
12
|
+
const SECRET = "hunter2-secret-value";
|
|
13
|
+
const handler = `export default {
|
|
14
|
+
async fetch(request, env, ctx) {
|
|
15
|
+
const path = new URL(request.url).pathname;
|
|
16
|
+
if (path === "/boom") throw new Error("upstream rejected token ${SECRET}");
|
|
17
|
+
if (path === "/async-boom") {
|
|
18
|
+
await Promise.resolve();
|
|
19
|
+
throw new TypeError("async failure ${SECRET}");
|
|
20
|
+
}
|
|
21
|
+
if (path === "/unawaited") {
|
|
22
|
+
async function failNow() { throw new Error("background failure"); }
|
|
23
|
+
failNow();
|
|
24
|
+
return new Response("fired");
|
|
25
|
+
}
|
|
26
|
+
if (path === "/wait") {
|
|
27
|
+
ctx.waitUntil(Promise.reject(new Error("waitUntil failure")));
|
|
28
|
+
return new Response("queued");
|
|
29
|
+
}
|
|
30
|
+
if (request.method === "POST") return new Response("echo:" + (await request.text()));
|
|
31
|
+
return new Response("ok");
|
|
32
|
+
}
|
|
33
|
+
};`;
|
|
34
|
+
|
|
35
|
+
function freePort(): number {
|
|
36
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
37
|
+
const port = listener.port;
|
|
38
|
+
listener.stop(true);
|
|
39
|
+
return port;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Run = { get: (path: string, init?: RequestInit) => Promise<Response>; stop: () => Promise<string> };
|
|
43
|
+
|
|
44
|
+
async function start(binary: string, workdir: string, extra: Record<string, string>): Promise<Run> {
|
|
45
|
+
const port = freePort();
|
|
46
|
+
const server = Bun.spawn([binary], {
|
|
47
|
+
cwd: workdir,
|
|
48
|
+
env: { ...process.env, PORT: String(port), SB_BROKER_TOKEN: "native-test-token", TOKEN: SECRET, ...extra },
|
|
49
|
+
stdout: "ignore",
|
|
50
|
+
stderr: "pipe",
|
|
51
|
+
});
|
|
52
|
+
const stderr = new Response(server.stderr).text();
|
|
53
|
+
let ready = false;
|
|
54
|
+
for (let attempt = 0; attempt < 50 && !ready; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
57
|
+
ready = true;
|
|
58
|
+
} catch {
|
|
59
|
+
if (server.exitCode !== null) break;
|
|
60
|
+
await Bun.sleep(100);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
assert(ready, "compiled handler did not start");
|
|
64
|
+
return {
|
|
65
|
+
get: (path, init) => fetch(`http://127.0.0.1:${port}${path}`, init),
|
|
66
|
+
stop: async () => {
|
|
67
|
+
server.kill(9);
|
|
68
|
+
await server.exited;
|
|
69
|
+
return stderr;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function records(log: string): JsonObject[] {
|
|
75
|
+
const found: JsonObject[] = [];
|
|
76
|
+
for (const line of log.split("\n")) {
|
|
77
|
+
// The binary also prints a startup banner of its own; every line that is JSON must be a record.
|
|
78
|
+
if (!line.startsWith("{")) continue;
|
|
79
|
+
const record = jsonObject(parseJsonValue(line));
|
|
80
|
+
assert(record, `a stderr line is not a JSON record: ${line}`);
|
|
81
|
+
found.push(record);
|
|
82
|
+
}
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-request-log-"));
|
|
87
|
+
try {
|
|
88
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
89
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
90
|
+
const generated = join(workdir, "log.generated.js");
|
|
91
|
+
const binary = join(workdir, "log-native");
|
|
92
|
+
await writeFile(
|
|
93
|
+
generated,
|
|
94
|
+
wrapNativeFetchHandler(
|
|
95
|
+
handler,
|
|
96
|
+
core.replace(TRANSPORT_MARKER, broker),
|
|
97
|
+
undefined,
|
|
98
|
+
{ ...EMPTY_BINDINGS, secrets: ["TOKEN"] },
|
|
99
|
+
undefined,
|
|
100
|
+
"2026-11-07",
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
104
|
+
|
|
105
|
+
// Off by default: the plain text line, no records.
|
|
106
|
+
const plain = await start(binary, workdir, {});
|
|
107
|
+
assert.equal(await (await plain.get("/ok")).text(), "ok");
|
|
108
|
+
assert.equal((await plain.get("/boom")).status, 500);
|
|
109
|
+
const plainLog = await plain.stop();
|
|
110
|
+
assert(plainLog.includes("Uncaught (in fetch)"), `the default error line changed: ${plainLog}`);
|
|
111
|
+
assert(!plainLog.includes('"type"'), `records were written without SB_LOG=json: ${plainLog}`);
|
|
112
|
+
|
|
113
|
+
// On: request and error records.
|
|
114
|
+
const json = await start(binary, workdir, { SB_LOG: "json" });
|
|
115
|
+
assert.equal(
|
|
116
|
+
await (await json.get("/ok?token=abc123&page=2", { headers: { authorization: "Bearer topsecret" } })).text(),
|
|
117
|
+
"ok",
|
|
118
|
+
);
|
|
119
|
+
assert.equal(
|
|
120
|
+
await (
|
|
121
|
+
await json.get("/echo", { method: "POST", headers: { cookie: "sid=cookiesecret" }, body: "body-payload-xyz" })
|
|
122
|
+
).text(),
|
|
123
|
+
"echo:body-payload-xyz",
|
|
124
|
+
);
|
|
125
|
+
assert.equal((await json.get("/boom")).status, 500);
|
|
126
|
+
assert.equal((await json.get("/async-boom")).status, 500);
|
|
127
|
+
assert.equal(await (await json.get("/unawaited")).text(), "fired");
|
|
128
|
+
assert.equal(await (await json.get("/wait")).text(), "queued");
|
|
129
|
+
await Bun.sleep(300);
|
|
130
|
+
const log = await json.stop();
|
|
131
|
+
for (const leaked of ["abc123", "topsecret", "cookiesecret", "body-payload-xyz", SECRET, "Uncaught"]) {
|
|
132
|
+
assert(!log.includes(leaked), `"${leaked}" reached the log: ${log}`);
|
|
133
|
+
}
|
|
134
|
+
const all = records(log);
|
|
135
|
+
const requests = all.filter((record) => record.type === "request");
|
|
136
|
+
const errors = all.filter((record) => record.type === "error");
|
|
137
|
+
const request = (path: string) => {
|
|
138
|
+
const found = requests.find((record) => record.path === path);
|
|
139
|
+
assert(found, `no request record for ${path}: ${log}`);
|
|
140
|
+
return found;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const ok = request("/ok");
|
|
144
|
+
assert.equal(ok.v, 1);
|
|
145
|
+
assert.equal(ok.method, "GET");
|
|
146
|
+
assert.equal(ok.status, 200);
|
|
147
|
+
assert.equal(ok.outcome, "ok");
|
|
148
|
+
assert(isString(ok.id) && ok.id.length > 0);
|
|
149
|
+
assert(isSafeInteger(ok.durationMs) && ok.durationMs >= 0);
|
|
150
|
+
assert(isSafeInteger(ok.timestamp) && Math.abs(ok.timestamp - Date.now()) < 60_000);
|
|
151
|
+
assert.deepEqual(
|
|
152
|
+
Object.keys(ok).sort(),
|
|
153
|
+
["durationMs", "id", "method", "outcome", "path", "status", "timestamp", "type", "v"],
|
|
154
|
+
"a request record carries exactly these fields",
|
|
155
|
+
);
|
|
156
|
+
assert.equal(request("/echo").method, "POST");
|
|
157
|
+
|
|
158
|
+
for (const [path, name] of [
|
|
159
|
+
["/boom", "Error"],
|
|
160
|
+
["/async-boom", "TypeError"],
|
|
161
|
+
] as const) {
|
|
162
|
+
const failed = request(path);
|
|
163
|
+
assert.equal(failed.status, 500);
|
|
164
|
+
assert.equal(failed.outcome, "exception");
|
|
165
|
+
const error = errors.find((record) => record.id === failed.id);
|
|
166
|
+
assert(error, `no error record shares the id of ${path}: ${log}`);
|
|
167
|
+
assert.equal(error.source, "fetch");
|
|
168
|
+
assert.equal(error.name, name);
|
|
169
|
+
assert(String(error.message).includes("[redacted]"), `the secret was not redacted: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
const background = errors.find((record) => record.source === "promise");
|
|
172
|
+
assert(background, `no record for the unawaited rejection: ${log}`);
|
|
173
|
+
assert.equal(background.id, null);
|
|
174
|
+
const waitUntil = errors.find((record) => record.source === "waitUntil");
|
|
175
|
+
assert(waitUntil, `no record for the waitUntil rejection: ${log}`);
|
|
176
|
+
assert.equal(waitUntil.id, request("/wait").id);
|
|
177
|
+
|
|
178
|
+
// Bounded: a burst past 200 lines a second is counted, not written, and the count is reported.
|
|
179
|
+
const flood = await start(binary, workdir, { SB_LOG: "json" });
|
|
180
|
+
const burst = 700;
|
|
181
|
+
for (let sent = 0; sent < burst; sent += 50) {
|
|
182
|
+
await Promise.all(Array.from({ length: 50 }, () => flood.get("/ok")));
|
|
183
|
+
}
|
|
184
|
+
await Bun.sleep(1100);
|
|
185
|
+
await flood.get("/ok");
|
|
186
|
+
await Bun.sleep(200);
|
|
187
|
+
const floodRecords = records(await flood.stop());
|
|
188
|
+
const written = floodRecords.filter((record) => record.type === "request").length;
|
|
189
|
+
const dropped = floodRecords
|
|
190
|
+
.filter((record) => record.type === "dropped")
|
|
191
|
+
.reduce((sum, record) => sum + Number(record.count), 0);
|
|
192
|
+
assert(dropped > 0, "a burst past the rate limit was not reported as dropped");
|
|
193
|
+
assert.equal(written + dropped, burst + 2, "every request is either written or counted as dropped");
|
|
194
|
+
console.log("Native request and error records are structured, correlated, redacted and bounded");
|
|
195
|
+
} finally {
|
|
196
|
+
await rm(workdir, { recursive: true, force: true });
|
|
197
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
6
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
7
|
|
|
8
8
|
// Streamed request bodies, from compatibility date 2026-10-27: the handler runs once the
|
|
@@ -79,9 +79,6 @@ const expectedSum = (total: number) => {
|
|
|
79
79
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-request-streaming-"));
|
|
80
80
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
81
81
|
try {
|
|
82
|
-
const porffor = await ensurePorffor();
|
|
83
|
-
const node = Bun.which("node");
|
|
84
|
-
if (!node) throw new Error("native request-streaming test needs node on PATH");
|
|
85
82
|
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
86
83
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
87
84
|
const generated = join(workdir, "stream.generated.js");
|
|
@@ -95,20 +92,7 @@ try {
|
|
|
95
92
|
"2026-10-27",
|
|
96
93
|
);
|
|
97
94
|
await writeFile(generated, source);
|
|
98
|
-
|
|
99
|
-
[node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
|
|
100
|
-
{
|
|
101
|
-
cwd: workdir,
|
|
102
|
-
stdout: "pipe",
|
|
103
|
-
stderr: "pipe",
|
|
104
|
-
},
|
|
105
|
-
);
|
|
106
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
107
|
-
compile.exited,
|
|
108
|
-
new Response(compile.stdout).text(),
|
|
109
|
-
new Response(compile.stderr).text(),
|
|
110
|
-
]);
|
|
111
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
95
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
112
96
|
|
|
113
97
|
const port = freePort();
|
|
114
98
|
const linux = process.platform === "linux";
|
package/test/native-request.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
6
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
7
|
|
|
8
8
|
// new Request(request, init) and fetch-style Request copies, compared with Bun: the copy
|
|
@@ -31,7 +31,15 @@ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
|
|
|
31
31
|
}
|
|
32
32
|
return new Response(JSON.stringify(out));
|
|
33
33
|
}};`;
|
|
34
|
-
const want = await Promise.all(
|
|
34
|
+
const want = await Promise.all(
|
|
35
|
+
CASES.map(async (c) => {
|
|
36
|
+
try {
|
|
37
|
+
return String(await new Function("return " + c)());
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return "THROWS " + (e instanceof Error ? e.message : String(e));
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
35
43
|
|
|
36
44
|
function freePort(): number {
|
|
37
45
|
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
@@ -43,14 +51,7 @@ function freePort(): number {
|
|
|
43
51
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-request-"));
|
|
44
52
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
45
53
|
try {
|
|
46
|
-
const
|
|
47
|
-
const node = Bun.which("node");
|
|
48
|
-
if (!node) throw new Error("native streams test needs node on PATH");
|
|
49
|
-
|
|
50
|
-
const [core, broker] = await Promise.all([
|
|
51
|
-
readFile(preludePath, "utf8"),
|
|
52
|
-
readFile(transportPath("broker"), "utf8"),
|
|
53
|
-
]);
|
|
54
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
54
55
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
55
56
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
56
57
|
const generated = join(workdir, "request.generated.js");
|
|
@@ -58,18 +59,7 @@ try {
|
|
|
58
59
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
59
60
|
await rm(binary, { force: true });
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
cwd: workdir,
|
|
63
|
-
env: process.env,
|
|
64
|
-
stdout: "pipe",
|
|
65
|
-
stderr: "pipe",
|
|
66
|
-
});
|
|
67
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
68
|
-
compile.exited,
|
|
69
|
-
new Response(compile.stdout).text(),
|
|
70
|
-
new Response(compile.stderr).text(),
|
|
71
|
-
]);
|
|
72
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
62
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
73
63
|
|
|
74
64
|
const port = freePort();
|
|
75
65
|
server = Bun.spawn([binary], {
|
|
@@ -92,11 +82,19 @@ try {
|
|
|
92
82
|
// SAFETY: the handler above is the only producer of this body: a JSON array with one
|
|
93
83
|
// string per case; the comparison below reports any entry that differs.
|
|
94
84
|
const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
|
|
95
|
-
const diffs = CASES.flatMap((c, i) =>
|
|
96
|
-
|
|
85
|
+
const diffs = CASES.flatMap((c, i) =>
|
|
86
|
+
got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
|
|
87
|
+
);
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
diffs,
|
|
90
|
+
[],
|
|
91
|
+
`Request copies differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
|
|
92
|
+
);
|
|
97
93
|
const posted = await (await fetch(`http://127.0.0.1:${port}/`, { method: "POST", body: "posted é" })).text();
|
|
98
94
|
assert.equal(posted, "false:string:posted é", "before 2026-10-21 the incoming body stays the raw value");
|
|
99
|
-
console.log(
|
|
95
|
+
console.log(
|
|
96
|
+
`Native Request copies match Bun in all ${CASES.length} cases; an app before 2026-10-21 keeps its raw body`,
|
|
97
|
+
);
|
|
100
98
|
} finally {
|
|
101
99
|
if (server) {
|
|
102
100
|
server.kill(9);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
6
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
7
|
|
|
8
8
|
// What workerd answers for the same handler (checked 2026-10-19, workerd 2026-10-16).
|
|
@@ -37,14 +37,7 @@ function freePort(): number {
|
|
|
37
37
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-response-defaults-"));
|
|
38
38
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
39
39
|
try {
|
|
40
|
-
const
|
|
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
|
-
]);
|
|
40
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
48
41
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
49
42
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
50
43
|
const generated = join(workdir, "response.generated.js");
|
|
@@ -52,18 +45,7 @@ try {
|
|
|
52
45
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
53
46
|
await rm(binary, { force: true });
|
|
54
47
|
|
|
55
|
-
|
|
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}`);
|
|
48
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
67
49
|
|
|
68
50
|
const port = freePort();
|
|
69
51
|
server = Bun.spawn([binary], {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
6
|
import { createBroker, listen } from "../../wire/src/broker";
|
|
7
7
|
import { EMPTY_BINDINGS, preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
8
8
|
|
|
@@ -182,7 +182,11 @@ const edge = Bun.serve({
|
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
184
|
if (url.pathname === "/typed") {
|
|
185
|
-
return Response.json({
|
|
185
|
+
return Response.json({
|
|
186
|
+
method: request.method,
|
|
187
|
+
type: request.headers.get("content-type"),
|
|
188
|
+
body: await request.text(),
|
|
189
|
+
});
|
|
186
190
|
}
|
|
187
191
|
if (url.pathname === "/redirect") {
|
|
188
192
|
const status = Number(url.searchParams.get("status"));
|
|
@@ -246,9 +250,6 @@ const get = async (path: string) => {
|
|
|
246
250
|
return { status: res.status, text: await res.text(), ms: performance.now() - t0 };
|
|
247
251
|
};
|
|
248
252
|
try {
|
|
249
|
-
const porffor = await ensurePorffor({ cacheRoot: workdir });
|
|
250
|
-
const node = Bun.which("node");
|
|
251
|
-
if (!node) throw new Error("native service-binding test needs node on PATH");
|
|
252
253
|
const [core, transport] = await Promise.all([
|
|
253
254
|
readFile(preludePath, "utf8"),
|
|
254
255
|
readFile(transportPath("broker"), "utf8"),
|
|
@@ -256,20 +257,12 @@ try {
|
|
|
256
257
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
257
258
|
const generated = join(workdir, "services.generated.js");
|
|
258
259
|
const binary = join(workdir, "services-native");
|
|
259
|
-
await writeFile(
|
|
260
|
+
await writeFile(
|
|
261
|
+
generated,
|
|
262
|
+
wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, transport), {}, bindings, undefined, "2026-10-22"),
|
|
263
|
+
);
|
|
260
264
|
|
|
261
|
-
|
|
262
|
-
cwd: workdir,
|
|
263
|
-
env: process.env,
|
|
264
|
-
stdout: "pipe",
|
|
265
|
-
stderr: "pipe",
|
|
266
|
-
});
|
|
267
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
268
|
-
compile.exited,
|
|
269
|
-
new Response(compile.stdout).text(),
|
|
270
|
-
new Response(compile.stderr).text(),
|
|
271
|
-
]);
|
|
272
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
265
|
+
await compileNative({ generated, binary, dir: workdir, cacheRoot: workdir });
|
|
273
266
|
|
|
274
267
|
server = Bun.spawn([binary], {
|
|
275
268
|
cwd: workdir,
|
|
@@ -329,7 +322,10 @@ try {
|
|
|
329
322
|
const t0 = performance.now();
|
|
330
323
|
const all = await Promise.all(Array.from({ length: 8 }, () => get("/slow?ms=500")));
|
|
331
324
|
const total = performance.now() - t0;
|
|
332
|
-
assert(
|
|
325
|
+
assert(
|
|
326
|
+
all.every((r) => r.text === "slow done"),
|
|
327
|
+
JSON.stringify(all),
|
|
328
|
+
);
|
|
333
329
|
assert(total < 1500, `8 concurrent 500 ms calls took ${Math.round(total)} ms`);
|
|
334
330
|
|
|
335
331
|
// Cancellation: an aborted signal rejects at once, and the abort reaches the upstream
|
|
@@ -337,7 +333,11 @@ try {
|
|
|
337
333
|
const abandonedBefore = slowAbandoned;
|
|
338
334
|
const aborts = await get("/abort");
|
|
339
335
|
assert.equal(aborts.status, 200, aborts.text);
|
|
340
|
-
assert.deepEqual(JSON.parse(aborts.text), {
|
|
336
|
+
assert.deepEqual(JSON.parse(aborts.text), {
|
|
337
|
+
aborted: "AbortError fast",
|
|
338
|
+
timedOut: "TimeoutError fast",
|
|
339
|
+
already: "AbortError fast",
|
|
340
|
+
});
|
|
341
341
|
await Bun.sleep(300);
|
|
342
342
|
assert.equal(slowAbandoned - abandonedBefore, 2, "both in-flight calls were stopped upstream");
|
|
343
343
|
|
|
@@ -375,7 +375,11 @@ try {
|
|
|
375
375
|
context: { env: true, waitUntil: true },
|
|
376
376
|
});
|
|
377
377
|
// A client cannot call a method: without the edge's vouching, x-sb-rpc is just a header.
|
|
378
|
-
const forgedRpc = await fetch(`http://127.0.0.1:${port}/ping`, {
|
|
378
|
+
const forgedRpc = await fetch(`http://127.0.0.1:${port}/ping`, {
|
|
379
|
+
method: "POST",
|
|
380
|
+
headers: { "x-sb-rpc": "fail" },
|
|
381
|
+
body: '["x"]',
|
|
382
|
+
});
|
|
379
383
|
assert.equal(await forgedRpc.text(), "pong");
|
|
380
384
|
|
|
381
385
|
// A deployment calling itself, 30 levels deep: every level waits on the next at once.
|
|
@@ -394,19 +398,41 @@ export default {
|
|
|
394
398
|
const legacyPort = freePort();
|
|
395
399
|
const legacySource = join(workdir, "legacy.generated.js");
|
|
396
400
|
const legacyBinary = join(workdir, "legacy-native");
|
|
397
|
-
await writeFile(
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
await writeFile(
|
|
402
|
+
legacySource,
|
|
403
|
+
wrapNativeFetchHandler(
|
|
404
|
+
legacyHandler,
|
|
405
|
+
core.replace(TRANSPORT_MARKER, transport),
|
|
406
|
+
{},
|
|
407
|
+
bindings,
|
|
408
|
+
undefined,
|
|
409
|
+
"2026-10-21",
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
await compileNative({
|
|
413
|
+
generated: legacySource,
|
|
414
|
+
binary: legacyBinary,
|
|
415
|
+
dir: workdir,
|
|
416
|
+
cacheRoot: workdir,
|
|
417
|
+
failure: "legacy compile failed",
|
|
418
|
+
});
|
|
400
419
|
legacy = Bun.spawn([legacyBinary], {
|
|
401
420
|
cwd: workdir,
|
|
402
|
-
env: {
|
|
421
|
+
env: {
|
|
422
|
+
...process.env,
|
|
423
|
+
PORT: String(legacyPort),
|
|
424
|
+
SB_BROKER_PORT: String(brokerServer.port),
|
|
425
|
+
SB_BROKER_TOKEN: "native-services",
|
|
426
|
+
},
|
|
403
427
|
stdout: "ignore",
|
|
404
428
|
stderr: "inherit",
|
|
405
429
|
});
|
|
406
430
|
let legacyText = "";
|
|
407
431
|
for (let attempt = 0; attempt < 50 && !legacyText; attempt++) {
|
|
408
432
|
try {
|
|
409
|
-
legacyText = await (
|
|
433
|
+
legacyText = await (
|
|
434
|
+
await fetch(`http://127.0.0.1:${legacyPort}/`, { signal: AbortSignal.timeout(20_000) })
|
|
435
|
+
).text();
|
|
410
436
|
} catch {
|
|
411
437
|
await Bun.sleep(100);
|
|
412
438
|
}
|
|
@@ -2,10 +2,10 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { join
|
|
5
|
+
import { join } from "node:path";
|
|
6
6
|
import { Duplex } from "node:stream";
|
|
7
7
|
import tls from "node:tls";
|
|
8
|
-
import {
|
|
8
|
+
import { compileNative } from "./native-compile";
|
|
9
9
|
import { createBroker, listen } from "../../wire/src/broker";
|
|
10
10
|
import { EMPTY_BINDINGS, preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
11
11
|
|
|
@@ -364,9 +364,6 @@ const get = async (path: string, base = port) => {
|
|
|
364
364
|
};
|
|
365
365
|
|
|
366
366
|
try {
|
|
367
|
-
const porffor = await ensurePorffor({ cacheRoot: workdir });
|
|
368
|
-
const node = Bun.which("node");
|
|
369
|
-
if (!node) throw new Error("native sockets test needs node on PATH");
|
|
370
367
|
const [core, transport] = await Promise.all([
|
|
371
368
|
readFile(preludePath, "utf8"),
|
|
372
369
|
readFile(transportPath("broker"), "utf8"),
|
|
@@ -385,21 +382,7 @@ try {
|
|
|
385
382
|
"2026-11-17",
|
|
386
383
|
),
|
|
387
384
|
);
|
|
388
|
-
|
|
389
|
-
[node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
|
|
390
|
-
{
|
|
391
|
-
cwd: workdir,
|
|
392
|
-
env: process.env,
|
|
393
|
-
stdout: "pipe",
|
|
394
|
-
stderr: "pipe",
|
|
395
|
-
},
|
|
396
|
-
);
|
|
397
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
398
|
-
compile.exited,
|
|
399
|
-
new Response(compile.stdout).text(),
|
|
400
|
-
new Response(compile.stderr).text(),
|
|
401
|
-
]);
|
|
402
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
385
|
+
await compileNative({ generated, binary, dir: workdir, cacheRoot: workdir });
|
|
403
386
|
|
|
404
387
|
const start = async (listening: number, brokerPort: number) => {
|
|
405
388
|
const child = Bun.spawn([binary], {
|