@sproutboat/runtime 0.34.1 → 0.34.3

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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.34.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 49ac7df: Faster `atob`: each character is looked up in a 128-entry table instead of `indexOf` over the alphabet, and the same pass validates, so the regex test over the whole input is gone. Same output and same `InvalidCharacterError` cases (checked against the platform `atob` on 9,000 random inputs and edge cases).
8
+
9
+ A JWT check decodes three base64url parts. On one pinned x86 core at a fixed rate, CPU per request: like 854 to 761 us, create 922 to 866 us; feed and post reads do not call `atob`.
10
+ - 745f94c: Faster D1 in standalone builds: a D1 call no longer goes through `JSON.stringify` and `JSON.parse` on both sides of the in-process dispatch. The request object is handed straight to the dispatcher and the reply object comes straight back (the C layer still returns its own JSON, parsed once). Same results, same error messages, no change under the broker transport.
11
+
12
+ On one pinned x86 core at a fixed rate, CPU per request against a 500,000-row feed API: feed (20 rows) 1,418 to 1,133 us, post read 394 to 332 us, like 854 to 728 us, create 922 to 857 us, the realistic mix 670 to 580 us.
13
+ - 88c7bff: Faster strict D1 queries in standalone builds (compatibility date 2026-10-24 and later): the rows of a result are built as JS arrays in C, straight from the SQLite cells, instead of being written as JSON text in C and parsed again in JS. NULL, numbers, text (UTF-8 decoded exactly as the JSON path decoded it, including invalid sequences), BLOBs (byte arrays) and empty results are unchanged. Only a small header (columns, changes, last row id) still goes through JSON. Older compatibility dates and non-D1 operations keep the JSON path.
14
+
15
+ On one pinned x86 core at a fixed rate, CPU per request against a 500,000-row feed API, see the PR for numbers.
16
+
17
+ ## 0.34.2
18
+
19
+ ### Patch Changes
20
+
21
+ - a0ba6a3: Faster requests: about 57% of a request's CPU was Porffor's `globalThis` getter.
22
+
23
+ - **Toolchain, the root cause.** Porffor's `#get_globalThis` re-synced every top-level function and `var` of the module into the global object on every read, one hashed write per binding (about 20 µs with the prelude's ~120 bindings, so every `globalThis.x` in user code paid it too). Every binding is now written when the object is built, and a later read rewrites only the bindings whose value changed (a packed copy of the last value written is kept per binding: an unchanged binding costs one compare). 20,000 reads with 600 top-level functions: 9,300 ms before, a few ms after.
24
+ - **Runtime and toolchain, the request path.** The runtime's `globalThis.__sbCompat` / `globalThis.env` reads go through accessors over module-level `let`s that the app tail sets once, and `Request`/`Response` cache the class lookups and take the date and WebSocketPair state from statics the runtime sets. Until the tail has run (the shared template, unit tests) everything reads `globalThis` as before.
25
+ - **Client IP.** `request.cf` no longer reads and splits `SB_TRUSTED_PROXIES` four times a request: it is read once per process, and with no trusted proxy (the usual case) the peer is the client and the forwarded-header lookups are skipped.
26
+ - **`x-sb-cpu-ms`.** The stamp is appended to the handler's own Response instead of building a second Response with a copy of every header. The same responses are stamped, with the same headers and body.
27
+
28
+ A 404 on one pinned x86 core, CPU per request when saturated, standalone: 319 µs before, 127 µs with the getter patch alone, 113 µs with the request-path change, 81 µs with the client-IP and stamp changes.
29
+
3
30
  ## 0.34.1
4
31
 
5
32
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.34.1",
3
+ "version": "0.34.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,7 @@ const load = new Function(
17
17
  "__envMap",
18
18
  `${src.slice(start, end)}
19
19
  const __sbEnv = (k) => __envMap[k];
20
- return { __sbNormalizeIp, __sbIpInCidr, __sbIpTrusted, __sbClientIp, __sbSplitList, __sbTrustedProxyHeader };`,
20
+ return { __sbNormalizeIp, __sbIpInCidr, __sbIpTrusted, __sbClientIp, __sbSplitList, __sbTrustedProxyHeader, __sbTrustedProxiesOnce };`,
21
21
  );
22
22
  const H = load(new Proxy({}, { get: (_, k) => env[k] }));
23
23
 
@@ -62,7 +62,9 @@ test("__sbClientIp: a trusted peer resolves the rightmost untrusted XFF hop", ()
62
62
 
63
63
  test("__sbClientIp: an untrusted peer cannot use XFF even if it sends one", () => {
64
64
  env = { SB_TRUSTED_PROXIES: "10.0.0.0/8" };
65
- expect(H.__sbClientIp(req({ "x-sb-remote-addr": "198.51.100.9", "x-forwarded-for": "1.1.1.1" }))).toBe("198.51.100.9");
65
+ expect(H.__sbClientIp(req({ "x-sb-remote-addr": "198.51.100.9", "x-forwarded-for": "1.1.1.1" }))).toBe(
66
+ "198.51.100.9",
67
+ );
66
68
  });
67
69
 
68
70
  test("__sbTrustedProxyHeader: no trusted proxies configured means the header is ignored", () => {
@@ -83,3 +85,10 @@ test("__sbTrustedProxyHeader: a trusted peer's value passes through, absent stay
83
85
  expect(H.__sbTrustedProxyHeader(r, "x-sb-http-protocol")).toBe("HTTP/2");
84
86
  expect(H.__sbTrustedProxyHeader(r, "x-sb-tls-cipher")).toBeUndefined();
85
87
  });
88
+
89
+ test("__sbTrustedProxiesOnce reads SB_TRUSTED_PROXIES once and keeps the list", () => {
90
+ env = { SB_TRUSTED_PROXIES: "10.0.0.0/8, 192.0.2.1" };
91
+ expect(H.__sbTrustedProxiesOnce()).toEqual(["10.0.0.0/8", "192.0.2.1"]);
92
+ env = { SB_TRUSTED_PROXIES: "198.51.100.0/24" };
93
+ expect(H.__sbTrustedProxiesOnce()).toEqual(["10.0.0.0/8", "192.0.2.1"]); // fixed for the process
94
+ });
@@ -7,7 +7,7 @@ const rawBody = src.slice(src.indexOf("function __sbRawBody("), src.indexOf("fun
7
7
  if (!decoder || !ingress || !rawBody) throw new Error("Ingress decoder source not found");
8
8
  // oxlint-disable-next-line no-function-constructor -- Exercise the text-only runtime prelude.
9
9
  const load = new Function(
10
- `// The C fast paths (Porffor.c) cannot run here: the stubs make the JS fallback run.\nfunction __sbFromUtf8Native() { return ""; }\nfunction __sbToBytesNative() { return ""; }\n${decoder}\n${rawBody}\n${ingress}\nreturn __sbDecodeIngress;`,
10
+ `// The C fast paths (Porffor.c) cannot run here: the stubs make the JS fallback run.\nfunction __sbFromUtf8Native() { return ""; }\nfunction __sbToBytesNative() { return ""; }\nfunction __sbCompatDate() { return undefined; }\n${decoder}\n${rawBody}\n${ingress}\nreturn __sbDecodeIngress;`,
11
11
  );
12
12
  const decode = load();
13
13
  const raw = (text) => Array.from(new TextEncoder().encode(text), (byte) => String.fromCharCode(byte)).join("");
@@ -1,3 +1,24 @@
1
+ // Reads of globalThis are expensive in Porffor (the getter rebuilds the global object on each read);
2
+ // the app tail (wrap.ts) sets these two once. See sproutd/benchmarks/per-request-2026-10-26.
3
+ let __sbCompatNow;
4
+ let __sbEnvNow;
5
+ function __sbCompatDate() {
6
+ return __sbCompatNow === undefined ? globalThis.__sbCompat : __sbCompatNow;
7
+ }
8
+ function __sbEnvValue() {
9
+ return __sbEnvNow === undefined ? globalThis.env : __sbEnvNow;
10
+ }
11
+ // Setters, not bare assignments in the tail: the template and every app must compile the prelude identically.
12
+ function __sbSetCompat(date) {
13
+ __sbCompatNow = date;
14
+ Request._cd = date;
15
+ }
16
+ function __sbSetEnv(value) {
17
+ __sbEnvNow = value;
18
+ }
19
+ // Referenced, so the shared template keeps them: an app tail can only call what the template compiled.
20
+ globalThis.__sbSetCompat = __sbSetCompat;
21
+ globalThis.__sbSetEnv = __sbSetEnv;
1
22
  // WEB STREAMS: generated by gen-streams.ts, do not edit.
2
23
  /* oxlint-disable -- generated third-party implementation */
3
24
  // web-streams-polyfill 4.3.0, MIT, copyright Mattias Buelens (see WEB_STREAMS_LICENSE)
