@sproutboat/runtime 0.14.0 → 0.16.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 +14 -0
- package/README.md +6 -0
- package/WEB_STREAMS.md +33 -0
- package/WEB_STREAMS_LICENSE +22 -0
- package/package.json +1 -1
- package/src/gen-streams.ts +35 -0
- package/src/native-fetch-prelude.js +2351 -9
- package/src/source.ts +1 -8
- package/src/streams-entry.js +5 -0
- package/test/native-base64.ts +89 -0
- package/test/native-iterators.ts +103 -0
- package/test/native-model-resolver.ts +100 -0
- package/test/native-streams.ts +107 -0
- package/test/native-timers.ts +89 -0
package/src/source.ts
CHANGED
|
@@ -25,14 +25,7 @@ const alwaysForbidden: Array<[RegExp, string]> = [
|
|
|
25
25
|
// match would also flag it appearing in an ordinary string a dependency
|
|
26
26
|
// happens to construct (a doc link, a log message).
|
|
27
27
|
[/['"`]node:/, "Node, Bun, and Deno APIs are not supported"],
|
|
28
|
-
|
|
29
|
-
// trapped property reads back as `undefined`, with no throw. Rejecting it
|
|
30
|
-
// here is the difference between a build error and a 502 nobody can explain.
|
|
31
|
-
// It is why itty-router and other Proxy-based routers do not work yet.
|
|
32
|
-
[
|
|
33
|
-
/\bnew\s+Proxy\s*\(|\bProxy\s*\.\s*revocable\s*\(/,
|
|
34
|
-
"Proxy is not supported by the compiler: its traps are silently ignored and the property reads back as undefined",
|
|
35
|
-
],
|
|
28
|
+
|
|
36
29
|
];
|
|
37
30
|
|
|
38
31
|
export function validateHttpSyncSource(source: string): SourceValidation {
|
|
@@ -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 = Array.from({length:10}, (_,length) => {
|
|
9
|
+
const bytes=Uint8Array.from({length}, (_,i) => (i*71+251)%256);
|
|
10
|
+
const padded=Buffer.from(bytes).toString("base64");
|
|
11
|
+
return {padded,url:padded.replaceAll("+","-").replaceAll("/","_").replace(/=+$/, ""),decoded:Array.from(bytes)};
|
|
12
|
+
});
|
|
13
|
+
const handler = `export default {fetch() {
|
|
14
|
+
const rows=[];
|
|
15
|
+
for(let length=0;length<10;length++) {
|
|
16
|
+
const bytes=new Uint8Array(length);for(let i=0;i<length;i++)bytes[i]=(i*71+251)%256;
|
|
17
|
+
const padded=bytes.toBase64();const url=bytes.toBase64({alphabet:"base64url",omitPadding:true});
|
|
18
|
+
const decoded=Array.from(Uint8Array.fromBase64(url,{alphabet:"base64url"}));
|
|
19
|
+
rows.push({padded,url,decoded});
|
|
20
|
+
}
|
|
21
|
+
return Response.json(rows);
|
|
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-base64-"));
|
|
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 streams 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, "base64.generated.js");
|
|
45
|
+
const binary = join(workdir, "base64-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 streams handler did not start");
|
|
80
|
+
assert.equal(response.status, 200);
|
|
81
|
+
assert.deepEqual(await response.json(), expected);
|
|
82
|
+
console.log("Native base64 and base64url round-trip zero to nine bytes with correct padding");
|
|
83
|
+
} finally {
|
|
84
|
+
if (server) {
|
|
85
|
+
server.kill(9);
|
|
86
|
+
await server.exited;
|
|
87
|
+
}
|
|
88
|
+
await rm(workdir, { recursive: true, force: true });
|
|
89
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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 = {
|
|
9
|
+
query: [["a", "1"], ["a", "2"], ["b", "3"]],
|
|
10
|
+
keys: ["a", "a", "b"], values: ["1", "2", "3"],
|
|
11
|
+
live: ["x", "y", true], form: [["x", "1"], ["x", "2"]],
|
|
12
|
+
formLive: ["x", "y", true], self: true,
|
|
13
|
+
};
|
|
14
|
+
const handler = `export default { fetch(request) {
|
|
15
|
+
if (new URL(request.url).pathname === "/null") return new Response(null);
|
|
16
|
+
const params = new URLSearchParams("a=1&a=2&b=3");
|
|
17
|
+
const query = [];
|
|
18
|
+
for (const pair of params) query.push(pair);
|
|
19
|
+
const liveParams = new URLSearchParams("x=1");
|
|
20
|
+
const iterator = liveParams.keys();
|
|
21
|
+
const first = iterator.next().value;
|
|
22
|
+
liveParams.append("y", "2");
|
|
23
|
+
const second = iterator.next().value;
|
|
24
|
+
const done = iterator.next().done;
|
|
25
|
+
const fd = new FormData(); fd.append("x", "1"); fd.append("x", "2");
|
|
26
|
+
const form = []; for (const pair of fd) form.push(pair);
|
|
27
|
+
const liveForm = new FormData(); liveForm.append("x", "1");
|
|
28
|
+
const formIterator = liveForm.keys(); const f1 = formIterator.next().value;
|
|
29
|
+
liveForm.append("y", "2"); const f2 = formIterator.next().value;
|
|
30
|
+
return Response.json({ query, keys: Array.from(params.keys()), values: Array.from(params.values()),
|
|
31
|
+
live: [first, second, done], form, formLive: [f1, f2, formIterator.next().done],
|
|
32
|
+
self: iterator[Symbol.iterator]() === iterator });
|
|
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
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-iterators-"));
|
|
43
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
44
|
+
try {
|
|
45
|
+
const porffor = await ensurePorffor({ cacheRoot: workdir });
|
|
46
|
+
const node = Bun.which("node");
|
|
47
|
+
if (!node) throw new Error("native iterators test needs node on PATH");
|
|
48
|
+
|
|
49
|
+
const [core, broker] = await Promise.all([
|
|
50
|
+
readFile(preludePath, "utf8"),
|
|
51
|
+
readFile(transportPath("broker"), "utf8"),
|
|
52
|
+
]);
|
|
53
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
54
|
+
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
55
|
+
const generated = join(workdir, "iterators.generated.js");
|
|
56
|
+
const binary = join(workdir, "iterators-native");
|
|
57
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
58
|
+
await rm(binary, { force: true });
|
|
59
|
+
|
|
60
|
+
const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
|
|
61
|
+
cwd: workdir,
|
|
62
|
+
env: process.env,
|
|
63
|
+
stdout: "pipe",
|
|
64
|
+
stderr: "pipe",
|
|
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
|
+
|
|
73
|
+
const port = freePort();
|
|
74
|
+
server = Bun.spawn([binary], {
|
|
75
|
+
cwd: workdir,
|
|
76
|
+
env: { ...process.env, PORT: String(port) },
|
|
77
|
+
stdout: "ignore",
|
|
78
|
+
stderr: "inherit",
|
|
79
|
+
});
|
|
80
|
+
let response: Response | undefined;
|
|
81
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
82
|
+
try {
|
|
83
|
+
response = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
84
|
+
break;
|
|
85
|
+
} catch {
|
|
86
|
+
if (server.exitCode !== null) break;
|
|
87
|
+
await Bun.sleep(100);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
assert(response, "compiled iterators handler did not start");
|
|
91
|
+
assert.equal(response.status, 200);
|
|
92
|
+
assert.deepEqual(await response.json(), expected);
|
|
93
|
+
const empty = await fetch(`http://127.0.0.1:${port}/null`);
|
|
94
|
+
assert.equal(empty.status, 200);
|
|
95
|
+
assert.equal(await empty.text(), "");
|
|
96
|
+
console.log("Native Web API iterators preserve entries, duplicate keys and live mutations");
|
|
97
|
+
} finally {
|
|
98
|
+
if (server) {
|
|
99
|
+
server.kill(9);
|
|
100
|
+
await server.exited;
|
|
101
|
+
}
|
|
102
|
+
await rm(workdir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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 = {a: "user", b: "account", again: "user", plural: "user", custom: "user"};
|
|
9
|
+
const handler = `
|
|
10
|
+
const makeResolver = ({usePlural, schema}) => {
|
|
11
|
+
return (model) => {
|
|
12
|
+
const resolve = (candidate) => {
|
|
13
|
+
if (schema[candidate]) return candidate;
|
|
14
|
+
return Object.entries(schema).find((entry) => entry[1].modelName === candidate)?.[0];
|
|
15
|
+
};
|
|
16
|
+
if (usePlural && model.endsWith("s")) {
|
|
17
|
+
const m = resolve(model.slice(0, -1));
|
|
18
|
+
if (m) return m;
|
|
19
|
+
}
|
|
20
|
+
const m = resolve(model);
|
|
21
|
+
if (!m) throw new Error("Missing model");
|
|
22
|
+
return m;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export default {fetch() {
|
|
26
|
+
const schema = {user:{modelName:"user"},session:{modelName:"session"}};
|
|
27
|
+
const a = makeResolver({schema,usePlural:false});
|
|
28
|
+
const b = makeResolver({schema:{account:{modelName:"account"}},usePlural:false});
|
|
29
|
+
return Response.json({a:a("user"),b:b("account"),again:a("user"),
|
|
30
|
+
plural:makeResolver({schema,usePlural:true})("users"),
|
|
31
|
+
custom:makeResolver({schema:{user:{modelName:"members"}},usePlural:false})("members")});
|
|
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
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-model-resolver-"));
|
|
43
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
44
|
+
try {
|
|
45
|
+
const porffor = await ensurePorffor();
|
|
46
|
+
const node = Bun.which("node");
|
|
47
|
+
if (!node) throw new Error("native streams test needs node on PATH");
|
|
48
|
+
|
|
49
|
+
const [core, broker] = await Promise.all([
|
|
50
|
+
readFile(preludePath, "utf8"),
|
|
51
|
+
readFile(transportPath("broker"), "utf8"),
|
|
52
|
+
]);
|
|
53
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
54
|
+
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
55
|
+
const generated = join(workdir, "model-resolver.generated.js");
|
|
56
|
+
const binary = join(workdir, "model-resolver-native");
|
|
57
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
58
|
+
await rm(binary, { force: true });
|
|
59
|
+
|
|
60
|
+
const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
|
|
61
|
+
cwd: workdir,
|
|
62
|
+
env: process.env,
|
|
63
|
+
stdout: "pipe",
|
|
64
|
+
stderr: "pipe",
|
|
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
|
+
|
|
73
|
+
const port = freePort();
|
|
74
|
+
server = Bun.spawn([binary], {
|
|
75
|
+
cwd: workdir,
|
|
76
|
+
env: { ...process.env, PORT: String(port) },
|
|
77
|
+
stdout: "ignore",
|
|
78
|
+
stderr: "inherit",
|
|
79
|
+
});
|
|
80
|
+
let response: Response | undefined;
|
|
81
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
82
|
+
try {
|
|
83
|
+
response = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
84
|
+
break;
|
|
85
|
+
} catch {
|
|
86
|
+
if (server.exitCode !== null) break;
|
|
87
|
+
await Bun.sleep(100);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
assert(response, "compiled streams handler did not start");
|
|
91
|
+
assert.equal(response.status, 200);
|
|
92
|
+
assert.deepEqual(await response.json(), expected);
|
|
93
|
+
console.log("Native model resolvers preserve lexical function types, plural aliases and schema names");
|
|
94
|
+
} finally {
|
|
95
|
+
if (server) {
|
|
96
|
+
server.kill(9);
|
|
97
|
+
await server.exited;
|
|
98
|
+
}
|
|
99
|
+
await rm(workdir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
@@ -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
|
+
}
|