@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.35.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7f5e87b: A standalone binary now opens its SQLite databases in `PRAGMA locking_mode=EXCLUSIVE` by default, with the page cache at 2 MB (`cache_size=-2000`, unchanged). The lock is taken once and held, so a transaction no longer takes and releases file locks around itself, and the WAL index lives in heap memory instead of a shared `-shm` file. The cost: no other process can open the database while the binary runs, so the `sqlite3` shell, `sproutboat kv/d1 --local --data-dir` against a running binary, and the several-copies-behind-`SO_REUSEPORT` recipe do not work on a live binary. Set `SB_SQLITE_EXCLUSIVE=0` to opt out and keep shared locking. A database under LTX replication is never opened exclusive.
|
|
8
|
+
|
|
9
|
+
The default belongs to the standalone binary only: the toolchain's native server shim sets `SB_SQLITE_EXCLUSIVE=1` at startup unless the variable is already set. The embedded transport itself reads only `SB_SQLITE_EXCLUSIVE` (`1` is exclusive, anything else is shared), so sproutd and the broker keep shared locking unless an operator sets it.
|
|
10
|
+
|
|
11
|
+
`SB_SQLITE_CACHE_MB=<n>` (1 to 4096) sets each database's page cache in MiB; unset it stays at 2 MB.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 2c9d885: A strict D1 query in a standalone binary does less around SQLite. It skips the generic dispatch (a request object, a walk down the op list, a result object), a statement is a class instead of an object with five closures, `raw()` and `first()` no longer build the `meta` they throw away (two clock reads), the transaction-keyword check reads character codes instead of compiling a regular expression on every query, and the column names, change count and row id come back as JavaScript values built in C instead of as JSON text that is written, parsed and discarded. Replies, errors and `meta` are unchanged.
|
|
16
|
+
- 004b8e7: A request's fixed cost in a native sprout drops five items. The CPU stamp and the streamed-body probe come back as numbers instead of formatted and parsed text (a `printf` and a `parseFloat` each, per request). The ingress reads of `x-sb-trigger` and `x-sb-remote-addr` scan the stored header names instead of validating and lowercasing a name for each, and the three `x-sb-` names the runtime strips are looked for in one pass. `SB_EDGE_TOKEN` is read once. `Headers` checks a name and a value in C and only falls back to its regular expression for a name it cannot judge there, so `get`, `has`, `set`, `append` and `new Response(body, { headers })` stop compiling that expression on every call. Behavior is unchanged.
|
|
17
|
+
- 4213ed7: A standalone binary stops mapping and unmapping 88 KiB for every statement that builds a temporary table. SQLite allocates 20 pages in one block for each page cache it creates, and `INSERT ... RETURNING` (and any sort that spills to a temporary table) creates one, so each such statement cost an `mmap` and an `munmap`. The runtime now sets the initial page-cache allocation to 0 before SQLite starts, so a cache takes its pages one at a time as it grows. Query results and the main database's cache size are unchanged.
|
|
18
|
+
|
|
19
|
+
## 0.34.5
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- e62b600: The client address is folded to plain IPv4 once per distinct address instead of on every request: a request read `x-sb-remote-addr` and, for an IPv4-mapped IPv6 peer, lowercased, split and hex-parsed it each time. The last address and its result are kept; a different address takes the same path as before.
|
|
24
|
+
- 0b12b94: A strict D1 query hands its rows to JavaScript with fewer copies: the text of every cell goes into one arena instead of one `malloc` per cell, and a cell that is plain ASCII becomes its JS string in one copy (it used to go through a UTF-16 buffer, an 8-bit buffer and a third copy, three `malloc`s each). Non-ASCII text still takes the old path, so the strings are unchanged.
|
|
25
|
+
|
|
3
26
|
## 0.34.4
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/package.json
CHANGED
package/src/d1-shim.test.js
CHANGED
|
@@ -64,7 +64,9 @@ for (const date of ["2026-09-28", "2026-10-24"]) {
|
|
|
64
64
|
test(`${date}: batch() is one transaction`, async () => {
|
|
65
65
|
const DB = fresh(date);
|
|
66
66
|
await DB.exec("CREATE TABLE t (id INTEGER PRIMARY KEY)");
|
|
67
|
-
expect(
|
|
67
|
+
expect(
|
|
68
|
+
await failure(() => DB.batch([DB.prepare("INSERT INTO t VALUES (1)"), DB.prepare("INSERT INTO t VALUES (1)")])),
|
|
69
|
+
).toMatch(/UNIQUE/);
|
|
68
70
|
expect(await DB.prepare("SELECT count(*) AS n FROM t").first("n")).toBe(0);
|
|
69
71
|
});
|
|
70
72
|
}
|
|
@@ -85,13 +87,23 @@ test("before 2026-10-24: synchronous, lenient, no foreign keys, BLOBs as one cha
|
|
|
85
87
|
|
|
86
88
|
test("from 2026-10-24: D1's parameter rules", async () => {
|
|
87
89
|
const DB = fresh("2026-10-24");
|
|
88
|
-
expect(caught(() => DB.prepare("SELECT ?1").bind(undefined))).toBe(
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
expect(caught(() => DB.prepare("SELECT ?1").bind(undefined))).toBe(
|
|
91
|
+
"D1_TYPE_ERROR: Type 'undefined' not supported for value 'undefined'",
|
|
92
|
+
);
|
|
93
|
+
expect(caught(() => DB.prepare("SELECT ?1").bind({ a: 1 }))).toBe(
|
|
94
|
+
"D1_TYPE_ERROR: Type 'object' not supported for value '[object Object]'",
|
|
95
|
+
);
|
|
96
|
+
expect(caught(() => DB.prepare("SELECT ?1").bind(10n))).toBe(
|
|
97
|
+
"D1_TYPE_ERROR: Type 'bigint' not supported for value '10'",
|
|
98
|
+
);
|
|
91
99
|
expect(await failure(() => DB.prepare("SELECT ?1, ?2").bind(1).all())).toBe(WRONG_COUNT);
|
|
92
100
|
expect(await failure(() => DB.prepare("SELECT ?1").bind(1, 2).all())).toBe(WRONG_COUNT);
|
|
93
101
|
expect(await failure(() => DB.prepare("SELECT ?1").all())).toBe(WRONG_COUNT);
|
|
94
|
-
expect(await DB.prepare("SELECT ?1 AS a, ?1 AS b, ?2 AS c").bind("x", true).first()).toEqual({
|
|
102
|
+
expect(await DB.prepare("SELECT ?1 AS a, ?1 AS b, ?2 AS c").bind("x", true).first()).toEqual({
|
|
103
|
+
a: "x",
|
|
104
|
+
b: "x",
|
|
105
|
+
c: 1,
|
|
106
|
+
});
|
|
95
107
|
});
|
|
96
108
|
|
|
97
109
|
test("from 2026-10-24: foreign keys, BLOBs as byte arrays, D1's error text", async () => {
|
|
@@ -104,18 +116,25 @@ test("from 2026-10-24: foreign keys, BLOBs as byte arrays, D1's error text", asy
|
|
|
104
116
|
expect(await failure(() => DB.prepare("INSERT INTO p VALUES (1)").run())).toBe(
|
|
105
117
|
"D1_ERROR: UNIQUE constraint failed: p.id: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_PRIMARYKEY)",
|
|
106
118
|
);
|
|
107
|
-
expect(await failure(() => DB.prepare("SELECT * FROM nope").all())).toBe(
|
|
119
|
+
expect(await failure(() => DB.prepare("SELECT * FROM nope").all())).toBe(
|
|
120
|
+
"D1_ERROR: no such table: nope: SQLITE_ERROR",
|
|
121
|
+
);
|
|
108
122
|
expect(await DB.prepare("SELECT x'00ff' AS b").first("b")).toEqual([0, 255]);
|
|
109
123
|
});
|
|
110
124
|
|
|
111
125
|
test("from 2026-10-24: every query method returns a Promise", async () => {
|
|
112
126
|
const DB = fresh("2026-10-24");
|
|
113
127
|
const s = DB.prepare("SELECT 41 AS a");
|
|
114
|
-
for (const p of [s.all(), s.run(), s.first(), s.raw(), DB.batch([s]), DB.exec("SELECT 1"), DB.dump().catch(() => 0)])
|
|
128
|
+
for (const p of [s.all(), s.run(), s.first(), s.raw(), DB.batch([s]), DB.exec("SELECT 1"), DB.dump().catch(() => 0)])
|
|
129
|
+
expect(p).toBeInstanceOf(Promise);
|
|
115
130
|
expect(await s.first("a").then((a) => a + 1)).toBe(42);
|
|
116
131
|
expect(await Promise.all([s.first("a"), DB.prepare("SELECT 2 AS a").first("a")])).toEqual([41, 2]);
|
|
117
132
|
// Errors reject rather than throw: .catch() sees them.
|
|
118
|
-
expect(
|
|
133
|
+
expect(
|
|
134
|
+
await DB.prepare("SELEC 1")
|
|
135
|
+
.all()
|
|
136
|
+
.catch((e) => e.message),
|
|
137
|
+
).toMatch(/^D1_ERROR: near "SELEC": syntax error/);
|
|
119
138
|
});
|
|
120
139
|
|
|
121
140
|
test("from 2026-10-24: results, meta, raw() and first() as in workerd", async () => {
|
|
@@ -123,16 +142,42 @@ test("from 2026-10-24: results, meta, raw() and first() as in workerd", async ()
|
|
|
123
142
|
await DB.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)");
|
|
124
143
|
const inserted = await DB.prepare("INSERT INTO t (name) VALUES (?), (?)").bind("one", "two").run();
|
|
125
144
|
expect(Object.keys(inserted).sort()).toEqual(["meta", "results", "success"]);
|
|
126
|
-
expect(Object.keys(inserted.meta).sort()).toEqual([
|
|
127
|
-
|
|
145
|
+
expect(Object.keys(inserted.meta).sort()).toEqual([
|
|
146
|
+
"changed_db",
|
|
147
|
+
"changes",
|
|
148
|
+
"duration",
|
|
149
|
+
"last_row_id",
|
|
150
|
+
"rows_read",
|
|
151
|
+
"rows_written",
|
|
152
|
+
"served_by",
|
|
153
|
+
"size_after",
|
|
154
|
+
]);
|
|
155
|
+
expect([
|
|
156
|
+
inserted.meta.changes,
|
|
157
|
+
inserted.meta.last_row_id,
|
|
158
|
+
inserted.meta.changed_db,
|
|
159
|
+
inserted.meta.rows_written,
|
|
160
|
+
]).toEqual([2, 2, true, 2]);
|
|
128
161
|
const read = await DB.prepare("SELECT id, name FROM t ORDER BY id").all();
|
|
129
162
|
expect([read.meta.changes, read.meta.changed_db, read.meta.rows_written]).toEqual([0, false, 0]);
|
|
130
|
-
expect(await DB.prepare("SELECT id, name FROM t ORDER BY id").raw()).toEqual([
|
|
131
|
-
|
|
163
|
+
expect(await DB.prepare("SELECT id, name FROM t ORDER BY id").raw()).toEqual([
|
|
164
|
+
[1, "one"],
|
|
165
|
+
[2, "two"],
|
|
166
|
+
]);
|
|
167
|
+
expect(await DB.prepare("SELECT id, name FROM t ORDER BY id").raw({ columnNames: true })).toEqual([
|
|
168
|
+
["id", "name"],
|
|
169
|
+
[1, "one"],
|
|
170
|
+
[2, "two"],
|
|
171
|
+
]);
|
|
132
172
|
expect(await DB.prepare("SELECT id FROM t WHERE id > 9").raw({ columnNames: true })).toEqual([["id"]]);
|
|
133
|
-
expect(await DB.prepare("SELECT 1 AS a, 2 AS a").raw({ columnNames: true })).toEqual([
|
|
173
|
+
expect(await DB.prepare("SELECT 1 AS a, 2 AS a").raw({ columnNames: true })).toEqual([
|
|
174
|
+
["a", "a"],
|
|
175
|
+
[1, 2],
|
|
176
|
+
]);
|
|
134
177
|
expect((await DB.prepare("SELECT 1 AS a, 2 AS a").all()).results).toEqual([{ a: 2 }]);
|
|
135
|
-
expect(await failure(() => DB.prepare("SELECT id FROM t").first("nope"))).toBe(
|
|
178
|
+
expect(await failure(() => DB.prepare("SELECT id FROM t").first("nope"))).toBe(
|
|
179
|
+
"D1_COLUMN_NOTFOUND: Column not found (nope)",
|
|
180
|
+
);
|
|
136
181
|
expect(await DB.prepare("SELECT id FROM t WHERE id > 9").first("nope")).toBeNull();
|
|
137
182
|
});
|
|
138
183
|
|
|
@@ -140,15 +185,74 @@ test("from 2026-10-24: empty SQL, transactions, exec() and sessions as in worker
|
|
|
140
185
|
const DB = fresh("2026-10-24");
|
|
141
186
|
expect(await failure(() => DB.prepare("").all())).toBe(NO_STATEMENTS);
|
|
142
187
|
expect(await failure(() => DB.batch([]))).toBe(NO_STATEMENTS);
|
|
143
|
-
expect(await failure(() => DB.prepare("BEGIN TRANSACTION").run())).toMatch(
|
|
188
|
+
expect(await failure(() => DB.prepare("BEGIN TRANSACTION").run())).toMatch(
|
|
189
|
+
/^D1_ERROR: To execute a transaction, please use the state\.storage\.transaction\(\)/,
|
|
190
|
+
);
|
|
144
191
|
expect(await failure(() => DB.prepare("savepoint x").run())).toMatch(/^D1_ERROR: To execute a transaction/);
|
|
145
192
|
const done = await DB.exec("CREATE TABLE t (id INTEGER)\nINSERT INTO t VALUES (1);\n\nINSERT INTO t VALUES (2);\n");
|
|
146
193
|
expect([done.count, Number.isFinite(done.duration)]).toEqual([3, true]);
|
|
147
194
|
expect(await DB.exec("SELECT 1").then((r) => r.count)).toBe(1);
|
|
148
|
-
expect(await failure(() => DB.exec("INSERT INTO t VALUES (3);\nSELEC 2"))).toMatch(
|
|
195
|
+
expect(await failure(() => DB.exec("INSERT INTO t VALUES (3);\nSELEC 2"))).toMatch(
|
|
196
|
+
/^D1_EXEC_ERROR: Error in line 2: SELEC 2: near "SELEC": syntax error/,
|
|
197
|
+
);
|
|
149
198
|
expect(await DB.prepare("SELECT count(*) AS n FROM t").first("n")).toBe(2);
|
|
150
199
|
const session = DB.withSession("first-unconstrained");
|
|
151
200
|
expect(await session.prepare("SELECT count(*) AS n FROM t").first("n")).toBe(2);
|
|
152
201
|
expect(session.getBookmark()).toBeNull();
|
|
153
202
|
expect(await session.batch([session.prepare("SELECT 1 AS a")]).then((r) => r[0].results)).toEqual([{ a: 1 }]);
|
|
154
203
|
});
|
|
204
|
+
|
|
205
|
+
// __sbD1IsTransaction replaced `/^(BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(text)`.
|
|
206
|
+
test("a statement is a transaction keyword exactly as the regular expression it replaced said", () => {
|
|
207
|
+
// oxlint-disable-next-line no-function-constructor -- the prelude is prepended as text in production too
|
|
208
|
+
const { isTx } = new Function("Porffor", `${source}\nreturn { isTx: __sbD1IsTransaction };`)({ c: () => "" });
|
|
209
|
+
const old = (text) => /^(BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(text);
|
|
210
|
+
const cases = [
|
|
211
|
+
"BEGIN",
|
|
212
|
+
"begin",
|
|
213
|
+
"Begin TRANSACTION",
|
|
214
|
+
"begin;",
|
|
215
|
+
"BEGIN\n",
|
|
216
|
+
"commit",
|
|
217
|
+
"COMMIT;",
|
|
218
|
+
"end",
|
|
219
|
+
"END TRANSACTION",
|
|
220
|
+
"rollback to x",
|
|
221
|
+
"ROLLBACK",
|
|
222
|
+
"savepoint x",
|
|
223
|
+
"SAVEPOINT\tx",
|
|
224
|
+
"release x",
|
|
225
|
+
"RELEASE",
|
|
226
|
+
"beginning",
|
|
227
|
+
"begin_x",
|
|
228
|
+
"begin1",
|
|
229
|
+
"ending",
|
|
230
|
+
"endx",
|
|
231
|
+
"ender",
|
|
232
|
+
"committed",
|
|
233
|
+
"rollbacks",
|
|
234
|
+
"savepoints",
|
|
235
|
+
"released",
|
|
236
|
+
"select 1",
|
|
237
|
+
"SELECT begin FROM t",
|
|
238
|
+
"insert into t values (1)",
|
|
239
|
+
"be",
|
|
240
|
+
"en",
|
|
241
|
+
"e",
|
|
242
|
+
"",
|
|
243
|
+
"BEGINX",
|
|
244
|
+
"begin-",
|
|
245
|
+
"end(",
|
|
246
|
+
"end.",
|
|
247
|
+
"release@",
|
|
248
|
+
"commité",
|
|
249
|
+
"xbegin",
|
|
250
|
+
" begin",
|
|
251
|
+
"savepoint",
|
|
252
|
+
"savepointx",
|
|
253
|
+
"rollback_",
|
|
254
|
+
"ROLLBACKĀ",
|
|
255
|
+
"bégin",
|
|
256
|
+
];
|
|
257
|
+
for (const text of cases) expect([text, isTx(text)]).toEqual([text, old(text)]);
|
|
258
|
+
});
|
|
@@ -3092,18 +3092,34 @@ __sbStartupMark();
|
|
|
3092
3092
|
// string via the same primitives as __sbEnv / __sbRandomBytes (proven working),
|
|
3093
3093
|
// then parsed — Porffor's number boxing for a bare inline-C assignment is not
|
|
3094
3094
|
// relied on. `__sbEntry` samples it around the handler for per-invocation CPU.
|
|
3095
|
+
// The number comes back as a number: no text and no parse around it.
|
|
3095
3096
|
function __sbCpuMs() {
|
|
3096
|
-
let
|
|
3097
|
+
let ms = 0;
|
|
3097
3098
|
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
3098
3099
|
Porffor.c`
|
|
3099
3100
|
struct timespec __ts;
|
|
3100
3101
|
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &__ts);
|
|
3101
|
-
|
|
3102
|
+
ms = (f64)((double)__ts.tv_sec * 1000.0 + (double)__ts.tv_nsec / 1000000.0);
|
|
3103
|
+
`;
|
|
3104
|
+
return ms;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// The `x-sb-cpu-ms` value for the CPU time since `t0`, as three decimals, floored at zero: one
|
|
3108
|
+
// clock read and one format call in C, where it was a read, a format, a parse and a toFixed.
|
|
3109
|
+
// oxlint-disable-next-line no-unused-vars -- `t0` is read inside the RawC block below, not by JS.
|
|
3110
|
+
function __sbCpuStamp(t0) {
|
|
3111
|
+
let res = "0.000";
|
|
3112
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
3113
|
+
Porffor.c`
|
|
3114
|
+
struct timespec __ts;
|
|
3115
|
+
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &__ts);
|
|
3116
|
+
double __d = ((double)__ts.tv_sec * 1000.0 + (double)__ts.tv_nsec / 1000000.0) - (t0.type == 1 ? (double)t0.val : 0.0);
|
|
3117
|
+
if (!(__d >= 0)) __d = 0;
|
|
3102
3118
|
char __b[32];
|
|
3103
|
-
int __n = snprintf(__b, sizeof(__b), "%.3f",
|
|
3119
|
+
int __n = snprintf(__b, sizeof(__b), "%.3f", __d);
|
|
3104
3120
|
if (__n > 0) res = porf_box((f64)porf_native_fetch_alloc_bytestring(__b, (size_t)__n), 195);
|
|
3105
3121
|
`;
|
|
3106
|
-
return res
|
|
3122
|
+
return res;
|
|
3107
3123
|
}
|
|
3108
3124
|
|
|
3109
3125
|
// #28 — stamp `x-sb-cpu-ms` onto a handler Response. Porffor alpha-10's
|
|
@@ -3114,7 +3130,6 @@ function __sbCpuMs() {
|
|
|
3114
3130
|
// http-sync-v0) with no Set-Cookie to comma-fold; anything else is returned
|
|
3115
3131
|
// untouched and the edge simply omits cpuMs for that request.
|
|
3116
3132
|
function __sbTagCpu(res, t0) {
|
|
3117
|
-
const cpu = __sbCpuMs() - t0;
|
|
3118
3133
|
try {
|
|
3119
3134
|
// A string body and no Set-Cookie, as before. The native writer reads Headers' flat `_entries`,
|
|
3120
3135
|
// so the stamp is appended to the handler's own Response instead of building a second one (a
|
|
@@ -3122,7 +3137,7 @@ function __sbTagCpu(res, t0) {
|
|
|
3122
3137
|
// the handler set itself is replaced by the measured one, as before.
|
|
3123
3138
|
const headers = res && res.headers;
|
|
3124
3139
|
if (headers && __sbIsStr(__sbRawBody(res)) && !headers.has("set-cookie")) {
|
|
3125
|
-
const stamp = (
|
|
3140
|
+
const stamp = __sbCpuStamp(t0);
|
|
3126
3141
|
const entries = headers._entries;
|
|
3127
3142
|
let set = false;
|
|
3128
3143
|
for (let i = 0; i < entries.length; i += 2) {
|
|
@@ -5295,6 +5310,11 @@ function __sbD1Param(value, strict) {
|
|
|
5295
5310
|
// The broker transport leaves it unset and keeps the string hop. (No initializer: the transport is placed ahead of the prelude, and `= null` would run after its assignment.)
|
|
5296
5311
|
// oxlint-disable-next-line no-unassigned-vars -- the embedded transport assigns it; the broker transport does not.
|
|
5297
5312
|
var __sbCallDirect;
|
|
5313
|
+
// The embedded transport also answers a strict D1 query without the generic dispatch around it
|
|
5314
|
+
// (a request object, a walk down the op list, a result object): (db, sql, params) -> the reply
|
|
5315
|
+
// __sbD1Run builds. Unset elsewhere, and the query goes through __sbD1Rpc as before.
|
|
5316
|
+
// oxlint-disable-next-line no-unassigned-vars -- the embedded transport assigns it; the broker transport does not.
|
|
5317
|
+
var __sbD1QueryDirect;
|
|
5298
5318
|
function __sbRpcDirect(op, extra) {
|
|
5299
5319
|
if (!__sbCallDirect) return __sbRpc(op, extra);
|
|
5300
5320
|
const req = { v: 1, id: ++__sbReqId, op };
|
|
@@ -5355,10 +5375,30 @@ function __sbD1Fail(message) {
|
|
|
5355
5375
|
// transaction statements are refused, exec() runs line by line, and withSession() exists.
|
|
5356
5376
|
var __sbD1NoTransactions =
|
|
5357
5377
|
"D1_ERROR: To execute a transaction, please use the state.storage.transaction() or state.storage.transactionSync() APIs instead of the SQL BEGIN TRANSACTION or SAVEPOINT statements. The JavaScript API is safer because it will automatically roll back on exceptions, and because it interacts correctly with Durable Objects' automatic atomic write coalescing.";
|
|
5378
|
+
// A statement's first word is one of the transaction keywords: read by character code, where the
|
|
5379
|
+
// regular expression this replaced was compiled on every query. The word ends at the first character
|
|
5380
|
+
// that is not a letter, digit or underscore (the regular expression's \b after a keyword).
|
|
5381
|
+
function __sbD1IsTransaction(text) {
|
|
5382
|
+
let end = 0;
|
|
5383
|
+
while (end < text.length && end < 10) {
|
|
5384
|
+
const c = text.charCodeAt(end) | 32;
|
|
5385
|
+
if (c < 97 || c > 122) break;
|
|
5386
|
+
end++;
|
|
5387
|
+
}
|
|
5388
|
+
if (end < 3 || end > 9) return false;
|
|
5389
|
+
if (end < text.length) {
|
|
5390
|
+
const c = text.charCodeAt(end);
|
|
5391
|
+
if ((c >= 48 && c <= 57) || (c >= 65 && c <= 90) || c === 95 || (c >= 97 && c <= 122)) return false;
|
|
5392
|
+
}
|
|
5393
|
+
const word = text.slice(0, end).toLowerCase();
|
|
5394
|
+
return (
|
|
5395
|
+
word === "begin" || word === "commit" || word === "end" || word === "rollback" || word === "savepoint" || word === "release"
|
|
5396
|
+
);
|
|
5397
|
+
}
|
|
5358
5398
|
function __sbD1Check(sql) {
|
|
5359
5399
|
const text = String(sql).trim();
|
|
5360
5400
|
if (!text) throw __sbD1Fail("D1_ERROR: No SQL statements detected.");
|
|
5361
|
-
if (
|
|
5401
|
+
if (__sbD1IsTransaction(text)) throw __sbD1Fail(__sbD1NoTransactions);
|
|
5362
5402
|
}
|
|
5363
5403
|
function __sbD1Meta(meta, started) {
|
|
5364
5404
|
const changes = Number(meta.changes) || 0;
|
|
@@ -5394,70 +5434,91 @@ function __sbD1Promise(run) {
|
|
|
5394
5434
|
return Promise.reject(err);
|
|
5395
5435
|
}
|
|
5396
5436
|
}
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
const
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
function stmt(sql, params) {
|
|
5408
|
-
const s = {
|
|
5409
|
-
__sql: sql,
|
|
5410
|
-
__params: params,
|
|
5411
|
-
bind() {
|
|
5412
|
-
const strict = __sbD1Strict();
|
|
5413
|
-
const list = [];
|
|
5414
|
-
for (let i = 0; i < arguments.length; i++) list.push(__sbD1Param(arguments[i], strict));
|
|
5415
|
-
return stmt(sql, list);
|
|
5416
|
-
},
|
|
5417
|
-
all() {
|
|
5418
|
-
if (!__sbD1Strict()) return query(sql, params, false);
|
|
5419
|
-
return __sbD1Promise(() => {
|
|
5420
|
-
const t = query(sql, params, true);
|
|
5421
|
-
return { success: true, meta: t.meta, results: __sbD1Objects(t.columns, t.rows) };
|
|
5422
|
-
});
|
|
5423
|
-
},
|
|
5424
|
-
run() {
|
|
5425
|
-
return s.all();
|
|
5426
|
-
},
|
|
5427
|
-
raw(options) {
|
|
5428
|
-
if (!__sbD1Strict()) {
|
|
5429
|
-
const rows = query(sql, params, false).results;
|
|
5430
|
-
const out = [];
|
|
5431
|
-
for (let i = 0; i < rows.length; i++) {
|
|
5432
|
-
const cols = [];
|
|
5433
|
-
for (const k in rows[i]) cols.push(rows[i][k]);
|
|
5434
|
-
out.push(cols);
|
|
5435
|
-
}
|
|
5436
|
-
return out;
|
|
5437
|
-
}
|
|
5438
|
-
return __sbD1Promise(() => {
|
|
5439
|
-
const t = query(sql, params, true);
|
|
5440
|
-
return options && options.columnNames ? [t.columns].concat(t.rows) : t.rows;
|
|
5441
|
-
});
|
|
5442
|
-
},
|
|
5443
|
-
first(column) {
|
|
5444
|
-
if (!__sbD1Strict()) {
|
|
5445
|
-
const rows = query(sql, params, false).results;
|
|
5446
|
-
if (rows.length === 0) return null;
|
|
5447
|
-
return column == null ? rows[0] : rows[0][column];
|
|
5448
|
-
}
|
|
5449
|
-
return __sbD1Promise(() => {
|
|
5450
|
-
const t = query(sql, params, true);
|
|
5451
|
-
if (column == null) return t.rows.length ? __sbD1Objects(t.columns, [t.rows[0]])[0] : null;
|
|
5452
|
-
const at = t.columns.indexOf(String(column));
|
|
5453
|
-
if (!t.rows.length) return null;
|
|
5454
|
-
if (at < 0) throw __sbD1Fail("D1_COLUMN_NOTFOUND: Column not found (" + column + ")");
|
|
5455
|
-
return t.rows[0][t.columns.lastIndexOf(String(column))];
|
|
5456
|
-
});
|
|
5457
|
-
},
|
|
5458
|
-
};
|
|
5459
|
-
return s;
|
|
5437
|
+
// A strict query's reply from the embedded transport's direct lane, or through the generic
|
|
5438
|
+
// dispatch. The errors read the same either way: "D1_ERROR: ..." for SQLite's, and the
|
|
5439
|
+
// transport's own prefix for anything else.
|
|
5440
|
+
function __sbD1Direct(name, sql, params) {
|
|
5441
|
+
try {
|
|
5442
|
+
return __sbD1QueryDirect(name, sql, params);
|
|
5443
|
+
} catch (err) {
|
|
5444
|
+
const message = "sproutboat d1.query: " + (String((err && err.message) || err) || "failed");
|
|
5445
|
+
const at = message.indexOf("D1_");
|
|
5446
|
+
throw new Error(at > 0 ? message.slice(at) : message);
|
|
5460
5447
|
}
|
|
5448
|
+
}
|
|
5449
|
+
// The synchronous core: the answer to one statement as columns, rows and meta. meta is only built
|
|
5450
|
+
// when the caller will hand it out (all() and run()): raw() and first() drop it, and building it
|
|
5451
|
+
// reads the clock twice.
|
|
5452
|
+
function __sbD1Query(dbName, sql, params, strict, wantMeta) {
|
|
5453
|
+
if (strict) __sbD1Check(sql);
|
|
5454
|
+
const started = !strict || wantMeta ? Date.now() : 0;
|
|
5455
|
+
const r = strict && __sbD1QueryDirect ? __sbD1Direct(dbName, sql, params) : __sbD1Rpc("d1.query", { db: dbName, sql, params }, strict);
|
|
5456
|
+
if (!strict) return { results: r.results || [], success: true, meta: r.meta || {} };
|
|
5457
|
+
const table = __sbD1Table(r);
|
|
5458
|
+
return { columns: table.columns, rows: table.rows, meta: wantMeta ? __sbD1Meta(r.meta || {}, started) : null };
|
|
5459
|
+
}
|
|
5460
|
+
// A prepared statement. A class, so a prepare() or bind() does not allocate five closures.
|
|
5461
|
+
class __SbD1Statement {
|
|
5462
|
+
constructor(dbName, sql, params) {
|
|
5463
|
+
this.__name = dbName;
|
|
5464
|
+
this.__sql = sql;
|
|
5465
|
+
this.__params = params;
|
|
5466
|
+
}
|
|
5467
|
+
bind() {
|
|
5468
|
+
const strict = __sbD1Strict();
|
|
5469
|
+
const list = [];
|
|
5470
|
+
for (let i = 0; i < arguments.length; i++) list.push(__sbD1Param(arguments[i], strict));
|
|
5471
|
+
return new __SbD1Statement(this.__name, this.__sql, list);
|
|
5472
|
+
}
|
|
5473
|
+
all() {
|
|
5474
|
+
if (!__sbD1Strict()) return __sbD1Query(this.__name, this.__sql, this.__params, false, true);
|
|
5475
|
+
try {
|
|
5476
|
+
const t = __sbD1Query(this.__name, this.__sql, this.__params, true, true);
|
|
5477
|
+
return Promise.resolve({ success: true, meta: t.meta, results: __sbD1Objects(t.columns, t.rows) });
|
|
5478
|
+
} catch (err) {
|
|
5479
|
+
return Promise.reject(err);
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
run() {
|
|
5483
|
+
return this.all();
|
|
5484
|
+
}
|
|
5485
|
+
raw(options) {
|
|
5486
|
+
if (!__sbD1Strict()) {
|
|
5487
|
+
const rows = __sbD1Query(this.__name, this.__sql, this.__params, false, true).results;
|
|
5488
|
+
const out = [];
|
|
5489
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5490
|
+
const cols = [];
|
|
5491
|
+
for (const k in rows[i]) cols.push(rows[i][k]);
|
|
5492
|
+
out.push(cols);
|
|
5493
|
+
}
|
|
5494
|
+
return out;
|
|
5495
|
+
}
|
|
5496
|
+
try {
|
|
5497
|
+
const t = __sbD1Query(this.__name, this.__sql, this.__params, true, false);
|
|
5498
|
+
return Promise.resolve(options && options.columnNames ? [t.columns].concat(t.rows) : t.rows);
|
|
5499
|
+
} catch (err) {
|
|
5500
|
+
return Promise.reject(err);
|
|
5501
|
+
}
|
|
5502
|
+
}
|
|
5503
|
+
first(column) {
|
|
5504
|
+
if (!__sbD1Strict()) {
|
|
5505
|
+
const rows = __sbD1Query(this.__name, this.__sql, this.__params, false, true).results;
|
|
5506
|
+
if (rows.length === 0) return null;
|
|
5507
|
+
return column == null ? rows[0] : rows[0][column];
|
|
5508
|
+
}
|
|
5509
|
+
try {
|
|
5510
|
+
const t = __sbD1Query(this.__name, this.__sql, this.__params, true, false);
|
|
5511
|
+
if (column == null) return Promise.resolve(t.rows.length ? __sbD1Objects(t.columns, [t.rows[0]])[0] : null);
|
|
5512
|
+
const at = t.columns.indexOf(String(column));
|
|
5513
|
+
if (!t.rows.length) return Promise.resolve(null);
|
|
5514
|
+
if (at < 0) throw __sbD1Fail("D1_COLUMN_NOTFOUND: Column not found (" + column + ")");
|
|
5515
|
+
return Promise.resolve(t.rows[0][t.columns.lastIndexOf(String(column))]);
|
|
5516
|
+
} catch (err) {
|
|
5517
|
+
return Promise.reject(err);
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
function __sbMakeD1(dbName) {
|
|
5461
5522
|
function batch(statements) {
|
|
5462
5523
|
const strict = __sbD1Strict();
|
|
5463
5524
|
const list = [];
|
|
@@ -5495,7 +5556,7 @@ function __sbMakeD1(dbName) {
|
|
|
5495
5556
|
}
|
|
5496
5557
|
const db = {
|
|
5497
5558
|
prepare(sql) {
|
|
5498
|
-
return
|
|
5559
|
+
return new __SbD1Statement(dbName, String(sql), []);
|
|
5499
5560
|
},
|
|
5500
5561
|
batch(statements) {
|
|
5501
5562
|
if (!__sbD1Strict()) return batch(statements);
|
|
@@ -7959,8 +8020,19 @@ function __sbParseHex(s) {
|
|
|
7959
8020
|
// uWS hands back IPv4-mapped IPv6 for IPv4 clients on a dual-stack socket
|
|
7960
8021
|
// (`::ffff:1.2.3.4`, or fully expanded `0:0:0:0:0:ffff:0102:0304`). Fold those
|
|
7961
8022
|
// to the dotted IPv4 so CIDR matching and `clientIp` see a plain address.
|
|
8023
|
+
// The last address folded and its result: a client keeps the same address from one request to the
|
|
8024
|
+
// next, and folding an IPv6 text costs a lowercase, a split and eight hex parses.
|
|
8025
|
+
let __sbIpRaw = "";
|
|
8026
|
+
let __sbIpFolded = "";
|
|
7962
8027
|
function __sbNormalizeIp(ip) {
|
|
7963
8028
|
const s = String(ip || "");
|
|
8029
|
+
if (s === __sbIpRaw) return __sbIpFolded;
|
|
8030
|
+
const folded = __sbFoldIp(s);
|
|
8031
|
+
__sbIpRaw = s;
|
|
8032
|
+
__sbIpFolded = folded;
|
|
8033
|
+
return folded;
|
|
8034
|
+
}
|
|
8035
|
+
function __sbFoldIp(s) {
|
|
7964
8036
|
if (s.indexOf(":") === -1) return s;
|
|
7965
8037
|
const low = s.toLowerCase();
|
|
7966
8038
|
const dotted = low.indexOf("::ffff:");
|
|
@@ -9100,7 +9172,7 @@ async function __sbFetchWithDrain(res, tasks, bindingCall, rethrow, top) {
|
|
|
9100
9172
|
} catch (error) {
|
|
9101
9173
|
// A Durable Object's stub.fetch() from 2026-10-29: the caller gets the exception.
|
|
9102
9174
|
if (rethrow) {
|
|
9103
|
-
__sbDrainWaitUntil(tasks);
|
|
9175
|
+
if (tasks.length) __sbDrainWaitUntil(tasks);
|
|
9104
9176
|
throw error;
|
|
9105
9177
|
}
|
|
9106
9178
|
// #179 — a rejected handler promise must not take the process down with it.
|
|
@@ -9108,8 +9180,8 @@ async function __sbFetchWithDrain(res, tasks, bindingCall, rethrow, top) {
|
|
|
9108
9180
|
}
|
|
9109
9181
|
// The response goes out now and ctx.waitUntil work keeps running after it, as in
|
|
9110
9182
|
// workerd; it was awaited first, so a 1.5 s background task delayed the response by
|
|
9111
|
-
// 1.5 s. The 25 s cap still bounds it.
|
|
9112
|
-
__sbDrainWaitUntil(tasks);
|
|
9183
|
+
// 1.5 s. The 25 s cap still bounds it. Not called for no tasks: an async call is a coroutine and a promise.
|
|
9184
|
+
if (tasks.length) __sbDrainWaitUntil(tasks);
|
|
9113
9185
|
return r;
|
|
9114
9186
|
}
|
|
9115
9187
|
|
|
@@ -9140,7 +9212,7 @@ function __sbErrorResponse(error, bindingCall) {
|
|
|
9140
9212
|
// sees it. Returns whether the edge vouched for this one.
|
|
9141
9213
|
function __sbBindingCallIngress(request) {
|
|
9142
9214
|
const headers = request.headers;
|
|
9143
|
-
const want =
|
|
9215
|
+
const want = __sbEdgeToken();
|
|
9144
9216
|
const vouched = !!want && headers.get("x-sb-binding-call") === "1" && __sbEqCt(headers.get("x-sb-edge-token") || "", want);
|
|
9145
9217
|
if (vouched) {
|
|
9146
9218
|
const url = headers.get("x-sb-url");
|
|
@@ -9148,12 +9220,46 @@ function __sbBindingCallIngress(request) {
|
|
|
9148
9220
|
if (url && URL.canParse(url) && __sbCompatDate() >= "2026-10-22") request.url = url;
|
|
9149
9221
|
headers.delete("x-sb-remote-addr");
|
|
9150
9222
|
}
|
|
9151
|
-
|
|
9152
|
-
|
|
9223
|
+
// Nearly every request carries none of the three: one look at the stored names tells, where
|
|
9224
|
+
// three lookups (each validating and lowercasing its name) and an array of them did.
|
|
9225
|
+
const stored = headers._entries;
|
|
9226
|
+
let present = stored === undefined;
|
|
9227
|
+
for (let i = 0; !present && i < stored.length; i += 2) {
|
|
9228
|
+
const key = stored[i];
|
|
9229
|
+
if (key === "x-sb-binding-call" || key === "x-sb-url" || key === "x-sb-edge-token") present = true;
|
|
9230
|
+
}
|
|
9231
|
+
if (present) {
|
|
9232
|
+
for (const name of ["x-sb-binding-call", "x-sb-url", "x-sb-edge-token"]) {
|
|
9233
|
+
if (headers.get(name) != null) headers.delete(name);
|
|
9234
|
+
}
|
|
9153
9235
|
}
|
|
9154
9236
|
return vouched;
|
|
9155
9237
|
}
|
|
9156
9238
|
|
|
9239
|
+
// SB_EDGE_TOKEN is fixed for the life of a process, like SB_TRUSTED_PROXIES above: read once.
|
|
9240
|
+
let __sbEdgeTokenCache;
|
|
9241
|
+
function __sbEdgeToken() {
|
|
9242
|
+
if (__sbEdgeTokenCache === undefined) __sbEdgeTokenCache = __sbEnv("SB_EDGE_TOKEN");
|
|
9243
|
+
return __sbEdgeTokenCache;
|
|
9244
|
+
}
|
|
9245
|
+
|
|
9246
|
+
// One header of a request the native server built, by a name that is already lowercase and valid:
|
|
9247
|
+
// its stored names are scanned directly, where get() validates and lowercases the name first. A
|
|
9248
|
+
// name stored twice, or a headers object that is not the runtime's, goes through get().
|
|
9249
|
+
function __sbHeader(headers, name) {
|
|
9250
|
+
const stored = headers._entries;
|
|
9251
|
+
if (stored === undefined) return headers.get(name);
|
|
9252
|
+
let found = null;
|
|
9253
|
+
let count = 0;
|
|
9254
|
+
for (let i = 0; i < stored.length; i += 2) {
|
|
9255
|
+
if (stored[i] === name) {
|
|
9256
|
+
found = stored[i + 1];
|
|
9257
|
+
count++;
|
|
9258
|
+
}
|
|
9259
|
+
}
|
|
9260
|
+
return count > 1 ? headers.get(name) : found;
|
|
9261
|
+
}
|
|
9262
|
+
|
|
9157
9263
|
function __sbLogUncaught(where, error) {
|
|
9158
9264
|
try {
|
|
9159
9265
|
let text = "";
|
|
@@ -9317,12 +9423,24 @@ function __sbIngressStream(request, id) {
|
|
|
9317
9423
|
request.bytes = async () => await all();
|
|
9318
9424
|
}
|
|
9319
9425
|
|
|
9426
|
+
// The id of the current request's streamed body, 0 when it has none (op "t" of __sbReqBodyOp),
|
|
9427
|
+
// as a number: every request asks, and the text round trip was a format and a parse.
|
|
9428
|
+
function __sbReqBodyTake() {
|
|
9429
|
+
let id = 0;
|
|
9430
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
9431
|
+
Porffor.c`
|
|
9432
|
+
extern unsigned int sb_reqbody_take_current(void);
|
|
9433
|
+
id = (f64)sb_reqbody_take_current();
|
|
9434
|
+
`;
|
|
9435
|
+
return id;
|
|
9436
|
+
}
|
|
9437
|
+
|
|
9320
9438
|
// Native HTTP ingress stores the body as one character per raw byte. Keep
|
|
9321
9439
|
// that representation for binary consumers, but decode text lazily before
|
|
9322
9440
|
// handing the request to user code. Request.text() in Porffor otherwise treats
|
|
9323
9441
|
// the raw bytes as already-decoded characters and corrupts Unicode/HMAC input.
|
|
9324
9442
|
function __sbDecodeIngress(request) {
|
|
9325
|
-
const streamed = __sbCompatDate() >= "2026-10-27" ?
|
|
9443
|
+
const streamed = __sbCompatDate() >= "2026-10-27" ? __sbReqBodyTake() : 0;
|
|
9326
9444
|
if (streamed > 0) {
|
|
9327
9445
|
__sbIngressStream(request, streamed);
|
|
9328
9446
|
return;
|
|
@@ -9358,7 +9476,7 @@ function __sbTrustedProxiesOnce() {
|
|
|
9358
9476
|
|
|
9359
9477
|
globalThis.__sbEntry = function (handlers, request) {
|
|
9360
9478
|
__sbDecodeIngress(request);
|
|
9361
|
-
const trigger = request.headers
|
|
9479
|
+
const trigger = __sbHeader(request.headers, "x-sb-trigger");
|
|
9362
9480
|
if (!trigger) {
|
|
9363
9481
|
const bindingCall = __sbBindingCallIngress(request);
|
|
9364
9482
|
// #163/#128 — expose the request metadata the edge already has, the
|
|
@@ -9368,7 +9486,7 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
9368
9486
|
const __cf = request.cf || {};
|
|
9369
9487
|
if (__sbTrustedProxiesOnce().length === 0) {
|
|
9370
9488
|
// No trusted proxy (the usual case): the peer is the client and no forwarded header counts.
|
|
9371
|
-
__cf.clientIp = __sbNormalizeIp(request.headers
|
|
9489
|
+
__cf.clientIp = __sbNormalizeIp(__sbHeader(request.headers, "x-sb-remote-addr") || "");
|
|
9372
9490
|
} else {
|
|
9373
9491
|
__cf.clientIp = __sbClientIp(request);
|
|
9374
9492
|
const __httpProtocol = __sbTrustedProxyHeader(request, "x-sb-http-protocol");
|