@sproutboat/runtime 0.34.4 → 0.35.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 +23 -0
- package/package.json +1 -1
- package/src/d1-shim.test.js +120 -16
- package/src/native-fetch-prelude.js +198 -80
- package/src/transport-embedded.js +159 -42
- package/test/native-d1.ts +38 -8
- package/test/native-headers.ts +182 -0
- package/test/native-sqlite-lock.ts +94 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
// A standalone binary (the embedded transport, built by the CLI) opens its SQLite databases in
|
|
8
|
+
// EXCLUSIVE locking mode by default; SB_SQLITE_EXCLUSIVE=0 opts out and =1 is the default. The
|
|
9
|
+
// transport on its own, as sproutd and the broker embed it, stays shared unless the variable is
|
|
10
|
+
// set: native-d1.ts covers that. Needs SB_CLI, a sproutboat-cli checkout linked to this one.
|
|
11
|
+
const handler = `export default {
|
|
12
|
+
async fetch() {
|
|
13
|
+
await env.DB.prepare("CREATE TABLE IF NOT EXISTS t (v INTEGER)").run();
|
|
14
|
+
const row = await env.DB.prepare("PRAGMA locking_mode").first();
|
|
15
|
+
return Response.json({ mode: row.locking_mode });
|
|
16
|
+
},
|
|
17
|
+
};`;
|
|
18
|
+
|
|
19
|
+
const cli = process.env.SB_CLI;
|
|
20
|
+
if (!cli) {
|
|
21
|
+
console.log("skipped: set SB_CLI to a sproutboat-cli checkout linked to this checkout (it builds the sprout)");
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function freePort(): number {
|
|
26
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
27
|
+
const port = listener.port;
|
|
28
|
+
listener.stop(true);
|
|
29
|
+
return port;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const workdir = mkdtempSync(join(tmpdir(), "sb-native-sqlite-lock-"));
|
|
33
|
+
const servers: ReturnType<typeof Bun.spawn>[] = [];
|
|
34
|
+
try {
|
|
35
|
+
const project = join(workdir, "lock");
|
|
36
|
+
mkdirSync(join(project, "src"), { recursive: true });
|
|
37
|
+
writeFileSync(join(project, "src/index.js"), handler);
|
|
38
|
+
writeFileSync(
|
|
39
|
+
join(project, "sproutboat.jsonc"),
|
|
40
|
+
JSON.stringify({ name: "lock", main: "src/index.js", compatibility_date: "2026-10-28", d1_databases: ["DB"] }),
|
|
41
|
+
);
|
|
42
|
+
const built = Bun.spawnSync(["bun", join(cli, "src/main.ts"), "build", "--standalone", "--target", "host"], {
|
|
43
|
+
cwd: project,
|
|
44
|
+
stdout: "inherit",
|
|
45
|
+
stderr: "inherit",
|
|
46
|
+
});
|
|
47
|
+
assert.equal(built.exitCode, 0, "the CLI could not build the sprout");
|
|
48
|
+
|
|
49
|
+
const run = async (label: string, setting: string | undefined, expected: string) => {
|
|
50
|
+
const data = join(workdir, `data-${label}`);
|
|
51
|
+
const base = Object.fromEntries(Object.entries(process.env).filter(([key]) => key !== "SB_SQLITE_EXCLUSIVE"));
|
|
52
|
+
const env = setting === undefined ? base : { ...base, SB_SQLITE_EXCLUSIVE: setting };
|
|
53
|
+
const port = freePort();
|
|
54
|
+
const server = Bun.spawn([join(project, "dist/lock")], {
|
|
55
|
+
cwd: project,
|
|
56
|
+
env: { ...env, SB_DATA_DIR: data, PORT: String(port) },
|
|
57
|
+
stdout: "ignore",
|
|
58
|
+
stderr: "inherit",
|
|
59
|
+
});
|
|
60
|
+
servers.push(server);
|
|
61
|
+
let response: Response | undefined;
|
|
62
|
+
for (let attempt = 0; attempt < 100 && !response; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
response = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(2000) });
|
|
65
|
+
} catch {
|
|
66
|
+
if (server.exitCode !== null) break;
|
|
67
|
+
await Bun.sleep(100);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
assert(response, `${label}: the compiled handler did not start`);
|
|
71
|
+
assert.deepEqual(await response.json(), { mode: expected }, label);
|
|
72
|
+
// Another process can open the database while the binary runs only in shared mode.
|
|
73
|
+
let other = "";
|
|
74
|
+
try {
|
|
75
|
+
const db = new Database(join(data, "d1/DB.sqlite"), { readonly: true });
|
|
76
|
+
other = String(db.query("SELECT count(*) AS n FROM t").get()?.n);
|
|
77
|
+
db.close();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
other = String(error);
|
|
80
|
+
}
|
|
81
|
+
if (expected === "exclusive")
|
|
82
|
+
assert.match(other, /locked|busy/i, `${label}: a second opener is locked out (${other})`);
|
|
83
|
+
else assert.equal(other, "0", `${label}: a second opener can read`);
|
|
84
|
+
server.kill(9);
|
|
85
|
+
await server.exited;
|
|
86
|
+
};
|
|
87
|
+
await run("default", undefined, "exclusive");
|
|
88
|
+
await run("one", "1", "exclusive");
|
|
89
|
+
await run("zero", "0", "normal");
|
|
90
|
+
console.log("standalone SQLite: EXCLUSIVE by default, SB_SQLITE_EXCLUSIVE=0 opts out");
|
|
91
|
+
} finally {
|
|
92
|
+
for (const server of servers) server.kill(9);
|
|
93
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
94
|
+
}
|