@sproutboat/runtime 0.34.1 → 0.34.2

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,18 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.34.2
4
+
5
+ ### Patch Changes
6
+
7
+ - a0ba6a3: Faster requests: about 57% of a request's CPU was Porffor's `globalThis` getter.
8
+
9
+ - **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.
10
+ - **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.
11
+ - **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.
12
+ - **`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.
13
+
14
+ 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.
15
+
3
16
  ## 0.34.1
4
17
 
5
18
  ### 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.2",
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
 
@@ -3752,7 +3781,7 @@ if (typeof globalThis.EventTarget !== "function") {
3752
3781
  if (this.aborted) return;
3753
3782
  this.aborted = true;
3754
3783
  // 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";
3784
+ const message = __sbCompatDate() >= "2026-10-23" ? "The operation was aborted" : "This operation was aborted";
3756
3785
  this.reason = reason === undefined ? namedError("AbortError", message) : reason;
3757
3786
  const event = new Event("abort");
3758
3787
  if (typeof this.onabort === "function") this.onabort.call(this, event);
@@ -5007,7 +5036,7 @@ function __sbRpc(op, extra) {
5007
5036
  // pages with limit and cursor and returns each key's expiration and metadata. Earlier
5008
5037
  // dates keep the synchronous shim below.
5009
5038
  function __sbKvStrict() {
5010
- return globalThis.__sbCompat >= "2026-10-25";
5039
+ return __sbCompatDate() >= "2026-10-25";
5011
5040
  }
5012
5041
  function __sbKvKey(key, verb) {
5013
5042
  const name = String(key);
@@ -5131,7 +5160,7 @@ function __sbMakeKv(ns) {
5131
5160
  // are enforced, errors read "D1_ERROR: ...", and a BLOB comes back as an array of
5132
5161
  // byte values. Earlier dates keep the old behaviour.
5133
5162
  function __sbD1Strict() {
5134
- return globalThis.__sbCompat >= "2026-10-24";
5163
+ return __sbCompatDate() >= "2026-10-24";
5135
5164
  }
5136
5165
  function __sbD1Param(value, strict) {
5137
5166
  if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
@@ -5449,7 +5478,7 @@ function __sbR2Multipart(bucket, key, uploadId) {
5449
5478
  // cursor, groups by delimiter and returns metadata only when `include` asks for it.
5450
5479
  // Earlier dates keep the synchronous shim below.
5451
5480
  function __sbR2Strict() {
5452
- return globalThis.__sbCompat >= "2026-10-26";
5481
+ return __sbCompatDate() >= "2026-10-26";
5453
5482
  }
5454
5483
  function __sbR2Key(key, verb) {
5455
5484
  const name = String(key);
@@ -5790,7 +5819,7 @@ function __sbAssetResponse(path, r) {
5790
5819
  // The queue name and consumer settings (bindings.queueConfig, from `queues` in
5791
5820
  // sproutboat.jsonc) apply at every date: they are opt-in.
5792
5821
  function __sbQueuesStrict() {
5793
- return globalThis.__sbCompat >= "2026-10-31";
5822
+ return __sbCompatDate() >= "2026-10-31";
5794
5823
  }
5795
5824
 
5796
5825
  // binding -> { binding, queue, maxBatchSize?, maxBatchTimeout?, maxRetries?, retryDelay?, deadLetterQueue? }
@@ -6106,7 +6135,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
6106
6135
  sendBatch(messages, options) {
6107
6136
  // From 2026-10-28 sendBatch(messages, { delaySeconds }) delays every message that
6108
6137
  // 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;
6138
+ const batchDelay = __sbCompatDate() >= "2026-10-28" ? (options && options.delaySeconds) || 0 : 0;
6110
6139
  const list = [];
6111
6140
  for (let j = 0; j < (messages || []).length; j++) {
6112
6141
  const m = messages[j];
@@ -6193,7 +6222,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
6193
6222
  const binding = bindings.services[i].binding;
6194
6223
  const fetcher = {
6195
6224
  fetch(input, init) {
6196
- if (globalThis.__sbCompat >= "2026-10-22") return __sbServiceFetch(binding, input, init);
6225
+ if (__sbCompatDate() >= "2026-10-22") return __sbServiceFetch(binding, input, init);
6197
6226
  const parts = __sbFetchParts(input, init);
6198
6227
  const msg = { binding, url: parts.url, method: parts.method, headers: parts.headers, ...__sbPartsBody(parts) };
6199
6228
  return __sbUpstreamResponse(globalThis.__sbFetchUpstream("service.fetch", msg));
@@ -6383,7 +6412,7 @@ function __sbHandles(handlers, name) {
6383
6412
  // Callee: the receiver of one request. A default export that is a class (a
6384
6413
  // WorkerEntrypoint) is instantiated per request with its ctx and env, as in workerd.
6385
6414
  function __sbReceiver(handlers, ctx) {
6386
- return __sbIsFn(handlers) ? new handlers(ctx, globalThis.env) : handlers;
6415
+ return __sbIsFn(handlers) ? new handlers(ctx, __sbEnvValue()) : handlers;
6387
6416
  }
6388
6417
 
6389
6418
  // Callee: runs RPC method `method` of `receiver` with the arguments in the request body.
@@ -6412,7 +6441,7 @@ async function __sbRpcDispatch(handlers, receiver, method, request) {
6412
6441
  // throws), a Request given as init brings its body, GET and HEAD refuse a body, and the
6413
6442
  // method and redirect mode are checked. Before it, the parts are read off the input.
6414
6443
  function __sbFetchParts(input, init) {
6415
- if (globalThis.__sbCompat >= "2026-10-23") {
6444
+ if (__sbCompatDate() >= "2026-10-23") {
6416
6445
  const request = new Request(input, init);
6417
6446
  const headers = [];
6418
6447
  request.headers.forEach((v, k) => headers.push([k, v]));
@@ -6488,7 +6517,7 @@ function __sbUpstreamResponse(r) {
6488
6517
  function __sbStreamsBody(body) {
6489
6518
  return (
6490
6519
  body instanceof ReadableStream &&
6491
- globalThis.__sbCompat >= "2026-10-27" &&
6520
+ __sbCompatDate() >= "2026-10-27" &&
6492
6521
  globalThis.__sbBodyStreamable != null &&
6493
6522
  globalThis.__sbBodyStreamable()
6494
6523
  );
@@ -6615,7 +6644,7 @@ function __sbFollowHop(op, msg, label, mode, signal, hops) {
6615
6644
  const step = (res) => {
6616
6645
  const location = res.headers.get("location");
6617
6646
  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;
6647
+ if (!(__sbCompatDate() >= "2026-10-22") || mode === "manual" || !redirect || location == null) return res;
6619
6648
  if (mode === "error") throw new TypeError(label + ": redirect not allowed (redirect: \"error\")");
6620
6649
  if (hops >= 20) throw new TypeError(label + ": too many redirects");
6621
6650
  const dropped = __sbRawBody(res);
@@ -6652,9 +6681,9 @@ function __sbFollowHop(op, msg, label, mode, signal, hops) {
6652
6681
  // (__sbAsyncAtAnyDate). `always` is for RPC, which has no older behaviour to keep.
6653
6682
  function __sbUpstream(op, msg, label, signal, always) {
6654
6683
  if (signal) signal.throwIfAborted();
6655
- const promised = always || globalThis.__sbCompat >= "2026-10-22" || __sbAsyncAtAnyDate();
6684
+ const promised = always || __sbCompatDate() >= "2026-10-22" || __sbAsyncAtAnyDate();
6656
6685
  if (!__sbHttpAsyncEnabled() || !promised) return __sbUpstreamResponse(globalThis.__sbFetchUpstream(op, msg));
6657
- if (globalThis.__sbCompat >= "2026-10-22") return __sbUpstreamStreamed(op, msg, label, signal);
6686
+ if (__sbCompatDate() >= "2026-10-22") return __sbUpstreamStreamed(op, msg, label, signal);
6658
6687
  return new Promise((resolve, reject) => {
6659
6688
  let onAbort = null;
6660
6689
  const settled = () => {
@@ -6701,7 +6730,7 @@ globalThis.__sbInstallFetch = function () {
6701
6730
  parts = __sbFetchParts(input, init);
6702
6731
  } catch (error) {
6703
6732
  // 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);
6733
+ if (__sbCompatDate() >= "2026-10-23") return Promise.reject(error);
6705
6734
  throw error;
6706
6735
  }
6707
6736
  if (__sbWsOutboundOn() && __sbWsIsUpgrade(parts)) return __sbWsFetchUpgrade(parts);
@@ -6762,7 +6791,7 @@ function __sbMakeDONamespace(binding, className) {
6762
6791
  let req;
6763
6792
  // From 2026-10-23 the object gets the Request Fetch makes of input and init, as in
6764
6793
  // workerd: method, headers and body survive, and the input's body is used up.
6765
- if (globalThis.__sbCompat >= "2026-10-23") {
6794
+ if (__sbCompatDate() >= "2026-10-23") {
6766
6795
  req = new Request(input, init);
6767
6796
  } else if (__sbIsObj(input) && __sbIsStr(input.url) && !init) {
6768
6797
  req = input;
@@ -6814,7 +6843,7 @@ function __sbMakeDONamespace(binding, className) {
6814
6843
  // The object runs in this sprout on every transport (see above), so an RPC call is a
6815
6844
  // direct call; only its storage goes to the broker.
6816
6845
  function __sbDOv2() {
6817
- return globalThis.__sbCompat >= "2026-10-29";
6846
+ return __sbCompatDate() >= "2026-10-29";
6818
6847
  }
6819
6848
 
6820
6849
  function __sbDOSha(text) {
@@ -7060,7 +7089,7 @@ function __sbGetDOInstance(cls, idText, idObj) {
7060
7089
  // The hibernation API, from 2026-10-30 (where WebSocketPair exists).
7061
7090
  const holder = { inst: null, auto: null, cls, id, restored: [] };
7062
7091
  if (__sbWsEnabled()) __sbDOWebSocketState(state, holder);
7063
- inst = new Ctor(state, globalThis.env);
7092
+ inst = new Ctor(state, __sbEnvValue());
7064
7093
  holder.inst = inst;
7065
7094
  inst.__sbTasks = __sbTasks;
7066
7095
  inst.__sbGate = gate;
@@ -7154,7 +7183,7 @@ function __sbDOStorage(cls, id) {
7154
7183
  // Values written in the new format start with __sbDOTag (no JSON text starts with "#"),
7155
7184
  // so either side reads both.
7156
7185
  function __sbDOGated() {
7157
- return globalThis.__sbCompat >= "2026-10-28";
7186
+ return __sbCompatDate() >= "2026-10-28";
7158
7187
  }
7159
7188
 
7160
7189
  function __sbDOMakeGate() {
@@ -8436,7 +8465,7 @@ function __sbWsHostResponse(r) {
8436
8465
  }
8437
8466
  // The compatibility date goes along: a standalone or platform server carries client sockets
8438
8467
  // from 2026-11-01 only (sproutd, whose bridge ignores it, from 2026-10-30).
8439
- const id = Number(__sbWsOp("o", 0, globalThis.__sbCompat, 0));
8468
+ const id = Number(__sbWsOp("o", 0, __sbCompatDate(), 0));
8440
8469
  if (!(id > 0)) return new Response("WebSockets are not supported by this host yet", { status: 501 });
8441
8470
  ws._host = id;
8442
8471
  if (ws._peer !== null && ws._peer._hib !== null && __sbWsHibOn()) __sbWsHibRegister(ws._peer, id);
@@ -8496,7 +8525,7 @@ function __sbWsPump(ws) {
8496
8525
  // described at __sbWsOp, ids with the top bit set. Returning that Response from a handler
8497
8526
  // relays the socket to the client through a pair.
8498
8527
  function __sbWsOutboundOn() {
8499
- return globalThis.__sbCompat >= "2026-11-01";
8528
+ return __sbCompatDate() >= "2026-11-01";
8500
8529
  }
8501
8530
  let __sbWsWakeable = -1;
8502
8531
  function __sbWsCanWake() {
@@ -8783,7 +8812,7 @@ function __sbWsProxy(r, upstream) {
8783
8812
  // their tags and attachments) and the event is dispatched as webSocketMessage/webSocketClose.
8784
8813
  let __sbWsHibHost = -1;
8785
8814
  function __sbWsHibOn() {
8786
- if (globalThis.__sbCompat < "2026-11-01" || !__sbDOGated()) return false;
8815
+ if (__sbCompatDate() < "2026-11-01" || !__sbDOGated()) return false;
8787
8816
  if (__sbWsHibHost < 0 && __sbWsOp("H", 0, "", 0) === "1") __sbWsHibHost = 1;
8788
8817
  return __sbWsHibHost === 1;
8789
8818
  }
@@ -8915,7 +8944,9 @@ function __sbWsRestoreSocket(id) {
8915
8944
  }
8916
8945
 
8917
8946
  function __sbInstallWebSockets() {
8918
- if (globalThis.__sbCompat < "2026-10-30") return;
8947
+ Request._hasWsp = false;
8948
+ if (__sbCompatDate() < "2026-10-30") return;
8949
+ Request._hasWsp = true;
8919
8950
  globalThis.WebSocketPair = __SbWebSocketPair;
8920
8951
  globalThis.WebSocket = __SbWebSocket;
8921
8952
  globalThis.WebSocketRequestResponsePair = __SbWebSocketRequestResponsePair;
@@ -8978,7 +9009,7 @@ function __sbBindingCallIngress(request) {
8978
9009
  if (vouched) {
8979
9010
  const url = headers.get("x-sb-url");
8980
9011
  // 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;
9012
+ if (url && URL.canParse(url) && __sbCompatDate() >= "2026-10-22") request.url = url;
8982
9013
  headers.delete("x-sb-remote-addr");
8983
9014
  }
8984
9015
  for (const name of ["x-sb-binding-call", "x-sb-url", "x-sb-edge-token"]) {
@@ -9078,7 +9109,7 @@ function __sbReqBodyOp(op, id, data) {
9078
9109
  // Turns streaming on in the native server, from 2026-10-27. Called once at startup, after
9079
9110
  // the compatibility date is set (__sbInstallFetch).
9080
9111
  function __sbIngressStreamingSetup() {
9081
- if (globalThis.__sbCompat >= "2026-10-27") __sbReqBodyOp("e", 0, "");
9112
+ if (__sbCompatDate() >= "2026-10-27") __sbReqBodyOp("e", 0, "");
9082
9113
  }
9083
9114
 
9084
9115
  // request.body for a streamed request: chunks as the server receives them.
@@ -9155,7 +9186,7 @@ function __sbIngressStream(request, id) {
9155
9186
  // handing the request to user code. Request.text() in Porffor otherwise treats
9156
9187
  // the raw bytes as already-decoded characters and corrupts Unicode/HMAC input.
9157
9188
  function __sbDecodeIngress(request) {
9158
- const streamed = globalThis.__sbCompat >= "2026-10-27" ? Number(__sbReqBodyOp("t", 0, "")) : 0;
9189
+ const streamed = __sbCompatDate() >= "2026-10-27" ? Number(__sbReqBodyOp("t", 0, "")) : 0;
9159
9190
  if (streamed > 0) {
9160
9191
  __sbIngressStream(request, streamed);
9161
9192
  return;
@@ -9164,7 +9195,7 @@ function __sbDecodeIngress(request) {
9164
9195
  if (raw == null) return;
9165
9196
  // A GET or HEAD has no body, as in workerd (from 2026-10-23): request.body is null, and
9166
9197
  // 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") {
9198
+ if (raw === "" && (request.method === "GET" || request.method === "HEAD") && __sbCompatDate() >= "2026-10-23") {
9168
9199
  request.body = null;
9169
9200
  return;
9170
9201
  }
@@ -9181,6 +9212,14 @@ function __sbDecodeIngress(request) {
9181
9212
  };
9182
9213
  }
9183
9214
 
9215
+ // SB_TRUSTED_PROXIES is fixed for the life of a process (a sproutd worker gets its own after the fork,
9216
+ // and this runs per request, never before it), so it is read and split once, not four times a request.
9217
+ let __sbTrustedCache;
9218
+ function __sbTrustedProxiesOnce() {
9219
+ if (__sbTrustedCache === undefined) __sbTrustedCache = __sbSplitList(__sbEnv("SB_TRUSTED_PROXIES"));
9220
+ return __sbTrustedCache;
9221
+ }
9222
+
9184
9223
  globalThis.__sbEntry = function (handlers, request) {
9185
9224
  __sbDecodeIngress(request);
9186
9225
  const trigger = request.headers.get("x-sb-trigger");
@@ -9191,13 +9230,18 @@ globalThis.__sbEntry = function (handlers, request) {
9191
9230
  // are only set when a trusted front proxy forwarded them; colo, ASN and
9192
9231
  // geo fields stay absent everywhere (#128 explicitly rejects faking them).
9193
9232
  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;
9233
+ if (__sbTrustedProxiesOnce().length === 0) {
9234
+ // No trusted proxy (the usual case): the peer is the client and no forwarded header counts.
9235
+ __cf.clientIp = __sbNormalizeIp(request.headers.get("x-sb-remote-addr") || "");
9236
+ } else {
9237
+ __cf.clientIp = __sbClientIp(request);
9238
+ const __httpProtocol = __sbTrustedProxyHeader(request, "x-sb-http-protocol");
9239
+ if (__httpProtocol) __cf.httpProtocol = __httpProtocol;
9240
+ const __tlsVersion = __sbTrustedProxyHeader(request, "x-sb-tls-version");
9241
+ if (__tlsVersion) __cf.tlsVersion = __tlsVersion;
9242
+ const __tlsCipher = __sbTrustedProxyHeader(request, "x-sb-tls-cipher");
9243
+ if (__tlsCipher) __cf.tlsCipher = __tlsCipher;
9244
+ }
9201
9245
  request.cf = __cf;
9202
9246
  // The edge's headers are gone: from 2026-10-23 the handler cannot change the rest.
9203
9247
  __sbFreezeHeaders(request);
@@ -9227,8 +9271,8 @@ globalThis.__sbEntry = function (handlers, request) {
9227
9271
  if (rpc == null && !__sbHandles(handlers, "fetch")) throw new Error("Handler does not export a fetch() function.");
9228
9272
  __res = rpc != null
9229
9273
  ? __sbRpcDispatch(handlers, receiver, rpc, request)
9230
- : globalThis.__sbCompat >= "2026-09-28"
9231
- ? receiver.fetch(request, globalThis.env, ctx)
9274
+ : __sbCompatDate() >= "2026-09-28"
9275
+ ? receiver.fetch(request, __sbEnvValue(), ctx)
9232
9276
  : receiver.fetch(request, ctx);
9233
9277
  } catch (error) {
9234
9278
  // #179 — a synchronous throw anywhere in the handler must not take the
@@ -9260,8 +9304,8 @@ globalThis.__sbEntry = function (handlers, request) {
9260
9304
  try {
9261
9305
  const event = { cron: body.cron || "", scheduledTime: body.scheduledTime || Date.now(), noRetry() {} };
9262
9306
  const receiver = __sbReceiver(handlers, ctx);
9263
- __sres = globalThis.__sbCompat >= "2026-09-28"
9264
- ? receiver.scheduled(event, globalThis.env, ctx)
9307
+ __sres = __sbCompatDate() >= "2026-09-28"
9308
+ ? receiver.scheduled(event, __sbEnvValue(), ctx)
9265
9309
  : receiver.scheduled(event, ctx);
9266
9310
  } catch (error) {
9267
9311
  // #179 — same rule as fetch: don't let a throw here crash the process.
@@ -9389,8 +9433,8 @@ async function __sbRunQueueBatch(handlers, body, ctx) {
9389
9433
  };
9390
9434
  }
9391
9435
  try {
9392
- const __qres = globalThis.__sbCompat >= "2026-09-28"
9393
- ? handlers.queue(batch, globalThis.env, ctx)
9436
+ const __qres = __sbCompatDate() >= "2026-09-28"
9437
+ ? handlers.queue(batch, __sbEnvValue(), ctx)
9394
9438
  : handlers.queue(batch, ctx);
9395
9439
  // Was fire-and-forget: an async `queue()` had this default-ack pass run (and
9396
9440
  // the response go out) before its own `await`s did, so any ack()/retry()
@@ -5323,7 +5323,7 @@ function __sbLtxConfigure() {
5323
5323
  }
5324
5324
 
5325
5325
  function __sbCall(reqJson) {
5326
- if (globalThis.__sbCompat >= "2026-10-22") __sbLtxConfigure();
5326
+ if (__sbCompatDate() >= "2026-10-22") __sbLtxConfigure();
5327
5327
  try {
5328
5328
  return JSON.stringify(__sbEmbeddedDispatch(JSON.parse(reqJson)));
5329
5329
  } catch (err) {
@@ -5710,8 +5710,8 @@ globalThis.__sbStartLocalTriggers = function (handlers, bindings) {
5710
5710
  try {
5711
5711
  const receiver = __sbReceiver(handlers, ctx);
5712
5712
  const event = { cron: crons[i], scheduledTime: now.getTime(), noRetry() {} };
5713
- const res = globalThis.__sbCompat >= "2026-09-28"
5714
- ? receiver.scheduled(event, globalThis.env, ctx)
5713
+ const res = __sbCompatDate() >= "2026-09-28"
5714
+ ? receiver.scheduled(event, __sbEnvValue(), ctx)
5715
5715
  : receiver.scheduled(event, ctx);
5716
5716
  if (res && __sbIsFn(res.then)) await res;
5717
5717
  } 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
+ }