@@ -3095,17 +3116,25 @@ function __sbCpuMs() {
3095
3116
  function __sbTagCpu(res, t0) {
3096
3117
  const cpu = __sbCpuMs() - t0;
3097
3118
  try {
3098
- const body = __sbRawBody(res);
3099
- if (__sbIsStr(body) && !res.headers.has("set-cookie")) {
3100
- const headers = {};
3101
- res.headers.forEach(function (value, name) {
3102
- headers[name] = value;
3103
- });
3104
- headers["x-sb-cpu-ms"] = (cpu >= 0 ? cpu : 0).toFixed(3);
3105
- return new Response(body, { status: res.status, headers: headers });
3119
+ // A string body and no Set-Cookie, as before. The native writer reads Headers' flat `_entries`,
3120
+ // so the stamp is appended to the handler's own Response instead of building a second one (a
3121
+ // Headers, a body extraction and a status check, plus a copy of every header); an x-sb-cpu-ms
3122
+ // the handler set itself is replaced by the measured one, as before.
3123
+ const headers = res && res.headers;
3124
+ if (headers && __sbIsStr(__sbRawBody(res)) && !headers.has("set-cookie")) {
3125
+ const stamp = (cpu >= 0 ? cpu : 0).toFixed(3);
3126
+ const entries = headers._entries;
3127
+ let set = false;
3128
+ for (let i = 0; i < entries.length; i += 2) {
3129
+ if (entries[i] === "x-sb-cpu-ms") {
3130
+ entries[i + 1] = stamp;
3131
+ set = true;
3132
+ }
3133
+ }
3134
+ if (!set) headers._push("x-sb-cpu-ms", stamp);
3106
3135
  }
3107
3136
  } catch {
3108
- /* fall through to the original response */
3137
+ /* the handler's Response goes out unstamped */
3109
3138
  }
3110
3139
  return res;
3111
3140
  }
@@ -3323,7 +3352,7 @@ if (Headers.prototype[Symbol.iterator] == null) Headers.prototype[Symbol.iterato
3323
3352
  // and delete with a TypeError. A copy (new Headers(h), new Response(body, res)) is
3324
3353
  // mutable. The runtime's own writes after the handler lift the guard first.
3325
3354
  function __sbFreezeHeaders(message) {
3326
- if (globalThis.__sbCompat >= "2026-10-23") message.headers._immutable = true;
3355
+ if (__sbCompatDate() >= "2026-10-23") message.headers._immutable = true;
3327
3356
  return message;
3328
3357
  }
3329
3358
 
@@ -3610,16 +3639,24 @@ if (globalThis.crypto.getRandomValues == null) {
3610
3639
  // so a string held as UTF-16 (one from JSON.parse or TextDecoder, say) decoded to the
3611
3640
  // wrong bytes, and btoa encoded its UTF-16 units.
3612
3641
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3642
+ // Sextet per character code (255 = not base64): a table read rather than an indexOf over the
3643
+ // alphabet, and one pass that also validates, so no regex test over the whole input.
3644
+ const sextet = [];
3645
+ for (let i = 0; i < 128; i++) sextet.push(255);
3646
+ for (let i = 0; i < alphabet.length; i++) sextet[alphabet.charCodeAt(i)] = i;
3613
3647
  globalThis.atob = function (data) {
3614
3648
  let text = String(data).replace(/[\t\n\f\r ]/g, "");
3615
3649
  if (text.length % 4 === 0) text = text.replace(/==?$/, "");
3616
- if (text.length % 4 === 1 || !/^[A-Za-z0-9+/]*$/.test(text)) throw invalidCharacter("atob: the string is not valid base64");
3650
+ if (text.length % 4 === 1) throw invalidCharacter("atob: the string is not valid base64");
3617
3651
  const parts = [];
3618
3652
  let chunk = "";
3619
3653
  let bits = 0;
3620
3654
  let count = 0;
3621
3655
  for (let i = 0; i < text.length; i++) {
3622
- bits = (bits << 6) | alphabet.indexOf(text[i]);
3656
+ const code = text.charCodeAt(i);
3657
+ const value = code < 128 ? sextet[code] : 255;
3658
+ if (value === 255) throw invalidCharacter("atob: the string is not valid base64");
3659
+ bits = (bits << 6) | value;
3623
3660
  count += 6;
3624
3661
  if (count >= 8) {
3625
3662
  count -= 8;
@@ -3752,7 +3789,7 @@ if (typeof globalThis.EventTarget !== "function") {
3752
3789
  if (this.aborted) return;
3753
3790
  this.aborted = true;
3754
3791
  // workerd's default reason reads "The operation was aborted" (from 2026-10-23 here).
3755
- const message = globalThis.__sbCompat >= "2026-10-23" ? "The operation was aborted" : "This operation was aborted";
3792
+ const message = __sbCompatDate() >= "2026-10-23" ? "The operation was aborted" : "This operation was aborted";
3756
3793
  this.reason = reason === undefined ? namedError("AbortError", message) : reason;
3757
3794
  const event = new Event("abort");
3758
3795
  if (typeof this.onabort === "function") this.onabort.call(this, event);
@@ -5007,7 +5044,7 @@ function __sbRpc(op, extra) {
5007
5044
  // pages with limit and cursor and returns each key's expiration and metadata. Earlier
5008
5045
  // dates keep the synchronous shim below.
5009
5046
  function __sbKvStrict() {
5010
- return globalThis.__sbCompat >= "2026-10-25";
5047
+ return __sbCompatDate() >= "2026-10-25";
5011
5048
  }
5012
5049
  function __sbKvKey(key, verb) {
5013
5050
  const name = String(key);
@@ -5131,7 +5168,7 @@ function __sbMakeKv(ns) {
5131
5168
  // are enforced, errors read "D1_ERROR: ...", and a BLOB comes back as an array of
5132
5169
  // byte values. Earlier dates keep the old behaviour.
5133
5170
  function __sbD1Strict() {
5134
- return globalThis.__sbCompat >= "2026-10-24";
5171
+ return __sbCompatDate() >= "2026-10-24";
5135
5172
  }
5136
5173
  function __sbD1Param(value, strict) {
5137
5174
  if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
@@ -5144,11 +5181,30 @@ function __sbD1Param(value, strict) {
5144
5181
  // oxlint-disable-next-line anti-slop/no-runtime-typeof -- workerd's D1_TYPE_ERROR names the value's typeof
5145
5182
  throw new Error("D1_TYPE_ERROR: Type '" + typeof value + "' not supported for value '" + String(value) + "'");
5146
5183
  }
5184
+ // An embedded (standalone) build sets this to hand the request object straight to its
5185
+ // dispatcher. D1 replies are the biggest ones a handler sees (20 rows is 100 cells), and
5186
+ // the JSON.stringify and JSON.parse around the in-process call cost more than SQLite did.
5187
+ // 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.)
5188
+ // oxlint-disable-next-line no-unassigned-vars -- the embedded transport assigns it; the broker transport does not.
5189
+ var __sbCallDirect;
5190
+ function __sbRpcDirect(op, extra) {
5191
+ if (!__sbCallDirect) return __sbRpc(op, extra);
5192
+ const req = { v: 1, id: ++__sbReqId, op };
5193
+ if (extra) for (const k in extra) req[k] = extra[k];
5194
+ let reply;
5195
+ try {
5196
+ reply = __sbCallDirect(req);
5197
+ } catch (err) {
5198
+ throw new Error(`sproutboat ${op}: ${String((err && err.message) || err) || "failed"}`);
5199
+ }
5200
+ if (reply && reply.ok === false) throw new Error(`sproutboat ${op}: ${reply.error || "failed"}`);
5201
+ return reply;
5202
+ }
5147
5203
  function __sbD1Rpc(op, extra, strict) {
5148
- if (!strict) return __sbRpc(op, extra);
5204
+ if (!strict) return __sbRpcDirect(op, extra);
5149
5205
  extra.strict = true;
5150
5206
  try {
5151
- return __sbRpc(op, extra);
5207
+ return __sbRpcDirect(op, extra);
5152
5208
  } catch (err) {
5153
5209
  // "sproutboat d1.query: D1_ERROR: ..." -> "D1_ERROR: ...", as workerd words it.
5154
5210
  const message = String((err && err.message) || err);
@@ -5211,7 +5267,8 @@ function __sbD1Meta(meta, started) {
5211
5267
  }
5212
5268
  // Columns and rows of a strict reply (an older broker answers with objects only).
5213
5269
  function __sbD1Table(r) {
5214
- if (r.columns) return { columns: r.columns, rows: __sbD1Rows(r) };
5270
+ // rows built in C (r.native) already hold BLOBs as byte arrays
5271
+ if (r.columns) return { columns: r.columns, rows: r.native === true ? r.rows : __sbD1Rows(r) };
5215
5272
  const objects = r.results || [];
5216
5273
  const columns = objects.length ? Object.keys(objects[0]) : [];
5217
5274
  const rows = [];
@@ -5449,7 +5506,7 @@ function __sbR2Multipart(bucket, key, uploadId) {
5449
5506
  // cursor, groups by delimiter and returns metadata only when `include` asks for it.
5450
5507
  // Earlier dates keep the synchronous shim below.
5451
5508
  function __sbR2Strict() {
5452
- return globalThis.__sbCompat >= "2026-10-26";
5509
+ return __sbCompatDate() >= "2026-10-26";
5453
5510
  }
5454
5511
  function __sbR2Key(key, verb) {
5455
5512
  const name = String(key);
@@ -5790,7 +5847,7 @@ function __sbAssetResponse(path, r) {
5790
5847
  // The queue name and consumer settings (bindings.queueConfig, from `queues` in
5791
5848
  // sproutboat.jsonc) apply at every date: they are opt-in.
5792
5849
  function __sbQueuesStrict() {
5793
- return globalThis.__sbCompat >= "2026-10-31";
5850
+ return __sbCompatDate() >= "2026-10-31";
5794
5851
  }
5795
5852
 
5796
5853
  // binding -> { binding, queue, maxBatchSize?, maxBatchTimeout?, maxRetries?, retryDelay?, deadLetterQueue? }
@@ -6106,7 +6163,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
6106
6163
  sendBatch(messages, options) {
6107
6164
  // From 2026-10-28 sendBatch(messages, { delaySeconds }) delays every message that
6108
6165
  // sets none of its own, as on Workers; before, the batch option was ignored.
6109
- const batchDelay = globalThis.__sbCompat >= "2026-10-28" ? (options && options.delaySeconds) || 0 : 0;
6166
+ const batchDelay = __sbCompatDate() >= "2026-10-28" ? (options && options.delaySeconds) || 0 : 0;
6110
6167
  const list = [];
6111
6168
  for (let j = 0; j < (messages || []).length; j++) {
6112
6169
  const m = messages[j];
@@ -6193,7 +6250,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
6193
6250
  const binding = bindings.services[i].binding;
6194
6251
  const fetcher = {
6195
6252
  fetch(input, init) {
6196
- if (globalThis.__sbCompat >= "2026-10-22") return __sbServiceFetch(binding, input, init);
6253
+ if (__sbCompatDate() >= "2026-10-22") return __sbServiceFetch(binding, input, init);
6197
6254
  const parts = __sbFetchParts(input, init);
6198
6255
  const msg = { binding, url: parts.url, method: parts.method, headers: parts.headers, ...__sbPartsBody(parts) };
6199
6256
  return __sbUpstreamResponse(globalThis.__sbFetchUpstream("service.fetch", msg));
@@ -6383,7 +6440,7 @@ function __sbHandles(handlers, name) {
6383
6440
  // Callee: the receiver of one request. A default export that is a class (a
6384
6441
  // WorkerEntrypoint) is instantiated per request with its ctx and env, as in workerd.
6385
6442
  function __sbReceiver(handlers, ctx) {
6386
- return __sbIsFn(handlers) ? new handlers(ctx, globalThis.env) : handlers;
6443
+ return __sbIsFn(handlers) ? new handlers(ctx, __sbEnvValue()) : handlers;
6387
6444
  }
6388
6445
 
6389
6446
  // Callee: runs RPC method `method` of `receiver` with the arguments in the request body.
@@ -6412,7 +6469,7 @@ async function __sbRpcDispatch(handlers, receiver, method, request) {
6412
6469
  // throws), a Request given as init brings its body, GET and HEAD refuse a body, and the
6413
6470
  // method and redirect mode are checked. Before it, the parts are read off the input.
6414
6471
  function __sbFetchParts(input, init) {
6415
- if (globalThis.__sbCompat >= "2026-10-23") {
6472
+ if (__sbCompatDate() >= "2026-10-23") {
6416
6473
  const request = new Request(input, init);
6417
6474
  const headers = [];
6418
6475
  request.headers.forEach((v, k) => headers.push([k, v]));
@@ -6488,7 +6545,7 @@ function __sbUpstreamResponse(r) {
6488
6545
  function __sbStreamsBody(body) {
6489
6546
  return (
6490
6547
  body instanceof ReadableStream &&
6491
- globalThis.__sbCompat >= "2026-10-27" &&
6548
+ __sbCompatDate() >= "2026-10-27" &&
6492
6549
  globalThis.__sbBodyStreamable != null &&
6493
6550
  globalThis.__sbBodyStreamable()
6494
6551
  );
@@ -6615,7 +6672,7 @@ function __sbFollowHop(op, msg, label, mode, signal, hops) {
6615
6672
  const step = (res) => {
6616
6673
  const location = res.headers.get("location");
6617
6674
  const redirect = res.status === 301 || res.status === 302 || res.status === 303 || res.status === 307 || res.status === 308;
6618
- if (!(globalThis.__sbCompat >= "2026-10-22") || mode === "manual" || !redirect || location == null) return res;
6675
+ if (!(__sbCompatDate() >= "2026-10-22") || mode === "manual" || !redirect || location == null) return res;
6619
6676
  if (mode === "error") throw new TypeError(label + ": redirect not allowed (redirect: \"error\")");
6620
6677
  if (hops >= 20) throw new TypeError(label + ": too many redirects");
6621
6678
  const dropped = __sbRawBody(res);
@@ -6652,9 +6709,9 @@ function __sbFollowHop(op, msg, label, mode, signal, hops) {
6652
6709
  // (__sbAsyncAtAnyDate). `always` is for RPC, which has no older behaviour to keep.
6653
6710
  function __sbUpstream(op, msg, label, signal, always) {
6654
6711
  if (signal) signal.throwIfAborted();
6655
- const promised = always || globalThis.__sbCompat >= "2026-10-22" || __sbAsyncAtAnyDate();
6712
+ const promised = always || __sbCompatDate() >= "2026-10-22" || __sbAsyncAtAnyDate();
6656
6713
  if (!__sbHttpAsyncEnabled() || !promised) return __sbUpstreamResponse(globalThis.__sbFetchUpstream(op, msg));
6657
- if (globalThis.__sbCompat >= "2026-10-22") return __sbUpstreamStreamed(op, msg, label, signal);
6714
+ if (__sbCompatDate() >= "2026-10-22") return __sbUpstreamStreamed(op, msg, label, signal);
6658
6715
  return new Promise((resolve, reject) => {
6659
6716
  let onAbort = null;
6660
6717
  const settled = () => {
@@ -6701,7 +6758,7 @@ globalThis.__sbInstallFetch = function () {
6701
6758
  parts = __sbFetchParts(input, init);
6702
6759
  } catch (error) {
6703
6760
  // From 2026-10-23 an invalid request rejects, as in workerd; before it, it threw.
6704
- if (globalThis.__sbCompat >= "2026-10-23") return Promise.reject(error);
6761
+ if (__sbCompatDate() >= "2026-10-23") return Promise.reject(error);
6705
6762
  throw error;
6706
6763
  }
6707
6764
  if (__sbWsOutboundOn() && __sbWsIsUpgrade(parts)) return __sbWsFetchUpgrade(parts);
@@ -6762,7 +6819,7 @@ function __sbMakeDONamespace(binding, className) {
6762
6819
  let req;
6763
6820
  // From 2026-10-23 the object gets the Request Fetch makes of input and init, as in
6764
6821
  // workerd: method, headers and body survive, and the input's body is used up.
6765
- if (globalThis.__sbCompat >= "2026-10-23") {
6822
+ if (__sbCompatDate() >= "2026-10-23") {
6766
6823
  req = new Request(input, init);
6767
6824
  } else if (__sbIsObj(input) && __sbIsStr(input.url) && !init) {
6768
6825
  req = input;
@@ -6814,7 +6871,7 @@ function __sbMakeDONamespace(binding, className) {
6814
6871
  // The object runs in this sprout on every transport (see above), so an RPC call is a
6815
6872
  // direct call; only its storage goes to the broker.
6816
6873
  function __sbDOv2() {
6817
- return globalThis.__sbCompat >= "2026-10-29";
6874
+ return __sbCompatDate() >= "2026-10-29";
6818
6875
  }
6819
6876
 
6820
6877
  function __sbDOSha(text) {
@@ -7060,7 +7117,7 @@ function __sbGetDOInstance(cls, idText, idObj) {
7060
7117
  // The hibernation API, from 2026-10-30 (where WebSocketPair exists).
7061
7118
  const holder = { inst: null, auto: null, cls, id, restored: [] };
7062
7119
  if (__sbWsEnabled()) __sbDOWebSocketState(state, holder);
7063
- inst = new Ctor(state, globalThis.env);
7120
+ inst = new Ctor(state, __sbEnvValue());
7064
7121
  holder.inst = inst;
7065
7122
  inst.__sbTasks = __sbTasks;
7066
7123
  inst.__sbGate = gate;
@@ -7154,7 +7211,7 @@ function __sbDOStorage(cls, id) {
7154
7211
  // Values written in the new format start with __sbDOTag (no JSON text starts with "#"),
7155
7212
  // so either side reads both.
7156
7213
  function __sbDOGated() {
7157
- return globalThis.__sbCompat >= "2026-10-28";
7214
+ return __sbCompatDate() >= "2026-10-28";
7158
7215
  }
7159
7216
 
7160
7217
  function __sbDOMakeGate() {
@@ -8436,7 +8493,7 @@ function __sbWsHostResponse(r) {
8436
8493
  }
8437
8494
  // The compatibility date goes along: a standalone or platform server carries client sockets
8438
8495
  // from 2026-11-01 only (sproutd, whose bridge ignores it, from 2026-10-30).
8439
- const id = Number(__sbWsOp("o", 0, globalThis.__sbCompat, 0));
8496
+ const id = Number(__sbWsOp("o", 0, __sbCompatDate(), 0));
8440
8497
  if (!(id > 0)) return new Response("WebSockets are not supported by this host yet", { status: 501 });
8441
8498
  ws._host = id;
8442
8499
  if (ws._peer !== null && ws._peer._hib !== null && __sbWsHibOn()) __sbWsHibRegister(ws._peer, id);
@@ -8496,7 +8553,7 @@ function __sbWsPump(ws) {
8496
8553
  // described at __sbWsOp, ids with the top bit set. Returning that Response from a handler
8497
8554
  // relays the socket to the client through a pair.
8498
8555
  function __sbWsOutboundOn() {
8499
- return globalThis.__sbCompat >= "2026-11-01";
8556
+ return __sbCompatDate() >= "2026-11-01";
8500
8557
  }
8501
8558
  let __sbWsWakeable = -1;
8502
8559
  function __sbWsCanWake() {
@@ -8783,7 +8840,7 @@ function __sbWsProxy(r, upstream) {
8783
8840
  // their tags and attachments) and the event is dispatched as webSocketMessage/webSocketClose.
8784
8841
  let __sbWsHibHost = -1;
8785
8842
  function __sbWsHibOn() {
8786
- if (globalThis.__sbCompat < "2026-11-01" || !__sbDOGated()) return false;
8843
+ if (__sbCompatDate() < "2026-11-01" || !__sbDOGated()) return false;
8787
8844
  if (__sbWsHibHost < 0 && __sbWsOp("H", 0, "", 0) === "1") __sbWsHibHost = 1;
8788
8845
  return __sbWsHibHost === 1;
8789
8846
  }
@@ -8915,7 +8972,9 @@ function __sbWsRestoreSocket(id) {
8915
8972
  }
8916
8973
 
8917
8974
  function __sbInstallWebSockets() {
8918
- if (globalThis.__sbCompat < "2026-10-30") return;
8975
+ Request._hasWsp = false;
8976
+ if (__sbCompatDate() < "2026-10-30") return;
8977
+ Request._hasWsp = true;
8919
8978
  globalThis.WebSocketPair = __SbWebSocketPair;
8920
8979
  globalThis.WebSocket = __SbWebSocket;
8921
8980
  globalThis.WebSocketRequestResponsePair = __SbWebSocketRequestResponsePair;
@@ -8978,7 +9037,7 @@ function __sbBindingCallIngress(request) {
8978
9037
  if (vouched) {
8979
9038
  const url = headers.get("x-sb-url");
8980
9039
  // The callee sees its caller's URL from compatibility date 2026-10-22, as in workerd.
8981
- if (url && URL.canParse(url) && globalThis.__sbCompat >= "2026-10-22") request.url = url;
9040
+ if (url && URL.canParse(url) && __sbCompatDate() >= "2026-10-22") request.url = url;
8982
9041
  headers.delete("x-sb-remote-addr");
8983
9042
  }
8984
9043
  for (const name of ["x-sb-binding-call", "x-sb-url", "x-sb-edge-token"]) {
@@ -9078,7 +9137,7 @@ function __sbReqBodyOp(op, id, data) {
9078
9137
  // Turns streaming on in the native server, from 2026-10-27. Called once at startup, after
9079
9138
  // the compatibility date is set (__sbInstallFetch).
9080
9139
  function __sbIngressStreamingSetup() {
9081
- if (globalThis.__sbCompat >= "2026-10-27") __sbReqBodyOp("e", 0, "");
9140
+ if (__sbCompatDate() >= "2026-10-27") __sbReqBodyOp("e", 0, "");
9082
9141
  }
9083
9142
 
9084
9143
  // request.body for a streamed request: chunks as the server receives them.
@@ -9155,7 +9214,7 @@ function __sbIngressStream(request, id) {
9155
9214
  // handing the request to user code. Request.text() in Porffor otherwise treats
9156
9215
  // the raw bytes as already-decoded characters and corrupts Unicode/HMAC input.
9157
9216
  function __sbDecodeIngress(request) {
9158
- const streamed = globalThis.__sbCompat >= "2026-10-27" ? Number(__sbReqBodyOp("t", 0, "")) : 0;
9217
+ const streamed = __sbCompatDate() >= "2026-10-27" ? Number(__sbReqBodyOp("t", 0, "")) : 0;
9159
9218
  if (streamed > 0) {
9160
9219
  __sbIngressStream(request, streamed);
9161
9220
  return;
@@ -9164,7 +9223,7 @@ function __sbDecodeIngress(request) {
9164
9223
  if (raw == null) return;
9165
9224
  // A GET or HEAD has no body, as in workerd (from 2026-10-23): request.body is null, and
9166
9225
  // new Request(request) copies none. Before it, the body is the empty string.
9167
- if (raw === "" && (request.method === "GET" || request.method === "HEAD") && globalThis.__sbCompat >= "2026-10-23") {
9226
+ if (raw === "" && (request.method === "GET" || request.method === "HEAD") && __sbCompatDate() >= "2026-10-23") {
9168
9227
  request.body = null;
9169
9228
  return;
9170
9229
  }
@@ -9181,6 +9240,14 @@ function __sbDecodeIngress(request) {
9181
9240
  };
9182
9241
  }
9183
9242
 
9243
+ // SB_TRUSTED_PROXIES is fixed for the life of a process (a sproutd worker gets its own after the fork,
9244
+ // and this runs per request, never before it), so it is read and split once, not four times a request.
9245
+ let __sbTrustedCache;
9246
+ function __sbTrustedProxiesOnce() {
9247
+ if (__sbTrustedCache === undefined) __sbTrustedCache = __sbSplitList(__sbEnv("SB_TRUSTED_PROXIES"));
9248
+ return __sbTrustedCache;
9249
+ }
9250
+
9184
9251
  globalThis.__sbEntry = function (handlers, request) {
9185
9252
  __sbDecodeIngress(request);
9186
9253
  const trigger = request.headers.get("x-sb-trigger");
@@ -9191,13 +9258,18 @@ globalThis.__sbEntry = function (handlers, request) {
9191
9258
  // are only set when a trusted front proxy forwarded them; colo, ASN and
9192
9259
  // geo fields stay absent everywhere (#128 explicitly rejects faking them).
9193
9260
  const __cf = request.cf || {};
9194
- __cf.clientIp = __sbClientIp(request);
9195
- const __httpProtocol = __sbTrustedProxyHeader(request, "x-sb-http-protocol");
9196
- if (__httpProtocol) __cf.httpProtocol = __httpProtocol;
9197
- const __tlsVersion = __sbTrustedProxyHeader(request, "x-sb-tls-version");
9198
- if (__tlsVersion) __cf.tlsVersion = __tlsVersion;
9199
- const __tlsCipher = __sbTrustedProxyHeader(request, "x-sb-tls-cipher");
9200
- if (__tlsCipher) __cf.tlsCipher = __tlsCipher;
9261
+ if (__sbTrustedProxiesOnce().length === 0) {
9262
+ // No trusted proxy (the usual case): the peer is the client and no forwarded header counts.
9263
+ __cf.clientIp = __sbNormalizeIp(request.headers.get("x-sb-remote-addr") || "");
9264
+ } else {
9265
+ __cf.clientIp = __sbClientIp(request);
9266
+ const __httpProtocol = __sbTrustedProxyHeader(request, "x-sb-http-protocol");
9267
+ if (__httpProtocol) __cf.httpProtocol = __httpProtocol;
9268
+ const __tlsVersion = __sbTrustedProxyHeader(request, "x-sb-tls-version");
9269
+ if (__tlsVersion) __cf.tlsVersion = __tlsVersion;
9270
+ const __tlsCipher = __sbTrustedProxyHeader(request, "x-sb-tls-cipher");
9271
+ if (__tlsCipher) __cf.tlsCipher = __tlsCipher;
9272
+ }
9201
9273
  request.cf = __cf;
9202
9274
  // The edge's headers are gone: from 2026-10-23 the handler cannot change the rest.
9203
9275
  __sbFreezeHeaders(request);
@@ -9227,8 +9299,8 @@ globalThis.__sbEntry = function (handlers, request) {
9227
9299
  if (rpc == null && !__sbHandles(handlers, "fetch")) throw new Error("Handler does not export a fetch() function.");
9228
9300
  __res = rpc != null
9229
9301
  ? __sbRpcDispatch(handlers, receiver, rpc, request)
9230
- : globalThis.__sbCompat >= "2026-09-28"
9231
- ? receiver.fetch(request, globalThis.env, ctx)
9302
+ : __sbCompatDate() >= "2026-09-28"
9303
+ ? receiver.fetch(request, __sbEnvValue(), ctx)
9232
9304
  : receiver.fetch(request, ctx);
9233
9305
  } catch (error) {
9234
9306
  // #179 — a synchronous throw anywhere in the handler must not take the
@@ -9260,8 +9332,8 @@ globalThis.__sbEntry = function (handlers, request) {
9260
9332
  try {
9261
9333
  const event = { cron: body.cron || "", scheduledTime: body.scheduledTime || Date.now(), noRetry() {} };
9262
9334
  const receiver = __sbReceiver(handlers, ctx);
9263
- __sres = globalThis.__sbCompat >= "2026-09-28"
9264
- ? receiver.scheduled(event, globalThis.env, ctx)
9335
+ __sres = __sbCompatDate() >= "2026-09-28"
9336
+ ? receiver.scheduled(event, __sbEnvValue(), ctx)
9265
9337
  : receiver.scheduled(event, ctx);
9266
9338
  } catch (error) {
9267
9339
  // #179 — same rule as fetch: don't let a throw here crash the process.
@@ -9389,8 +9461,8 @@ async function __sbRunQueueBatch(handlers, body, ctx) {
9389
9461
  };
9390
9462
  }
9391
9463
  try {
9392
- const __qres = globalThis.__sbCompat >= "2026-09-28"
9393
- ? handlers.queue(batch, globalThis.env, ctx)
9464
+ const __qres = __sbCompatDate() >= "2026-09-28"
9465
+ ? handlers.queue(batch, __sbEnvValue(), ctx)
9394
9466
  : handlers.queue(batch, ctx);
9395
9467
  // Was fire-and-forget: an async `queue()` had this default-ack pass run (and
9396
9468
  // the response go out) before its own `await`s did, so any ack()/retry()
@@ -1352,6 +1352,43 @@ static void sb_putjson(sb_buf* b, const char* s, size_t n) {
1352
1352
  // doesn't start a valid sequence passes through as \u00XX, the same rule as
1353
1353
  // the prelude's __sbFromUtf8. Raw byte producers (the HTTP client's body,
1354
1354
  // BLOB columns) keep sb_putjson.
1355
+ // The UTF-16 units sb_putjson_text would write as u-escapes, for a TEXT cell handed over as
1356
+ // a JS string: same validity rules (an invalid lead byte is its own unit, an overlong form, a
1357
+ // surrogate or a value above U+10FFFF is not decoded). OUT has room for n units. *wide is set
1358
+ // when a unit is above 0xff.
1359
+ static size_t sb_text_units(const unsigned char* s, size_t n, uint16_t* out, int* wide) {
1360
+ size_t k = 0;
1361
+ *wide = 0;
1362
+ for (size_t i = 0; i < n; i++) {
1363
+ unsigned char c = s[i];
1364
+ if (c < 0x80) { out[k++] = c; continue; }
1365
+ unsigned int cp = 0, min = 0;
1366
+ size_t len = 0;
1367
+ if ((c & 0xe0) == 0xc0) { cp = c & 0x1f; len = 2; min = 0x80; }
1368
+ else if ((c & 0xf0) == 0xe0) { cp = c & 0x0f; len = 3; min = 0x800; }
1369
+ else if ((c & 0xf8) == 0xf0) { cp = c & 0x07; len = 4; min = 0x10000; }
1370
+ int ok = len > 0 && i + len <= n;
1371
+ for (size_t j = 1; ok && j < len; j++) {
1372
+ unsigned char cc = s[i + j];
1373
+ if ((cc & 0xc0) != 0x80) ok = 0;
1374
+ else cp = (cp << 6) | (cc & 0x3f);
1375
+ }
1376
+ if (ok && (cp < min || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff))) ok = 0;
1377
+ if (!ok) { out[k++] = c; if (c > 0xff) *wide = 1; continue; }
1378
+ if (cp >= 0x10000) {
1379
+ cp -= 0x10000;
1380
+ out[k++] = (uint16_t)(0xd800 + (cp >> 10));
1381
+ out[k++] = (uint16_t)(0xdc00 + (cp & 0x3ff));
1382
+ *wide = 1;
1383
+ } else {
1384
+ out[k++] = (uint16_t)cp;
1385
+ if (cp > 0xff) *wide = 1;
1386
+ }
1387
+ i += len - 1;
1388
+ }
1389
+ return k;
1390
+ }
1391
+
1355
1392
  static void sb_putjson_text(sb_buf* b, const char* s, size_t n) {
1356
1393
  sb_put(b, "\"", 1);
1357
1394
  for (size_t i = 0; i < n; i++) {
@@ -2122,7 +2159,29 @@ static char* sb_sql_error(sqlite3* db, const char* message, int code, int skip)
2122
2159
  // {"ok":true,"cols":[...],"rows":[[...]],"changes":n,"rowid":n}
2123
2160
  // or {"ok":false,"error":"...","code":n,"offset":n}. In a D1 database (a path under
2124
2161
  // /d1/) a BLOB comes back as {"$blob":"HEX"}; elsewhere as one char per byte.
2125
- static char* sb_sql_run(const char* path, const char* sql, const char* params) {
2162
+ // A strict D1 query can hand its rows back as cells instead of JSON text: the caller turns
2163
+ // them into JS arrays, with no JSON written here and none parsed there. t is 0 NULL,
2164
+ // 1 number, 2 TEXT (UTF-8 in p), 3 BLOB (bytes in p).
2165
+ typedef struct { unsigned char t; double d; char* p; int n; } sb_cell;
2166
+ typedef struct { sb_cell* c; size_t n; size_t cap; int ncol; int used; } sb_cells;
2167
+ static void sb_cells_free(sb_cells* cs) {
2168
+ if (!cs) return;
2169
+ for (size_t i = 0; i < cs->n; i++) free(cs->c[i].p);
2170
+ free(cs->c);
2171
+ cs->c = 0; cs->n = cs->cap = 0;
2172
+ }
2173
+ static sb_cell* sb_cells_add(sb_cells* cs) {
2174
+ if (cs->n == cs->cap) {
2175
+ size_t cap = cs->cap ? cs->cap * 2 : 64;
2176
+ sb_cell* grown = (sb_cell*)realloc(cs->c, cap * sizeof(sb_cell));
2177
+ if (!grown) return 0;
2178
+ cs->c = grown; cs->cap = cap;
2179
+ }
2180
+ sb_cell* cell = &cs->c[cs->n++];
2181
+ cell->t = 0; cell->d = 0; cell->p = 0; cell->n = 0;
2182
+ return cell;
2183
+ }
2184
+ static char* sb_sql_run_x(const char* path, const char* sql, const char* params, sb_cells* cells) {
2126
2185
  sb_buf b = { 0, 0, 0 };
2127
2186
  int idx = sb_db_for(path);
2128
2187
  if (idx < 0 && sb_db_capture_failed(path)) return sb_sql_error(0, SB_LTX_FAILED, 10, 0);
@@ -2151,8 +2210,30 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
2151
2210
  sb_putjson_text(&b, name ? name : "", name ? strlen(name) : 0);
2152
2211
  }
2153
2212
  sb_puts(&b, "],\"rows\":[");
2213
+ // Cells only for a strict D1 statement: its BLOBs are byte arrays either way.
2214
+ const int native = cells && strict && d1;
2215
+ if (native) { cells->ncol = ncol; cells->used = 1; }
2154
2216
  int rc, first = 1;
2155
2217
  while ((rc = sqlite3_step(st)) == SB_SQLITE_ROW) {
2218
+ if (native) {
2219
+ for (int i = 0; i < ncol; i++) {
2220
+ sb_cell* cell = sb_cells_add(cells);
2221
+ if (!cell) { cells->used = -1; break; }
2222
+ int t = sqlite3_column_type(st, i);
2223
+ if (t == 5) continue;
2224
+ if (t == 1 || t == 2) { cell->t = 1; cell->d = sqlite3_column_double(st, i); continue; }
2225
+ const unsigned char* txt = t == 4 ? (const unsigned char*)sqlite3_column_blob(st, i) : sqlite3_column_text(st, i);
2226
+ int n = sqlite3_column_bytes(st, i);
2227
+ cell->t = t == 4 ? 3 : 2;
2228
+ cell->n = txt ? n : 0;
2229
+ cell->p = (char*)malloc((size_t)cell->n + 1);
2230
+ if (!cell->p) { cells->used = -1; break; }
2231
+ if (cell->n) memcpy(cell->p, txt, (size_t)cell->n);
2232
+ cell->p[cell->n] = 0;
2233
+ }
2234
+ if (cells->used < 0) break;
2235
+ continue;
2236
+ }
2156
2237
  if (!first) sb_puts(&b, ",");
2157
2238
  first = 0;
2158
2239
  sb_puts(&b, "[");
@@ -2188,6 +2269,7 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
2188
2269
  // Reset, don't finalize: the statement stays compiled in sb_stmt_cache. Reset
2189
2270
  // now (not just on next reuse) so a SELECT stops holding its read transaction.
2190
2271
  sqlite3_reset(st);
2272
+ if (cells && cells->used < 0) { free(b.p); sb_cells_free(cells); return sb_sql_error(db, "out of memory", 7, 0); }
2191
2273
  // The statement's own error, kept before LTX capture runs: capture executes statements
2192
2274
  // of its own, after which SQLite reports "not an error".
2193
2275
  char failure[512] = { 0 };
@@ -2200,11 +2282,15 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
2200
2282
  if (capture_failed) rc = 10;
2201
2283
  if (rc != SB_SQLITE_DONE && rc != SB_SQLITE_ROW) {
2202
2284
  free(b.p);
2285
+ if (cells) sb_cells_free(cells);
2203
2286
  if (capture_failed) return sb_sql_error(db, SB_LTX_FAILED, 10, 0);
2204
2287
  return sb_sql_error(db, failure, failure_code, 0);
2205
2288
  }
2206
2289
  return b.p;
2207
2290
  }
2291
+ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
2292
+ return sb_sql_run_x(path, sql, params, 0);
2293
+ }
2208
2294
 
2209
2295
  // The storage quota (docs: an app's data directory is capped): bytes in regular files under
2210
2296
  // dir, symlinks not followed. Bounded by depth; an unreadable entry counts as 0.
@@ -4131,6 +4217,85 @@ function __sbSqlRaw(path, sql, paramsJson) {
4131
4217
  return res;
4132
4218
  }
4133
4219
 
4220
+ // One strict D1 statement: [header JSON, rows]. The header is {"ok":true,"cols":[...],"changes":n,
4221
+ // "rowid":n} (or the error reply) and rows is an array of row arrays built here from the
4222
+ // cells, BLOBs as byte arrays, so no JSON for the rows is written or parsed. rows is
4223
+ // undefined when the reply is not a result.
4224
+ // oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
4225
+ function __sbSqlRowsRaw(path, sql, paramsJson) {
4226
+ let res = "";
4227
+ // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
4228
+ Porffor.c`
4229
+ const char* __p; size_t __pl; char* __po = 0;
4230
+ porf_native_fetch_read_value(path, &__p, &__pl, &__po);
4231
+ char* __path = (char*)malloc(__pl + 1); memcpy(__path, __p, __pl); __path[__pl] = 0;
4232
+ if (__po) free(__po);
4233
+
4234
+ const char* __s; size_t __sl; char* __so = 0;
4235
+ porf_native_fetch_read_value(sql, &__s, &__sl, &__so);
4236
+ char* __sql = (char*)malloc(__sl + 1); memcpy(__sql, __s, __sl); __sql[__sl] = 0;
4237
+ if (__so) free(__so);
4238
+
4239
+ const char* __a; size_t __al; char* __ao = 0;
4240
+ porf_native_fetch_read_value(paramsJson, &__a, &__al, &__ao);
4241
+ char* __args = (char*)malloc(__al + 1); memcpy(__args, __a, __al); __args[__al] = 0;
4242
+ if (__ao) free(__ao);
4243
+
4244
+ sb_cells __cells = { 0, 0, 0, 0, 0 };
4245
+ char* __out = sb_sql_run_x(__path, __sql, __args, &__cells);
4246
+ free(__path); free(__sql); free(__args);
4247
+ const u32 __pair = porf_arr_new(2, 2);
4248
+ res = porf_box((f64)__pair, 72);
4249
+ if (!__out) __out = strdup("{\"ok\":false,\"error\":\"no result\"}");
4250
+ porf_arr_set(__pair, 0, porf_box((f64)porf_native_fetch_alloc_bytestring(__out, strlen(__out)), 195));
4251
+ free(__out);
4252
+ if (__cells.used == 1) {
4253
+ const size_t __ncol = (size_t)__cells.ncol;
4254
+ const size_t __nrow = __ncol ? __cells.n / __ncol : 0;
4255
+ const u32 __rows = porf_arr_new(0, (i32)(__nrow ? __nrow : 1));
4256
+ porf_arr_set(__pair, 1, porf_box((f64)__rows, 72));
4257
+ for (size_t __r = 0; __r < __nrow; __r++) {
4258
+ const u32 __row = porf_arr_new((i32)__ncol, (i32)__ncol);
4259
+ porf_arr_set(__rows, (u32)__r, porf_box((f64)__row, 72));
4260
+ for (size_t __c = 0; __c < __ncol; __c++) {
4261
+ const sb_cell* __cell = &__cells.c[__r * __ncol + __c];
4262
+ jsval __v = porf_box(0.0, 7);
4263
+ if (__cell->t == 1) __v = porf_box_num(__cell->d);
4264
+ else if (__cell->t == 2) {
4265
+ const size_t __n = (size_t)__cell->n;
4266
+ uint16_t* __u = (uint16_t*)malloc(__n * 2 + 2);
4267
+ if (__u) {
4268
+ int __wide = 0;
4269
+ const size_t __k = sb_text_units((const unsigned char*)__cell->p, __n, __u, &__wide);
4270
+ if (__wide) {
4271
+ const u32 __str = porf_native_fetch_alloc((u32)(4 + __k * 2), 67);
4272
+ *(u32*)(MEM + __str) = (u32)__k;
4273
+ memcpy(MEM + __str + 4, __u, __k * 2);
4274
+ __v = porf_box((f64)__str, 67);
4275
+ } else {
4276
+ unsigned char* __b = (unsigned char*)malloc(__k + 1);
4277
+ if (__b) {
4278
+ for (size_t __i = 0; __i < __k; __i++) __b[__i] = (unsigned char)__u[__i];
4279
+ __v = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__b, __k), 195);
4280
+ free(__b);
4281
+ }
4282
+ }
4283
+ free(__u);
4284
+ }
4285
+ } else if (__cell->t == 3) {
4286
+ const u32 __bytes = porf_arr_new((i32)__cell->n, (i32)__cell->n);
4287
+ for (int __i = 0; __i < __cell->n; __i++) porf_arr_set(__bytes, (u32)__i, porf_box_num((f64)(unsigned char)__cell->p[__i]));
4288
+ __v = porf_box((f64)__bytes, 72);
4289
+ }
4290
+ porf_arr_set(__row, (u32)__c, __v);
4291
+ }
4292
+ }
4293
+ }
4294
+ sb_cells_free(&__cells);
4295
+ `;
4296
+ return res;
4297
+ }
4298
+
4134
4299
  // --- the op dispatch, in JS -------------------------------------------------
4135
4300
  // Deliberately the same SQL and the same partition keys as broker.ts. When one
4136
4301
  // changes the other has to, and the conformance suite is what says so.
@@ -4621,7 +4786,7 @@ function __sbEmbeddedDispatch(msg) {
4621
4786
  __sbD1Quota(path);
4622
4787
  if (op === "d1.batch") return { ok: true, results: __sbD1Batch(path, msg.statements || [], strict) };
4623
4788
  const one = __sbD1Run(path, msg.sql, msg.params, strict);
4624
- return { ok: true, results: one.results, columns: one.columns, rows: one.rows, meta: one.meta, success: true };
4789
+ return { ok: true, results: one.results, columns: one.columns, rows: one.rows, native: one.native, meta: one.meta, success: true };
4625
4790
  }
4626
4791
 
4627
4792
  // Durable Objects storage.sql (compatibility date 2026-10-29): one database per object,
@@ -5168,6 +5333,14 @@ function __sbD1Bytes(cell) {
5168
5333
  return out;
5169
5334
  }
5170
5335
  function __sbD1Run(path, sql, params, strict) {
5336
+ if (strict) {
5337
+ // Rows come back as arrays, built in C (see __sbSqlRowsRaw); the header is small JSON.
5338
+ const pair = __sbSqlRowsRaw(path, "/*sb-d1-strict*/" + String(sql), JSON.stringify(params || []));
5339
+ const head = JSON.parse(pair[0]);
5340
+ if (head.ok === false) throw __sbD1Error(head);
5341
+ const rows = pair[1];
5342
+ return { columns: head.cols, rows, native: true, meta: { changes: head.changes, last_row_id: head.rowid, rows_read: rows.length } };
5343
+ }
5171
5344
  const r = JSON.parse(__sbSqlRaw(path, (strict ? "/*sb-d1-strict*/" : "") + String(sql), JSON.stringify(params || [])));
5172
5345
  if (r.ok === false) throw strict ? __sbD1Error(r) : new Error("sqlite: " + r.error);
5173
5346
  const meta = { changes: r.changes, last_row_id: r.rowid, rows_read: r.rows.length };
@@ -5323,7 +5496,7 @@ function __sbLtxConfigure() {
5323
5496
  }
5324
5497
 
5325
5498
  function __sbCall(reqJson) {
5326
- if (globalThis.__sbCompat >= "2026-10-22") __sbLtxConfigure();
5499
+ if (__sbCompatDate() >= "2026-10-22") __sbLtxConfigure();
5327
5500
  try {
5328
5501
  return JSON.stringify(__sbEmbeddedDispatch(JSON.parse(reqJson)));
5329
5502
  } catch (err) {
@@ -5331,6 +5504,12 @@ function __sbCall(reqJson) {
5331
5504
  }
5332
5505
  }
5333
5506
 
5507
+ // The same dispatch without the JSON text either side: D1 calls take this (see __sbRpcDirect).
5508
+ __sbCallDirect = function (req) {
5509
+ if (__sbCompatDate() >= "2026-10-22") __sbLtxConfigure();
5510
+ return __sbEmbeddedDispatch(req);
5511
+ };
5512
+
5334
5513
  // --- local triggers ---------------------------------------------------------
5335
5514
  // A deployed sprout is driven by the broker: it POSTs x-sb-trigger for cron
5336
5515
  // ticks, queue batches and DO alarms. An embedded binary has no broker, so the
@@ -5710,8 +5889,8 @@ globalThis.__sbStartLocalTriggers = function (handlers, bindings) {
5710
5889
  try {
5711
5890
  const receiver = __sbReceiver(handlers, ctx);
5712
5891
  const event = { cron: crons[i], scheduledTime: now.getTime(), noRetry() {} };
5713
- const res = globalThis.__sbCompat >= "2026-09-28"
5714
- ? receiver.scheduled(event, globalThis.env, ctx)
5892
+ const res = __sbCompatDate() >= "2026-09-28"
5893
+ ? receiver.scheduled(event, __sbEnvValue(), ctx)
5715
5894
  : receiver.scheduled(event, ctx);
5716
5895
  if (res && __sbIsFn(res.then)) await res;
5717
5896
  } catch (error) {
package/src/wrap.ts CHANGED
@@ -234,7 +234,7 @@ export function wrapNativeFetchHandler(
234
234
  );
235
235
  }
236
236
 
237
- const env = `const env = ${JSON.stringify(vars)};\nglobalThis.env = env;\n`;
237
+ const env = `const env = ${JSON.stringify(vars)};\nglobalThis.env = env;\n__sbSetEnv(env);\n`;
238
238
  const versionMetadataLine = versionMetadata
239
239
  ? `env[${JSON.stringify(versionMetadata.binding)}] = ${JSON.stringify({
240
240
  id: versionMetadata.id,
@@ -246,6 +246,7 @@ export function wrapNativeFetchHandler(
246
246
  // not be able to change the semantics it was compiled against at runtime.
247
247
  const compat =
248
248
  `globalThis.__sbCompat = ${JSON.stringify(compatibilityDate)};\n` +
249
+ `__sbSetCompat(${JSON.stringify(compatibilityDate)});\n` +
249
250
  // #15 — the embedded transport derives its default data directory from this.
250
251
  `globalThis.__sbAppName = ${JSON.stringify(appName)};\n` +
251
252
  (assets ? `globalThis.__sbAssets = ${JSON.stringify(assets)};\n` : "");
@@ -0,0 +1,129 @@
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
+ // The x-sb-cpu-ms stamp is added to the handler's own Response (it used to build a second one),
9
+ // and request.cf's client IP skips the trusted-proxy work when no proxy is configured. What goes on
10
+ // the wire must not change: which responses are stamped, the headers and the body, the client IP.
11
+ const handler = `export default {fetch(request) {
12
+ const path = new URL(request.url).pathname;
13
+ if (path === "/text") return new Response("hello");
14
+ if (path === "/json") return new Response('{"a":1}', { status: 201, headers: { "content-type": "application/json", "x-a": "1" } });
15
+ if (path === "/bytes") return new Response(new Uint8Array([1, 2, 3]));
16
+ if (path === "/cookies") {
17
+ const h = new Headers(); h.append("set-cookie", "a=1"); h.append("set-cookie", "b=2");
18
+ return new Response("c", { headers: h });
19
+ }
20
+ if (path === "/own") return new Response("own", { headers: { "x-sb-cpu-ms": "mine" } });
21
+ if (path === "/ip") return new Response(JSON.stringify({ ip: request.cf.clientIp, proto: request.cf.httpProtocol ?? null }));
22
+ return new Response("not found", { status: 404 });
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 = await mkdtemp(join(tmpdir(), "sb-native-cpu-stamp-"));
33
+ const servers: ReturnType<typeof Bun.spawn>[] = [];
34
+ async function start(binary: string, env: Record<string, string>): Promise<number> {
35
+ const port = freePort();
36
+ const server = Bun.spawn([binary], {
37
+ cwd: workdir,
38
+ env: { ...process.env, ...env, PORT: String(port) },
39
+ stdout: "ignore",
40
+ stderr: "inherit",
41
+ });
42
+ servers.push(server);
43
+ for (let attempt = 0; attempt < 50; attempt++) {
44
+ try {
45
+ await fetch(`http://127.0.0.1:${port}/text`, { signal: AbortSignal.timeout(500) });
46
+ return port;
47
+ } catch {
48
+ if (server.exitCode !== null) break;
49
+ await Bun.sleep(100);
50
+ }
51
+ }
52
+ throw new Error("compiled handler did not start");
53
+ }
54
+ try {
55
+ const porffor = await ensurePorffor();
56
+ const node = Bun.which("node");
57
+ if (!node) throw new Error("native cpu stamp test needs node on PATH");
58
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
59
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
60
+ const generated = join(workdir, "stamp.generated.js");
61
+ const binary = join(workdir, "stamp-native");
62
+ await writeFile(generated, wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, broker)));
63
+ const compile = Bun.spawn(
64
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
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(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
78
+
79
+ const port = await start(binary, {});
80
+ const get = (path: string, headers: Record<string, string> = {}, p = port) =>
81
+ fetch(`http://127.0.0.1:${p}${path}`, { headers });
82
+
83
+ const text = await get("/text");
84
+ assert.equal(await text.text(), "hello");
85
+ assert.match(text.headers.get("x-sb-cpu-ms") ?? "", /^\d+\.\d{3}$/, "a string body is stamped");
86
+ assert.equal(text.headers.get("content-type"), "text/plain;charset=UTF-8");
87
+
88
+ const json = await get("/json");
89
+ assert.equal(json.status, 201);
90
+ assert.equal(await json.text(), '{"a":1}');
91
+ assert.equal(json.headers.get("x-a"), "1");
92
+ assert.equal(json.headers.get("content-type"), "application/json");
93
+ assert.match(json.headers.get("x-sb-cpu-ms") ?? "", /^\d+\.\d{3}$/);
94
+
95
+ const notFound = await get("/missing");
96
+ assert.equal(notFound.status, 404);
97
+ assert.match(notFound.headers.get("x-sb-cpu-ms") ?? "", /^\d+\.\d{3}$/);
98
+
99
+ const bytes = await get("/bytes");
100
+ assert.deepEqual([...new Uint8Array(await bytes.arrayBuffer())], [1, 2, 3]);
101
+ assert.equal(bytes.headers.get("x-sb-cpu-ms"), null, "a binary body is not stamped");
102
+
103
+ const cookies = await get("/cookies");
104
+ assert.equal(await cookies.text(), "c");
105
+ assert.deepEqual(cookies.headers.getSetCookie(), ["a=1", "b=2"]);
106
+ assert.equal(cookies.headers.get("x-sb-cpu-ms"), null, "a Set-Cookie response is not stamped");
107
+
108
+ const own = await get("/own");
109
+ assert.match(own.headers.get("x-sb-cpu-ms") ?? "", /^\d+\.\d{3}$/, "the measured value replaces a handler-set one");
110
+
111
+ for (let i = 0; i < 5; i++) assert.equal(await (await get("/text")).text(), "hello");
112
+
113
+ // No trusted proxy: the peer is the client, forwarded headers do not count.
114
+ const plain = await (await get("/ip", { "x-forwarded-for": "203.0.113.9", "x-sb-http-protocol": "h2" })).json();
115
+ assert.deepEqual(plain, { ip: "127.0.0.1", proto: null });
116
+ // The peer is a trusted proxy: the forwarded client and protocol count.
117
+ const proxied = await start(binary, { SB_TRUSTED_PROXIES: "127.0.0.1" });
118
+ const forwarded = await (
119
+ await get("/ip", { "x-forwarded-for": "203.0.113.9", "x-sb-http-protocol": "h2" }, proxied)
120
+ ).json();
121
+ assert.deepEqual(forwarded, { ip: "203.0.113.9", proto: "h2" });
122
+ console.log("Native x-sb-cpu-ms stamp and client IP: same responses on the wire, no second Response");
123
+ } finally {
124
+ for (const server of servers) {
125
+ server.kill(9);
126
+ await server.exited;
127
+ }
128
+ await rm(workdir, { recursive: true, force: true });
129
+ }
package/test/native-d1.ts CHANGED
@@ -51,6 +51,33 @@ int main(void) {
51
51
  char* params = strchr(sql, '\\t'); if (!params) return 2; *params++ = 0;
52
52
  if (!strncmp(line, ":db ", 4)) { int r = sql[0] == 'p' ? sb_db_page_size(line + 4) : sb_db_quota_freeze(line + 4, sql[0] == 'c' ? -1 : sql[0] == 'f'); printf("{\\"n\\":%d}\\n", r); fflush(stdout); continue; }
53
53
  if (!strncmp(line, ":bytes ", 7)) { line[strcspn(line, "\\t")] = 0; printf("{\\"n\\":%lld}\\n", sb_dir_bytes(line + 7, 0)); fflush(stdout); continue; }
54
+ // ":cells PATH": the same statement through sb_sql_run_x with cells, printed as JSON the test
55
+ // can compare with sb_sql_run's rows: TEXT as \\uXXXX units (sb_text_units), BLOB as {"$blob":hex}.
56
+ if (!strncmp(line, ":cells ", 7)) {
57
+ sb_cells cs = { 0, 0, 0, 0, 0 };
58
+ char* head = sb_sql_run_x(line + 7, sql, params, &cs);
59
+ printf("{\\"head\\":%s,\\"rows\\":[", head);
60
+ size_t nrow = cs.ncol ? cs.n / (size_t)cs.ncol : 0;
61
+ for (size_t r = 0; r < nrow; r++) {
62
+ printf(r ? ",[" : "[");
63
+ for (int c = 0; c < cs.ncol; c++) {
64
+ sb_cell* x = &cs.c[r * (size_t)cs.ncol + (size_t)c];
65
+ if (c) printf(",");
66
+ if (x->t == 0) printf("null");
67
+ else if (x->t == 1) printf("%.17g", x->d);
68
+ else if (x->t == 2) {
69
+ uint16_t* u = (uint16_t*)malloc((size_t)x->n * 2 + 2); int wide = 0;
70
+ size_t k = sb_text_units((const unsigned char*)x->p, (size_t)x->n, u, &wide);
71
+ printf("\\"");
72
+ for (size_t i = 0; i < k; i++) printf("\\\\u%04x", u[i]);
73
+ printf("\\"");
74
+ free(u);
75
+ } else { printf("{\\"$blob\\":\\""); for (int i = 0; i < x->n; i++) printf("%02x", (unsigned char)x->p[i]); printf("\\"}"); }
76
+ }
77
+ printf("]");
78
+ }
79
+ printf("]}\\n"); fflush(stdout); sb_cells_free(&cs); free(head); continue;
80
+ }
54
81
  // ":script PATH" runs SQL as a script (sb_sql_script, what exec() uses).
55
82
  char* out = !strncmp(line, ":script ", 8) ? sb_sql_script(line + 8, sql) : sb_sql_run(line, sql, params); puts(out); fflush(stdout); free(out);
56
83
  }
@@ -91,6 +118,59 @@ int main(void) {
91
118
  );
92
119
  assert.equal(other.rows[0][0], String(other.rows[0][0]), "outside D1 a BLOB is still one char per byte, not hex");
93
120
 
121
+ // Strict rows handed over as cells (the JS arrays are built from them in C) hold exactly what
122
+ // the JSON rows hold: NULL, integers, floats, ASCII, Latin-1, BMP and astral text, every kind
123
+ // of invalid UTF-8, a BLOB, and no rows at all.
124
+ const cellRun = (sql: string, params: unknown[] = []) => {
125
+ const input = `:cells ${d1}\t/*sb-d1-strict*/${sql}\t${JSON.stringify(params)}\n${d1}\t/*sb-d1-strict*/${sql}\t${JSON.stringify(params)}\n`;
126
+ const out = Bun.spawnSync([binary], { stdin: Buffer.from(input) });
127
+ assert.equal(out.exitCode, 0, out.stderr.toString());
128
+ const [cells, json] = out.stdout
129
+ .toString()
130
+ .trim()
131
+ .split("\n")
132
+ .map((l) => JSON.parse(l));
133
+ return { cells, json };
134
+ };
135
+ const bad = [
136
+ "x'80'",
137
+ "x'c0af'",
138
+ "x'c3'",
139
+ "x'e282'",
140
+ "x'f09f9880'",
141
+ "x'f0'",
142
+ "x'eda080'",
143
+ "x'f4908080'",
144
+ "x'e0809a'",
145
+ "x'41c342'",
146
+ "x'ffff'",
147
+ "x'c3a9e282'",
148
+ ];
149
+ const texts = [
150
+ "''",
151
+ "'plain ascii'",
152
+ "'caf\u00e9'",
153
+ "'\u4e1c\u4eac'",
154
+ "'x\u{1F680}y'",
155
+ "'\u00ff\u0100'",
156
+ "char(113, 34, 92, 10, 9, 1, 127)",
157
+ ...bad.map((b) => `CAST(${b} AS TEXT)`),
158
+ ];
159
+ for (const t of texts) {
160
+ const { cells, json } = cellRun(`SELECT ${t} AS t, 7 AS i, 2.5 AS f, NULL AS n, x'00ff7f' AS b, ${t} || 'z' AS u`);
161
+ assert.equal(cells.head.ok, true);
162
+ assert.deepEqual(cells.rows, json.rows, `cells equal the JSON rows for ${t}`);
163
+ assert.deepEqual(cells.head.cols, json.cols);
164
+ assert.equal(cells.head.changes, json.changes);
165
+ }
166
+ const empty = cellRun("SELECT a FROM t WHERE 0");
167
+ assert.deepEqual(empty.cells.rows, []);
168
+ assert.deepEqual(empty.json.rows, []);
169
+ const wide = cellRun("SELECT * FROM t");
170
+ assert.deepEqual(wide.cells.rows, wide.json.rows, "a table with a BLOB and NULLs");
171
+ const failed = cellRun("SELECT nope FROM nowhere");
172
+ assert.equal(failed.cells.head.ok, false);
173
+
94
174
  // An object that is not a blob binds NULL and the parse carries on (a "}" in a string included).
95
175
  const [, kept] = run([
96
176
  [d1, "INSERT INTO t VALUES (?1, ?2, ?3)", [{ a: "}{" }, 5, "x"]],
@@ -172,12 +252,23 @@ int main(void) {
172
252
  };
173
253
  // oxlint-disable-next-line no-function-constructor -- lifting a block out of the text-only transport
174
254
  const scriptRaw = (path: string, text: string) => sqlRaw(":script " + path, text, "[]");
255
+ // __sbSqlRowsRaw as the transport's C block gives it: [header JSON, rows], BLOBs as byte arrays.
256
+ const rowsOf =
257
+ (raw: (path: string, text: string, params: string) => string) => (path: string, text: string, params: string) => {
258
+ const r = JSON.parse(raw(":cells " + path, text, params));
259
+ if (r.head.ok === false) return [JSON.stringify(r.head), undefined];
260
+ const rows = r.rows.map((row: unknown[]) =>
261
+ row.map((c) => (c instanceof Object && "$blob" in c ? [...Buffer.from(String(c.$blob), "hex")] : c)),
262
+ );
263
+ return [JSON.stringify(r.head), rows];
264
+ };
175
265
  const ops = new Function(
176
266
  "__sbSqlRaw",
177
267
  "__sbSql",
178
268
  "__sbSqlScriptRaw",
269
+ "__sbSqlRowsRaw",
179
270
  `${transport.slice(opsStart, opsEnd)}\nreturn { __sbD1Run, __sbD1Batch, __sbD1Open, __sbD1ExecLines };`,
180
- )(sqlRaw, sql, scriptRaw);
271
+ )(sqlRaw, sql, scriptRaw, rowsOf(sqlRaw));
181
272
  // The storage quota, lifted from "The storage quota" to __sbStore, for DIR and LIMIT.
182
273
  const quotaStart = transport.indexOf("// The storage quota: an app's data directory");
183
274
  const quotaEnd = transport.indexOf("function __sbStore() {");
@@ -233,8 +324,9 @@ int main(void) {
233
324
  "__sbSqlRaw",
234
325
  "__sbSql",
235
326
  "__sbSqlScriptRaw",
327
+ "__sbSqlRowsRaw",
236
328
  `${transport.slice(opsStart, opsEnd)}\nreturn { __sbD1Run, __sbD1Batch, __sbD1Open, __sbD1ExecLines };`,
237
- )(raw, run, (path: string, text: string) => raw(":script " + path, text, "[]"));
329
+ )(raw, run, (path: string, text: string) => raw(":script " + path, text, "[]"), rowsOf(raw));
238
330
  const lift = (dir: string, limit: number) =>
239
331
  // oxlint-disable-next-line no-function-constructor -- lifting a block out of the text-only transport
240
332
  new Function(
@@ -312,7 +404,7 @@ int main(void) {
312
404
  "D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_FOREIGNKEY)",
313
405
  );
314
406
  const strictRow = ops.__sbD1Run(db2, "SELECT 1 AS a, ?1 AS a", [{ $blob: "0a" }], true);
315
- assert.deepEqual([strictRow.columns, strictRow.rows], [["a", "a"], [[1, { $blob: "0a" }]]]);
407
+ assert.deepEqual([strictRow.columns, strictRow.rows], [["a", "a"], [[1, [10]]]]);
316
408
  assert.throws(
317
409
  () => ops.__sbD1Run(db2, "SELEC 1", [], true),
318
410
  (e: Error) => e.message === 'D1_ERROR: near "SELEC": syntax error at offset 0: SQLITE_ERROR',
@@ -413,6 +505,7 @@ int main(void) {
413
505
  "__sbSqlRaw",
414
506
  "__sbSql",
415
507
  "__sbSqlScriptRaw",
508
+ "__sbSqlRowsRaw",
416
509
  `${transport.slice(opsStart, opsEnd)}\nreturn { __sbD1Run, __sbD1Batch };`,
417
510
  )(
418
511
  (path: string, text: string, params: string) => {
@@ -421,6 +514,7 @@ int main(void) {
421
514
  },
422
515
  sql,
423
516
  scriptRaw,
517
+ rowsOf(sqlRaw),
424
518
  );
425
519
  const LTX_FAILED =
426
520
  "D1_ERROR: LTX capture failed: the transaction may be committed locally but is not durable in the replication log; writes are refused until restart: SQLITE_IOERR";
@@ -0,0 +1,131 @@
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
+ // JSON.stringify of strings as the spec defines it (Bun is the reference): quotes and backslash,
9
+ // every control character, U+2028 and U+2029 (not escaped), DEL, Latin-1 and wider text, astral
10
+ // characters, and lone or misordered surrogates (escaped as \\udXXX). Each input is built from
11
+ // code units in the handler, so the compiled string has whatever layout concatenation gives it.
12
+ const units = (s: string) => Array.from({ length: s.length }, (_, i) => s.charCodeAt(i));
13
+ const everyControl = Array.from({ length: 0x20 }, (_, i) => String.fromCharCode(i)).join("");
14
+ const INPUTS: string[] = [
15
+ "",
16
+ "abc",
17
+ 'a"b\\c"',
18
+ everyControl,
19
+ "x" + everyControl + "y",
20
+ "\u007f\u0080\u00ff",
21
+ "caf\u00e9",
22
+ "line\u2028sep\u2029end",
23
+ "\u0100",
24
+ "\u4e1c\u4eac",
25
+ "x\u{1F680}y",
26
+ "\u{1F680}".repeat(40),
27
+ "\ud800",
28
+ "\udc00",
29
+ "\ud800x",
30
+ "x\udc00",
31
+ "\udc00\ud800",
32
+ "\ud800\ud800\udc00",
33
+ "a\ud83dz",
34
+ "\ud83d\ude80\ud83d",
35
+ "tab\there\nnewline\r\f\b",
36
+ "plain ascii text that is long enough to run the loop for a while. ".repeat(60),
37
+ 'mix \u00e9\u4e1c\u{1F680}"\\\n\ud800 '.repeat(50),
38
+ ];
39
+ const handler = `export default {fetch() {
40
+ const inputs = ${JSON.stringify(INPUTS.map(units))};
41
+ const out = [];
42
+ for (let n = 0; n < inputs.length; n++) {
43
+ let s = "";
44
+ for (let i = 0; i < inputs[n].length; i++) s += String.fromCharCode(inputs[n][i]);
45
+ out.push(JSON.stringify(s));
46
+ out.push(JSON.stringify({ [s]: s, list: [s, 1, null], nested: { k: s } }));
47
+ }
48
+ return new Response(out.join("\\u0001"));
49
+ }};`;
50
+ const want = INPUTS.flatMap((s) => [
51
+ JSON.stringify(s),
52
+ JSON.stringify({ [s]: s, list: [s, 1, null], nested: { k: s } }),
53
+ ]);
54
+ function freePort(): number {
55
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
56
+ const port = listener.port;
57
+ listener.stop(true);
58
+ return port;
59
+ }
60
+
61
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-json-strings-"));
62
+ let server: ReturnType<typeof Bun.spawn> | undefined;
63
+ try {
64
+ const porffor = await ensurePorffor();
65
+ const node = Bun.which("node");
66
+ if (!node) throw new Error("native streams test needs node on PATH");
67
+
68
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
69
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
70
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
71
+ const generated = join(workdir, "json-strings.generated.js");
72
+ const binary = join(workdir, "json-strings-native");
73
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
74
+ await rm(binary, { force: true });
75
+
76
+ const compile = Bun.spawn(
77
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
78
+ {
79
+ cwd: workdir,
80
+ env: process.env,
81
+ stdout: "pipe",
82
+ stderr: "pipe",
83
+ },
84
+ );
85
+ const [exitCode, stdout, stderr] = await Promise.all([
86
+ compile.exited,
87
+ new Response(compile.stdout).text(),
88
+ new Response(compile.stderr).text(),
89
+ ]);
90
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
91
+
92
+ const port = freePort();
93
+ server = Bun.spawn([binary], {
94
+ cwd: workdir,
95
+ env: { ...process.env, PORT: String(port) },
96
+ stdout: "ignore",
97
+ stderr: "inherit",
98
+ });
99
+ let ready = false;
100
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
101
+ try {
102
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
103
+ ready = true;
104
+ } catch {
105
+ if (server.exitCode !== null) break;
106
+ await Bun.sleep(100);
107
+ }
108
+ }
109
+ assert(ready, "compiled handler did not start");
110
+ const got = (await (await fetch(`http://127.0.0.1:${port}/`)).text()).split("\u0001");
111
+ assert.equal(got.length, want.length, "one result per case");
112
+ const diffs = want.flatMap((w, i) =>
113
+ got[i] === w
114
+ ? []
115
+ : [
116
+ `case ${i >> 1} (${i & 1 ? "object" : "string"})\n want ${w.slice(0, 200)}\n got ${got[i].slice(0, 200)}`,
117
+ ],
118
+ );
119
+ assert.deepEqual(
120
+ diffs,
121
+ [],
122
+ `JSON.stringify differs from the spec in ${diffs.length} of ${want.length} cases:\n${diffs.join("\n")}`,
123
+ );
124
+ console.log(`Native JSON.stringify of strings is byte-identical to the spec in all ${want.length} cases`);
125
+ } finally {
126
+ if (server) {
127
+ server.kill(9);
128
+ await server.exited;
129
+ }
130
+ await rm(workdir, { recursive: true, force: true });
131
+ }