@sproutboat/runtime 0.21.0 → 0.22.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 +34 -0
- package/LTX_REPLICATION.md +124 -0
- package/README.md +7 -0
- package/package.json +2 -2
- package/src/native-fetch-prelude.js +38 -22
- package/src/transport-broker.js +5 -0
- package/src/transport-embedded.js +1082 -4
- package/test/native-ltx.ts +359 -0
- package/test/native-service-bindings.ts +39 -2
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, readdir, rm, writeFile, stat } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
|
|
7
|
+
// Compile the exact inline C used by the embedded transport against SQLite.
|
|
8
|
+
// This tests WAL bytes, crash boundaries and fsync paths without a JS mock.
|
|
9
|
+
const transport = await readFile(new URL("../src/transport-embedded.js", import.meta.url), "utf8");
|
|
10
|
+
const start = transport.indexOf("// BEGIN LTX NATIVE");
|
|
11
|
+
const end = transport.indexOf("// END LTX NATIVE", start);
|
|
12
|
+
assert(start > 0 && end > start);
|
|
13
|
+
const work = await mkdtemp(join(tmpdir(), "sb-native-ltx-"));
|
|
14
|
+
const binary = join(work, "driver");
|
|
15
|
+
const source = `
|
|
16
|
+
#include <sqlite3.h>
|
|
17
|
+
#include <stdio.h>
|
|
18
|
+
#include <stdlib.h>
|
|
19
|
+
#include <stdint.h>
|
|
20
|
+
#include <string.h>
|
|
21
|
+
#include <strings.h>
|
|
22
|
+
#include <unistd.h>
|
|
23
|
+
#include <fcntl.h>
|
|
24
|
+
#include <sys/stat.h>
|
|
25
|
+
#include <time.h>
|
|
26
|
+
#include <signal.h>
|
|
27
|
+
#define SB_MAX_DB 16
|
|
28
|
+
${transport.slice(start, end)}
|
|
29
|
+
int main(int argc,char **argv) {
|
|
30
|
+
if(argc!=2)return 2;sb_ltx_compat=1;if(sb_ltx_register())return 3;
|
|
31
|
+
char lockpath[1024];snprintf(lockpath,sizeof(lockpath),"%s.ltx.lock",argv[1]);
|
|
32
|
+
int lock=open(lockpath,O_RDWR|O_CREAT,0600);if(lock<0||flock(lock,LOCK_EX|LOCK_NB))return 4;
|
|
33
|
+
sqlite3 *db=0;if(sqlite3_open_v2(argv[1],&db,6,"sb-ltx-bounded"))return 5;
|
|
34
|
+
if(getenv("SB_LTX_TEST_AUTO_VACUUM"))sqlite3_exec(db,"PRAGMA auto_vacuum=INCREMENTAL;",0,0,0);
|
|
35
|
+
if(sqlite3_exec(db,"PRAGMA journal_mode=WAL;PRAGMA cache_size=-2000;",0,0,0)||sb_ltx_start(0,db,argv[1],lock))return 6;
|
|
36
|
+
puts("ready");fflush(stdout);sb_ltx_test_armed=1;
|
|
37
|
+
char *line=0;size_t cap=0;
|
|
38
|
+
while(getline(&line,&cap,stdin)>0) {
|
|
39
|
+
if(strncmp(line,":break-position",15)==0){char p[1024];snprintf(p,sizeof(p),"%s.ltx/position.tmp",argv[1]);mkdir(p,0700);puts("0 injected");fflush(stdout);continue;}
|
|
40
|
+
char *error=0;int rc=sb_ltx_states[0].failed?10:sqlite3_exec(db,line,0,0,&error);
|
|
41
|
+
if(sb_ltx_after(0))rc=10;
|
|
42
|
+
printf("%d %s\\n",rc,error?error:sqlite3_errmsg(db));fflush(stdout);sqlite3_free(error);
|
|
43
|
+
}
|
|
44
|
+
free(line);sqlite3_close(db);close(lock);return 0;
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
await writeFile(join(work, "driver.c"), source);
|
|
48
|
+
await rm(binary, { force: true });
|
|
49
|
+
const compile = Bun.spawnSync(["cc", "-O2", "-DSB_LTX_TESTING", join(work, "driver.c"), "-lsqlite3", "-o", binary]);
|
|
50
|
+
assert.equal(compile.exitCode, 0, compile.stderr.toString());
|
|
51
|
+
|
|
52
|
+
// Independent CRC baseline in Bun. This intentionally uses BigInt, unlike C.
|
|
53
|
+
const table: bigint[] = [];
|
|
54
|
+
for (let i = 0; i < 256; i++) {
|
|
55
|
+
let c = BigInt(i);
|
|
56
|
+
for (let j = 0; j < 8; j++) c = (c >> 1n) ^ (c & 1n ? 0xd800000000000000n : 0n);
|
|
57
|
+
table.push(c);
|
|
58
|
+
}
|
|
59
|
+
function crc(data: Uint8Array, init = 0xffffffffffffffffn) {
|
|
60
|
+
let c = init;
|
|
61
|
+
for (const b of data) c = table[Number((c ^ BigInt(b)) & 255n)] ^ (c >> 8n);
|
|
62
|
+
return c;
|
|
63
|
+
}
|
|
64
|
+
const flag = 0x8000000000000000n,
|
|
65
|
+
mask = 0xffffffffffffffffn;
|
|
66
|
+
function pageCrc(pgno: number, page: Uint8Array) {
|
|
67
|
+
const p = new Uint8Array(4);
|
|
68
|
+
new DataView(p.buffer).setUint32(0, pgno);
|
|
69
|
+
return flag | (crc(page, crc(p)) ^ mask);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function restore(epoch: string, output: string) {
|
|
73
|
+
const files = (await readdir(epoch)).filter((n) => n.endsWith(".ltx")).sort();
|
|
74
|
+
let db = new Uint8Array(0),
|
|
75
|
+
checksum = 0n,
|
|
76
|
+
txid = 0n;
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const b = new Uint8Array(await readFile(join(epoch, file)));
|
|
79
|
+
const v = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
80
|
+
assert.equal(new TextDecoder().decode(b.subarray(0, 4)), "LTX1");
|
|
81
|
+
const pageSize = v.getUint32(8),
|
|
82
|
+
commit = v.getUint32(12);
|
|
83
|
+
assert.equal(v.getBigUint64(16), txid + 1n);
|
|
84
|
+
assert.equal(v.getBigUint64(40), checksum);
|
|
85
|
+
const next = new Uint8Array(commit * pageSize);
|
|
86
|
+
next.set(db.subarray(0, next.length));
|
|
87
|
+
db = next;
|
|
88
|
+
let offset = 100,
|
|
89
|
+
hash = crc(b.subarray(0, 100));
|
|
90
|
+
const entries = [];
|
|
91
|
+
for (;;) {
|
|
92
|
+
const begin = offset,
|
|
93
|
+
pgno = v.getUint32(offset),
|
|
94
|
+
flags = v.getUint16(offset + 4);
|
|
95
|
+
hash = crc(b.subarray(offset, offset + 6), hash);
|
|
96
|
+
offset += 6;
|
|
97
|
+
if (!pgno) {
|
|
98
|
+
assert.equal(flags, 0);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
assert.equal(flags, 1);
|
|
102
|
+
assert(pgno <= commit);
|
|
103
|
+
const size = v.getUint32(offset);
|
|
104
|
+
hash = crc(b.subarray(offset, offset + 4), hash);
|
|
105
|
+
offset += 4;
|
|
106
|
+
const stop = offset + size;
|
|
107
|
+
assert(stop <= b.length);
|
|
108
|
+
assert.equal(b[offset++], 0xf0);
|
|
109
|
+
let literals = 15,
|
|
110
|
+
byte;
|
|
111
|
+
do {
|
|
112
|
+
byte = b[offset++];
|
|
113
|
+
literals += byte;
|
|
114
|
+
} while (byte === 255);
|
|
115
|
+
assert.equal(literals, pageSize);
|
|
116
|
+
assert.equal(offset + pageSize, stop);
|
|
117
|
+
const page = b.subarray(offset, stop);
|
|
118
|
+
db.set(page, (pgno - 1) * pageSize);
|
|
119
|
+
hash = crc(page, hash);
|
|
120
|
+
offset = stop;
|
|
121
|
+
entries.push({ pgno, offset: begin, size: stop - begin });
|
|
122
|
+
}
|
|
123
|
+
const indexStart = offset;
|
|
124
|
+
function varint() {
|
|
125
|
+
let n = 0n,
|
|
126
|
+
s = 0n;
|
|
127
|
+
for (let i = 0; i < 10; i++) {
|
|
128
|
+
assert(offset < b.length);
|
|
129
|
+
const c = b[offset++];
|
|
130
|
+
n |= BigInt(c & 127) << s;
|
|
131
|
+
if (c < 128) return Number(n);
|
|
132
|
+
s += 7n;
|
|
133
|
+
}
|
|
134
|
+
throw new Error("varint overflow");
|
|
135
|
+
}
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
assert.equal(varint(), entry.pgno);
|
|
138
|
+
assert.equal(varint(), entry.offset);
|
|
139
|
+
assert.equal(varint(), entry.size);
|
|
140
|
+
}
|
|
141
|
+
assert.equal(varint(), 0);
|
|
142
|
+
assert.equal(v.getBigUint64(offset), BigInt(offset - indexStart));
|
|
143
|
+
offset += 8;
|
|
144
|
+
hash = crc(b.subarray(indexStart, offset + 8), hash);
|
|
145
|
+
checksum = flag;
|
|
146
|
+
for (let p = 1; p <= commit; p++)
|
|
147
|
+
checksum = flag | (checksum ^ pageCrc(p, db.subarray((p - 1) * pageSize, p * pageSize)));
|
|
148
|
+
assert.equal(v.getBigUint64(offset), checksum, file);
|
|
149
|
+
assert.equal(v.getBigUint64(offset + 8), flag | (hash ^ mask), file);
|
|
150
|
+
assert.equal(offset + 16, b.length);
|
|
151
|
+
txid = v.getBigUint64(24);
|
|
152
|
+
}
|
|
153
|
+
await writeFile(output, db);
|
|
154
|
+
return { files: files.length, txid };
|
|
155
|
+
}
|
|
156
|
+
async function run(path: string, sql: string, extra: Record<string, string> = {}) {
|
|
157
|
+
const child = Bun.spawn([binary, path], {
|
|
158
|
+
env: { ...process.env, ...extra },
|
|
159
|
+
stdin: "pipe",
|
|
160
|
+
stdout: "pipe",
|
|
161
|
+
stderr: "pipe",
|
|
162
|
+
});
|
|
163
|
+
child.stdin.write(sql);
|
|
164
|
+
child.stdin.end();
|
|
165
|
+
const [code, out, error] = await Promise.all([
|
|
166
|
+
child.exited,
|
|
167
|
+
new Response(child.stdout).text(),
|
|
168
|
+
new Response(child.stderr).text(),
|
|
169
|
+
]);
|
|
170
|
+
assert.equal(code, 0, error);
|
|
171
|
+
assert(out.startsWith("ready\n"), out);
|
|
172
|
+
return out.split("\n").slice(1).filter(Boolean);
|
|
173
|
+
}
|
|
174
|
+
async function stoppedCommit(path: string, phase: string, count = 1) {
|
|
175
|
+
const child = Bun.spawn([binary, path], {
|
|
176
|
+
env: { ...process.env, SB_LTX_TEST_STOP: phase, SB_LTX_TEST_COMMIT: String(count) },
|
|
177
|
+
stdin: "pipe",
|
|
178
|
+
stdout: "pipe",
|
|
179
|
+
stderr: "pipe",
|
|
180
|
+
});
|
|
181
|
+
const reader = child.stdout.getReader();
|
|
182
|
+
let text = "";
|
|
183
|
+
async function line() {
|
|
184
|
+
while (!text.includes("\n")) {
|
|
185
|
+
const r = await reader.read();
|
|
186
|
+
if (r.done) throw new Error("driver exited before ready: " + (await new Response(child.stderr).text()));
|
|
187
|
+
text += new TextDecoder().decode(r.value);
|
|
188
|
+
}
|
|
189
|
+
const n = text.indexOf("\n");
|
|
190
|
+
const value = text.slice(0, n);
|
|
191
|
+
text = text.slice(n + 1);
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
assert.equal(await line(), "ready");
|
|
195
|
+
const position = await readFile(path + ".ltx/position", "utf8");
|
|
196
|
+
const epoch = position.split("\n")[1];
|
|
197
|
+
const sql =
|
|
198
|
+
count === 2
|
|
199
|
+
? "INSERT INTO events VALUES(1,'one'); INSERT INTO events VALUES(2,'two');\n"
|
|
200
|
+
: "INSERT INTO events VALUES(1,'one');\n";
|
|
201
|
+
child.stdin.write(sql);
|
|
202
|
+
child.stdin.flush();
|
|
203
|
+
let stopped = false;
|
|
204
|
+
for (let i = 0; i < 200; i++) {
|
|
205
|
+
const state = Bun.spawnSync(["ps", "-o", "state=", "-p", String(child.pid)]).stdout.toString();
|
|
206
|
+
if (state.includes("T")) {
|
|
207
|
+
stopped = true;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (child.exitCode !== null) break;
|
|
211
|
+
await Bun.sleep(10);
|
|
212
|
+
}
|
|
213
|
+
child.kill("SIGKILL");
|
|
214
|
+
await child.exited;
|
|
215
|
+
assert(stopped, "did not reach crash boundary " + phase);
|
|
216
|
+
return epoch;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const path = join(work, "test.sqlite");
|
|
221
|
+
const sql =
|
|
222
|
+
"CREATE TABLE t(id INTEGER PRIMARY KEY,value TEXT);\n" +
|
|
223
|
+
Array.from({ length: 100 }, (_, i) => `INSERT INTO t VALUES(${i + 1},'row-${i + 1}');`).join("\n") +
|
|
224
|
+
"\n";
|
|
225
|
+
const replies = await run(path, sql);
|
|
226
|
+
assert.equal(replies.length, 101);
|
|
227
|
+
assert(
|
|
228
|
+
replies.every((n) => n.startsWith("0 ")),
|
|
229
|
+
replies.join("\n"),
|
|
230
|
+
);
|
|
231
|
+
const root = path + ".ltx";
|
|
232
|
+
const epoch = (await readdir(root)).find((n) => n.startsWith("epoch-"));
|
|
233
|
+
assert(epoch);
|
|
234
|
+
const restored = join(work, "restored.sqlite");
|
|
235
|
+
const result = await restore(join(root, epoch), restored);
|
|
236
|
+
assert.equal(result.files, 102);
|
|
237
|
+
assert.deepEqual(await readFile(restored), await readFile(path));
|
|
238
|
+
const db = new Database(restored);
|
|
239
|
+
assert.equal(db.query("PRAGMA integrity_check").get()?.integrity_check, "ok");
|
|
240
|
+
db.close();
|
|
241
|
+
const cleanPosition = await readFile(root + "/position", "utf8");
|
|
242
|
+
assert(cleanPosition.split("\n")[2].startsWith("C "));
|
|
243
|
+
const finishedBefore = await readdir(join(root, epoch));
|
|
244
|
+
for (let n = 0; n < 5; n++) {
|
|
245
|
+
await run(path, "SELECT 1;\n");
|
|
246
|
+
assert.equal(await readFile(root + "/position", "utf8"), cleanPosition, "idle restart must not rewrite position");
|
|
247
|
+
assert.deepEqual(await readdir(join(root, epoch)), finishedBefore, "idle restart must not rewrite a base");
|
|
248
|
+
assert.equal((await readdir(root)).filter((n) => n.startsWith("epoch-")).length, 1);
|
|
249
|
+
}
|
|
250
|
+
// Size alone is not enough: an offline committed change must invalidate a clean marker.
|
|
251
|
+
const offline = new Database(path);
|
|
252
|
+
offline.exec("UPDATE t SET value='changed-offline' WHERE id=1");
|
|
253
|
+
offline.close();
|
|
254
|
+
await run(path, "SELECT 1;\n");
|
|
255
|
+
const offlineEpoch = (await readFile(root + "/position", "utf8")).split("\n")[1];
|
|
256
|
+
assert.notEqual(offlineEpoch, epoch);
|
|
257
|
+
const offlineRestore = join(work, "offline.sqlite");
|
|
258
|
+
await restore(join(root, offlineEpoch), offlineRestore);
|
|
259
|
+
assert.deepEqual(await readFile(offlineRestore), await readFile(path));
|
|
260
|
+
const bounded = join(work, "bounded.sqlite");
|
|
261
|
+
const boundedReplies = await run(
|
|
262
|
+
bounded,
|
|
263
|
+
"CREATE TABLE t(v BLOB);\nINSERT INTO t VALUES(zeroblob(1000000));\nINSERT INTO t VALUES('small');\n",
|
|
264
|
+
{ SB_LTX_MAX_WAL_BYTES: "131072" },
|
|
265
|
+
);
|
|
266
|
+
assert(boundedReplies[1].startsWith("13 "), boundedReplies.join("\n"));
|
|
267
|
+
assert(boundedReplies[2].startsWith("0 "), boundedReplies.join("\n"));
|
|
268
|
+
const live = new Database(bounded);
|
|
269
|
+
assert.equal(live.query("SELECT count(*) n FROM t").get()?.n, 1);
|
|
270
|
+
assert.equal(live.query("PRAGMA integrity_check").get()?.integrity_check, "ok");
|
|
271
|
+
live.close();
|
|
272
|
+
const walStat = await stat(bounded + "-wal").catch(() => null);
|
|
273
|
+
assert(!walStat || walStat.size <= 131072);
|
|
274
|
+
const shrinking = join(work, "shrinking.sqlite");
|
|
275
|
+
const shrinkReplies = await run(
|
|
276
|
+
shrinking,
|
|
277
|
+
"CREATE TABLE t(v BLOB);\nBEGIN; INSERT INTO t VALUES(zeroblob(500000)); COMMIT;\nBEGIN; INSERT INTO t VALUES('rolled-back'); ROLLBACK;\nDELETE FROM t;\nPRAGMA incremental_vacuum(10000);\n",
|
|
278
|
+
{ SB_LTX_TEST_AUTO_VACUUM: "1" },
|
|
279
|
+
);
|
|
280
|
+
assert(
|
|
281
|
+
shrinkReplies.every((n) => n.startsWith("0 ")),
|
|
282
|
+
shrinkReplies.join("\n"),
|
|
283
|
+
);
|
|
284
|
+
const shrinkEpoch = (await readFile(shrinking + ".ltx/position", "utf8")).split("\n")[1];
|
|
285
|
+
const shrinkRestore = join(work, "shrinking-restored.sqlite");
|
|
286
|
+
await restore(shrinking + ".ltx/" + shrinkEpoch, shrinkRestore);
|
|
287
|
+
assert.deepEqual(await readFile(shrinking), await readFile(shrinkRestore));
|
|
288
|
+
assert((await stat(shrinking)).size < 32768, "incremental vacuum should shrink the file");
|
|
289
|
+
const failure = join(work, "failure.sqlite");
|
|
290
|
+
const failed = await run(
|
|
291
|
+
failure,
|
|
292
|
+
"CREATE TABLE events(id INTEGER PRIMARY KEY,value TEXT);\n:break-position\nINSERT INTO events VALUES(1,'committed-before-error');\nINSERT INTO events VALUES(2,'must-be-rejected');\n",
|
|
293
|
+
);
|
|
294
|
+
assert(failed[2].startsWith("10 ") && failed[3].startsWith("10 "), failed.join("\n"));
|
|
295
|
+
await rm(failure + ".ltx/position.tmp", { recursive: true });
|
|
296
|
+
await run(failure, "SELECT 1;\n");
|
|
297
|
+
const failureEpoch = (await readFile(failure + ".ltx/position", "utf8")).split("\n")[1];
|
|
298
|
+
const failureRestore = join(work, "failure-restored.sqlite");
|
|
299
|
+
await restore(failure + ".ltx/" + failureEpoch, failureRestore);
|
|
300
|
+
const failedDb = new Database(failureRestore);
|
|
301
|
+
assert.equal(failedDb.query("SELECT count(*) n FROM events").get()?.n, 1);
|
|
302
|
+
failedDb.close();
|
|
303
|
+
const phases = [
|
|
304
|
+
"wal-committed",
|
|
305
|
+
"ltx-written",
|
|
306
|
+
"ltx-fsynced",
|
|
307
|
+
"ltx-published",
|
|
308
|
+
"position-fsynced",
|
|
309
|
+
"position-published",
|
|
310
|
+
"before-checkpoint",
|
|
311
|
+
"after-checkpoint",
|
|
312
|
+
"checkpoint-record-fsynced",
|
|
313
|
+
"checkpoint-record-published",
|
|
314
|
+
];
|
|
315
|
+
for (const phase of phases) {
|
|
316
|
+
const crashPath = join(work, "crash-" + phase + ".sqlite");
|
|
317
|
+
await run(crashPath, "CREATE TABLE events(id INTEGER PRIMARY KEY,value TEXT);\n");
|
|
318
|
+
const epoch = await stoppedCommit(crashPath, phase);
|
|
319
|
+
const before = (await readdir(crashPath + ".ltx/" + epoch)).filter((n) => n.endsWith(".ltx")).length;
|
|
320
|
+
const oldRestore = join(work, "old-" + phase + ".sqlite");
|
|
321
|
+
await restore(crashPath + ".ltx/" + epoch, oldRestore);
|
|
322
|
+
const beforeDb = new Database(oldRestore);
|
|
323
|
+
assert.equal(beforeDb.query("PRAGMA integrity_check").get()?.integrity_check, "ok");
|
|
324
|
+
const durableRows = beforeDb.query("SELECT count(*) n FROM events").get()?.n;
|
|
325
|
+
beforeDb.close();
|
|
326
|
+
assert.equal(durableRows, before > 2 ? 1 : 0);
|
|
327
|
+
await run(crashPath, "SELECT 1;\n");
|
|
328
|
+
const position = (await readFile(crashPath + ".ltx/position", "utf8")).split("\n");
|
|
329
|
+
if (phase === "position-published" || phase === "before-checkpoint" || phase === "checkpoint-record-published")
|
|
330
|
+
assert.equal(position[1], epoch, "matching WAL must resume");
|
|
331
|
+
else assert.notEqual(position[1], epoch, "mismatched position must make a fresh base");
|
|
332
|
+
const recovered = join(work, "recovered-" + phase + ".sqlite");
|
|
333
|
+
await restore(crashPath + ".ltx/" + position[1], recovered);
|
|
334
|
+
const rdb = new Database(recovered);
|
|
335
|
+
assert.equal(rdb.query("SELECT count(*) n FROM events").get()?.n, 1);
|
|
336
|
+
assert.equal(rdb.query("PRAGMA integrity_check").get()?.integrity_check, "ok");
|
|
337
|
+
rdb.close();
|
|
338
|
+
}
|
|
339
|
+
const pending = join(work, "pending.sqlite");
|
|
340
|
+
await run(pending, "CREATE TABLE events(id INTEGER PRIMARY KEY,value TEXT);\n");
|
|
341
|
+
const pendingEpoch = await stoppedCommit(pending, "wal-committed", 2);
|
|
342
|
+
await run(pending, "SELECT 1;\n");
|
|
343
|
+
assert.equal((await readFile(pending + ".ltx/position", "utf8")).split("\n")[1], pendingEpoch);
|
|
344
|
+
const pendingRestore = join(work, "pending-restored.sqlite");
|
|
345
|
+
const pendingResult = await restore(pending + ".ltx/" + pendingEpoch, pendingRestore);
|
|
346
|
+
assert.equal(pendingResult.files, 4); // Base, schema commit and both recovered inserts.
|
|
347
|
+
const pdb = new Database(pendingRestore);
|
|
348
|
+
assert.equal(pdb.query("SELECT count(*) n FROM events").get()?.n, 2);
|
|
349
|
+
assert.equal(pdb.query("PRAGMA integrity_check").get()?.integrity_check, "ok");
|
|
350
|
+
pdb.close();
|
|
351
|
+
console.log(
|
|
352
|
+
"PASS restart: 10 SIGKILL durability boundaries, matching-prefix resume, orphan/missing/reset fallback, pending committed WAL tail captured before serving",
|
|
353
|
+
);
|
|
354
|
+
console.log(
|
|
355
|
+
"PASS native LTX: 102-file chain, independent CRC/index checks, byte-exact restore, integrity, hard WAL ceiling and oversized transaction rollback",
|
|
356
|
+
);
|
|
357
|
+
} finally {
|
|
358
|
+
await rm(work, { recursive: true, force: true });
|
|
359
|
+
}
|
|
@@ -239,6 +239,7 @@ const broker = createBroker({
|
|
|
239
239
|
});
|
|
240
240
|
const brokerServer = listen(broker, "127.0.0.1", 0);
|
|
241
241
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
242
|
+
let legacy: ReturnType<typeof Bun.spawn> | undefined;
|
|
242
243
|
const get = async (path: string) => {
|
|
243
244
|
const t0 = performance.now();
|
|
244
245
|
const res = await fetch(`http://127.0.0.1:${port}${path}`, { signal: AbortSignal.timeout(20_000) });
|
|
@@ -356,7 +357,7 @@ try {
|
|
|
356
357
|
assert.equal(publicBoom.status, 500);
|
|
357
358
|
assert.equal(publicBoom.headers.get("x-sb-uncaught"), null);
|
|
358
359
|
|
|
359
|
-
// Streamed responses (compatibility date 2026-10-
|
|
360
|
+
// Streamed responses (compatibility date 2026-10-22 on): the call resolves on the
|
|
360
361
|
// head, the first chunk arrives before the rest, and a 12 MiB body arrives intact.
|
|
361
362
|
const streamed = await get("/streamed");
|
|
362
363
|
assert.equal(streamed.status, 200, streamed.text);
|
|
@@ -380,15 +381,51 @@ try {
|
|
|
380
381
|
// A deployment calling itself, 30 levels deep: every level waits on the next at once.
|
|
381
382
|
const self = await get("/self");
|
|
382
383
|
assert.equal(self.text, "bottom 30");
|
|
384
|
+
// Before compatibility date 2026-10-22 a binding call answers at once, as it always
|
|
385
|
+
// did: a handler that uses the Response without awaiting it keeps working.
|
|
386
|
+
const legacyHandler = `
|
|
387
|
+
export default {
|
|
388
|
+
fetch(request, env) {
|
|
389
|
+
const res = env.PEER.fetch("https://peer/echo?legacy=1");
|
|
390
|
+
return new Response(String(res instanceof Response) + ":" + res.status + ":" + res.json().path);
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
`;
|
|
394
|
+
const legacyPort = freePort();
|
|
395
|
+
const legacySource = join(workdir, "legacy.generated.js");
|
|
396
|
+
const legacyBinary = join(workdir, "legacy-native");
|
|
397
|
+
await writeFile(legacySource, wrapNativeFetchHandler(legacyHandler, core.replace(TRANSPORT_MARKER, transport), {}, bindings, undefined, "2026-10-21"));
|
|
398
|
+
const legacyCompile = Bun.spawnSync([node, resolve(porffor, "runtime/index.js"), "native", legacySource, "-o", legacyBinary, "-s", "-O0"], { cwd: workdir });
|
|
399
|
+
assert.equal(legacyCompile.exitCode, 0, `legacy compile failed:\n${legacyCompile.stderr}`);
|
|
400
|
+
legacy = Bun.spawn([legacyBinary], {
|
|
401
|
+
cwd: workdir,
|
|
402
|
+
env: { ...process.env, PORT: String(legacyPort), SB_BROKER_PORT: String(brokerServer.port), SB_BROKER_TOKEN: "native-services" },
|
|
403
|
+
stdout: "ignore",
|
|
404
|
+
stderr: "inherit",
|
|
405
|
+
});
|
|
406
|
+
let legacyText = "";
|
|
407
|
+
for (let attempt = 0; attempt < 50 && !legacyText; attempt++) {
|
|
408
|
+
try {
|
|
409
|
+
legacyText = await (await fetch(`http://127.0.0.1:${legacyPort}/`, { signal: AbortSignal.timeout(20_000) })).text();
|
|
410
|
+
} catch {
|
|
411
|
+
await Bun.sleep(100);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
assert.equal(legacyText, "true:200:/echo?legacy=1", "before 2026-10-22 a binding call answers synchronously");
|
|
415
|
+
|
|
383
416
|
console.log(
|
|
384
417
|
`Service bindings return promises and run off the loop: /ping ${Math.round(ping.ms)} ms during a call, ` +
|
|
385
|
-
`8x500 ms in ${Math.round(total)} ms, self-recursion ok; URL, Request+init, stream bodies, Set-Cookie and missing content types preserved; redirects follow Fetch; aborted calls stop upstream; vouched calls see the caller's URL and rethrow the callee's errors; responses stream; RPC calls WorkerEntrypoint methods`,
|
|
418
|
+
`8x500 ms in ${Math.round(total)} ms, self-recursion ok; URL, Request+init, stream bodies, Set-Cookie and missing content types preserved; redirects follow Fetch; aborted calls stop upstream; before 2026-10-22 calls answer at once; vouched calls see the caller's URL and rethrow the callee's errors; responses stream; RPC calls WorkerEntrypoint methods`,
|
|
386
419
|
);
|
|
387
420
|
} finally {
|
|
388
421
|
if (server) {
|
|
389
422
|
server.kill(9);
|
|
390
423
|
await server.exited;
|
|
391
424
|
}
|
|
425
|
+
if (legacy) {
|
|
426
|
+
legacy.kill(9);
|
|
427
|
+
await legacy.exited;
|
|
428
|
+
}
|
|
392
429
|
brokerServer.stop();
|
|
393
430
|
broker.close();
|
|
394
431
|
edge.stop(true);
|