@sproutboat/runtime 0.27.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4745,7 +4745,7 @@ static size_t sb_utf8_encode_units(const uint16_t* s, size_t len, unsigned char*
4745
4745
 
4746
4746
  `;
4747
4747
 
4748
- // sproutd #22: __sbFromUtf8 for a string of bytes (one byte per unit), in C.
4748
+ // sproutd #22: __sbFromUtf8 for a string of bytes (one byte per unit) or a Uint8Array, in C.
4749
4749
  // Returns "" when it cannot (not a string, or out of memory), so the caller
4750
4750
  // falls back to the JS loop. All-ASCII byte strings are already their text.
4751
4751
  // oxlint-disable-next-line no-unused-vars -- `bytes` is read inside the RawC block below, not by JS.
@@ -4753,9 +4753,10 @@ function __sbFromUtf8Native(bytes) {
4753
4753
  let out = "";
4754
4754
  // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
4755
4755
  Porffor.c`
4756
- if (bytes.type == 195 || bytes.type == 67) {
4757
- const u32 __p = (u32)bytes.val;
4758
- const size_t __n = (size_t)*(u32*)(MEM + __p);
4756
+ const int __view = bytes.type == 81 && *(int32_t*)(MEM + (u32)bytes.val) >= 0;
4757
+ if (bytes.type == 195 || bytes.type == 67 || __view) {
4758
+ const u32 __p = __view ? *(u32*)(MEM + (u32)bytes.val + 4) : (u32)bytes.val;
4759
+ const size_t __n = __view ? (size_t)*(int32_t*)(MEM + (u32)bytes.val) : (size_t)*(u32*)(MEM + __p);
4759
4760
  int __ascii = bytes.type == 195;
4760
4761
  if (__ascii) {
4761
4762
  const unsigned char* __s = (const unsigned char*)(MEM + __p + 4);
@@ -4766,7 +4767,7 @@ function __sbFromUtf8Native(bytes) {
4766
4767
  unsigned char* __b = (unsigned char*)malloc(__n + 1);
4767
4768
  uint16_t* __u = (uint16_t*)malloc(__n * 2 + 2);
4768
4769
  if (__b && __u) {
4769
- if (bytes.type == 195) memcpy(__b, MEM + __p + 4, __n);
4770
+ if (bytes.type != 67) memcpy(__b, MEM + __p + 4, __n);
4770
4771
  else { const uint16_t* __w = (const uint16_t*)(MEM + __p + 4); for (size_t __i = 0; __i < __n; __i++) __b[__i] = (unsigned char)(__w[__i] & 0xff); }
4771
4772
  int __wide = 0;
4772
4773
  const size_t __k = sb_utf8_decode_units(__b, __n, __u, &__wide);
@@ -5904,11 +5905,18 @@ globalThis.__sbInstallBindings = function (target, bindings) {
5904
5905
  delaySeconds: o.delaySeconds || 0,
5905
5906
  });
5906
5907
  },
5907
- sendBatch(messages) {
5908
+ sendBatch(messages, options) {
5909
+ // From 2026-10-28 sendBatch(messages, { delaySeconds }) delays every message that
5910
+ // sets none of its own, as on Workers; before, the batch option was ignored.
5911
+ const batchDelay = globalThis.__sbCompat >= "2026-10-28" ? (options && options.delaySeconds) || 0 : 0;
5908
5912
  const list = [];
5909
5913
  for (let j = 0; j < (messages || []).length; j++) {
5910
5914
  const m = messages[j];
5911
- list.push({ body: __sbIsStr(m.body) ? m.body : JSON.stringify(m.body), delaySeconds: m.delaySeconds || 0 });
5915
+ list.push({
5916
+ body: __sbIsStr(m.body) ? m.body : JSON.stringify(m.body),
5917
+ // A message delay of 0 (or none) takes the batch delay, as miniflare does.
5918
+ delaySeconds: m.delaySeconds || batchDelay,
5919
+ });
5912
5920
  }
5913
5921
  __sbRpc("queue.send_batch", { queue: name, messages: list });
5914
5922
  },
@@ -6010,9 +6018,14 @@ globalThis.__sbInstallBindings = function (target, bindings) {
6010
6018
  async function __sbServiceFetch(binding, input, init) {
6011
6019
  const parts = __sbFetchParts(input, init);
6012
6020
  if (parts.signal) parts.signal.throwIfAborted();
6013
- // A stream body is read to the end first: the wire carries a whole body.
6021
+ // A stream body goes out as it is read where the transport can (__sbStreamsBody), else it
6022
+ // is read to the end first.
6014
6023
  const body = __sbUnwrapBody(parts.body);
6015
- const wire = body instanceof ReadableStream ? __sbRequestBody(await __sbCollectBody(body)) : __sbPartsBody({ ...parts, body });
6024
+ const wire = __sbStreamsBody(body)
6025
+ ? { bodyStream: true, __sbBodySource: body }
6026
+ : body instanceof ReadableStream
6027
+ ? __sbRequestBody(await __sbCollectBody(body))
6028
+ : __sbPartsBody({ ...parts, body });
6016
6029
  const msg = { binding, url: parts.url, method: parts.method, headers: parts.headers, ...wire };
6017
6030
  const response = await __sbFetchFollowing("service.fetch", msg, "sproutboat service " + binding, parts.redirect, parts.signal);
6018
6031
  // The callee threw: as in workerd, its error is thrown here.
@@ -6270,7 +6283,52 @@ function __sbUpstreamResponse(r) {
6270
6283
  // whole (a broker from before streaming, or sproutd's embedded transport) is handled
6271
6284
  // as before. Cancelling the body, or the signal, drops the exchange, which stops the
6272
6285
  // upstream request.
6286
+ // Streamed request bodies, from compatibility date 2026-10-27, on a transport that sends
6287
+ // them in frames (the broker's, on an async host, against a broker that takes them):
6288
+ // fetch() and service bindings send a ReadableStream body as it is read, instead of
6289
+ // reading it to the end first. As in workerd, a streamed body can't follow a 307 or 308.
6290
+ function __sbStreamsBody(body) {
6291
+ return (
6292
+ body instanceof ReadableStream &&
6293
+ globalThis.__sbCompat >= "2026-10-27" &&
6294
+ globalThis.__sbBodyStreamable != null &&
6295
+ globalThis.__sbBodyStreamable()
6296
+ );
6297
+ }
6298
+ // Reads source into the exchange under timer id `id`, a frame per chunk, while ended()
6299
+ // is false. Above 4 MiB not yet written it waits for the socket to catch up.
6300
+ async function __sbPumpRequestBody(id, source, ended) {
6301
+ const reader = source.getReader();
6302
+ try {
6303
+ for (;;) {
6304
+ const { value, done } = await reader.read();
6305
+ if (done) break;
6306
+ if (ended()) {
6307
+ reader.cancel();
6308
+ return;
6309
+ }
6310
+ let bytes = null;
6311
+ if (value instanceof Uint8Array) bytes = value;
6312
+ else if (value instanceof ArrayBuffer) bytes = new Uint8Array(value);
6313
+ else if (ArrayBuffer.isView(value)) bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
6314
+ else throw new TypeError("This ReadableStream did not return bytes.");
6315
+ if (bytes.length === 0) continue;
6316
+ let backlog = Number(globalThis.__sbAsyncBody(id, "c", __sbToBytes(bytes)));
6317
+ while (backlog > 4 * 1024 * 1024 && !ended()) {
6318
+ await new Promise((resolve) => setTimeout(resolve, 2));
6319
+ backlog = Number(globalThis.__sbAsyncBody(id, "q", ""));
6320
+ }
6321
+ }
6322
+ globalThis.__sbAsyncBody(id, "e", "");
6323
+ } catch (error) {
6324
+ globalThis.__sbAsyncBody(id, "x", "");
6325
+ __sbLogUncaught("fetch", error);
6326
+ }
6327
+ }
6328
+
6273
6329
  function __sbUpstreamStreamed(op, msg, label, signal) {
6330
+ const source = msg.__sbBodySource;
6331
+ if (source !== undefined) delete msg.__sbBodySource;
6274
6332
  return new Promise((resolve, reject) => {
6275
6333
  let controller = null;
6276
6334
  let onAbort = null;
@@ -6338,6 +6396,7 @@ function __sbUpstreamStreamed(op, msg, label, signal) {
6338
6396
  msg.asyncId = id;
6339
6397
  msg.stream = true;
6340
6398
  globalThis.__sbFetchUpstream(op, msg);
6399
+ if (source !== undefined) __sbPumpRequestBody(id, source, () => done);
6341
6400
  } catch (e) {
6342
6401
  finish();
6343
6402
  reject(e);
@@ -6375,7 +6434,8 @@ function __sbFollowHop(op, msg, label, mode, signal, hops) {
6375
6434
  next.method = "GET";
6376
6435
  next.body = null;
6377
6436
  delete next.bodyBinary;
6378
- }
6437
+ delete next.bodyStream;
6438
+ } else if (msg.bodyStream === true) throw new TypeError(label + ": a streamed request body can't follow a " + res.status + " redirect");
6379
6439
  return __sbFollowHop(op, next, label, mode, signal, hops + 1);
6380
6440
  };
6381
6441
  const out = __sbUpstream(op, msg, label, signal);
@@ -6435,6 +6495,7 @@ function __sbUpstream(op, msg, label, signal, always) {
6435
6495
  // Worker's fetch. Installed by the generated module (wrap.ts), not here: the
6436
6496
  // prelude must not replace fetch just by being evaluated.
6437
6497
  globalThis.__sbInstallFetch = function () {
6498
+ __sbIngressStreamingSetup();
6438
6499
  globalThis.fetch = function (input, init) {
6439
6500
  let parts;
6440
6501
  try {
@@ -6448,9 +6509,11 @@ globalThis.__sbInstallFetch = function () {
6448
6509
  const msg = { url: parts.url, method: parts.method, headers: parts.headers, ...body };
6449
6510
  return __sbFetchFollowing("fetch", msg, "sproutboat fetch", parts.redirect, parts.signal);
6450
6511
  };
6451
- // A stream body is read to the end first, as for a service binding: the wire carries a
6452
- // whole body. It was sent as the text "[object ReadableStream]".
6512
+ // A stream body goes out as it is read, from 2026-10-27 where the transport can
6513
+ // (__sbStreamsBody); else it is read to the end first. It was sent as the text
6514
+ // "[object ReadableStream]".
6453
6515
  const stream = __sbUnwrapBody(parts.body);
6516
+ if (__sbStreamsBody(stream)) return send({ bodyStream: true, __sbBodySource: stream });
6454
6517
  if (stream instanceof ReadableStream) return __sbCollectBody(stream).then((bytes) => send(__sbRequestBody(bytes)));
6455
6518
  return send(__sbPartsBody(parts));
6456
6519
  };
@@ -6458,14 +6521,14 @@ globalThis.__sbInstallFetch = function () {
6458
6521
 
6459
6522
  // ---------------------------------------------------------------------------
6460
6523
  // Durable Objects. The class runs here in the sandboxed sprout. There is exactly
6461
- // one sprout process per deployment (the supervisor model) and the native-fetch
6462
- // runtime processes one turn at a time, so calls to a given object id are
6463
- // already serialized — `env.<NS>.get(id).fetch()` invokes the instance directly,
6464
- // no round-trip. Only `state.storage.*` goes to the broker (so object state
6465
- // outlives a sprout restart), scoped to (class, id).
6466
- // ponytail: serialization relies on the single sprout process; a multi-sprout
6467
- // deployment needs the broker to hold a per-id lock (cloud). Storage ops are one
6468
- // key at a time; blockConcurrencyWhile just runs the fn.
6524
+ // one sprout process per deployment (the supervisor model), so one instance per
6525
+ // id: `env.<NS>.get(id).fetch()` invokes the instance directly, no round-trip.
6526
+ // Events still interleave at every await; from 2026-10-28 an input gate (below,
6527
+ // __sbDOEnter) keeps them out while a storage read is in flight, as in workerd.
6528
+ // Only `state.storage.*` goes to the broker (so object state outlives a sprout
6529
+ // restart), scoped to (class, id).
6530
+ // ponytail: one instance per id relies on the single sprout process; a
6531
+ // multi-sprout deployment needs the broker to hold a per-id lock (cloud).
6469
6532
 
6470
6533
  function __sbMakeDONamespace(binding, className) {
6471
6534
  return {
@@ -6514,9 +6577,14 @@ function __sbMakeDONamespace(binding, className) {
6514
6577
  if (opts.body != null) req.body = String(opts.body);
6515
6578
  }
6516
6579
  const inst = __sbGetDOInstance(className, idStr);
6517
- const res = inst.fetch(req);
6518
- if (inst.__sbTasks.length || (res && __sbIsFn(res.then)) || __sbHasStreamBody(res)) return __sbFetchWithDrain(res, inst.__sbTasks);
6519
- return res;
6580
+ const deliver = () => {
6581
+ const res = inst.fetch(req);
6582
+ if (inst.__sbTasks.length || (res && __sbIsFn(res.then)) || __sbHasStreamBody(res)) return __sbFetchWithDrain(res, inst.__sbTasks);
6583
+ return res;
6584
+ };
6585
+ // From 2026-10-28 the call waits at the object's input gate, as in workerd.
6586
+ if (__sbDOGated()) return __sbDOEnter(inst.__sbGate, deliver);
6587
+ return deliver();
6520
6588
  },
6521
6589
  };
6522
6590
  },
@@ -6540,15 +6608,39 @@ function __sbGetDOInstance(cls, id) {
6540
6608
  // `__sbDOInstances`), so the queue lives on `state` too and is drained by
6541
6609
  // whichever call site invoked `fetch`/`alarm` this turn.
6542
6610
  const __sbTasks = [];
6611
+ const gate = __sbDOMakeGate();
6543
6612
  const state = {
6544
6613
  id: {
6545
6614
  toString() {
6546
6615
  return id;
6547
6616
  },
6548
6617
  },
6549
- storage: __sbDOStorage(cls, id),
6618
+ storage: __sbDOGated() ? __sbDOAsyncStorage(cls, id, gate) : __sbDOStorage(cls, id),
6550
6619
  blockConcurrencyWhile(fn) {
6551
- return fn();
6620
+ if (!__sbDOGated()) return fn();
6621
+ // No event enters until fn settles (2026-10-28).
6622
+ gate.locks++;
6623
+ const done = () => {
6624
+ gate.locks--;
6625
+ __sbDOPump(gate);
6626
+ };
6627
+ let r;
6628
+ try {
6629
+ r = fn();
6630
+ } catch (error) {
6631
+ done();
6632
+ return Promise.reject(error);
6633
+ }
6634
+ return Promise.resolve(r).then(
6635
+ (v) => {
6636
+ done();
6637
+ return v;
6638
+ },
6639
+ (error) => {
6640
+ done();
6641
+ throw error;
6642
+ },
6643
+ );
6552
6644
  },
6553
6645
  waitUntil(p) {
6554
6646
  if (p && __sbIsFn(p.then)) __sbTasks.push(p);
@@ -6556,6 +6648,7 @@ function __sbGetDOInstance(cls, id) {
6556
6648
  };
6557
6649
  inst = new Ctor(state, globalThis.env);
6558
6650
  inst.__sbTasks = __sbTasks;
6651
+ inst.__sbGate = gate;
6559
6652
  __sbDOInstances[cacheKey] = inst;
6560
6653
  }
6561
6654
  return inst;
@@ -6568,12 +6661,12 @@ function __sbDOStorage(cls, id) {
6568
6661
  const out = new Map();
6569
6662
  for (let i = 0; i < key.length; i++) {
6570
6663
  const r = __sbRpc("do.storage.get", { cls, id, key: String(key[i]) });
6571
- if (r.found) out.set(key[i], JSON.parse(r.value));
6664
+ if (r.found) out.set(key[i], __sbDODecode(r.value));
6572
6665
  }
6573
6666
  return out;
6574
6667
  }
6575
6668
  const r = __sbRpc("do.storage.get", { cls, id, key: String(key) });
6576
- return r.found ? JSON.parse(r.value) : undefined;
6669
+ return r.found ? __sbDODecode(r.value) : undefined;
6577
6670
  },
6578
6671
  put(key, value) {
6579
6672
  if (key != null && __sbIsObj(key)) {
@@ -6616,12 +6709,435 @@ function __sbDOStorage(cls, id) {
6616
6709
  limit: o.limit == null ? 1000 : o.limit,
6617
6710
  });
6618
6711
  const out = new Map();
6619
- for (let i = 0; i < (r.entries || []).length; i++) out.set(r.entries[i][0], JSON.parse(r.entries[i][1]));
6712
+ for (let i = 0; i < (r.entries || []).length; i++) out.set(r.entries[i][0], __sbDODecode(r.entries[i][1]));
6713
+ return out;
6714
+ },
6715
+ };
6716
+ }
6717
+
6718
+
6719
+ // From compatibility date 2026-10-28 Durable Objects behave like workerd's in three ways:
6720
+ //
6721
+ // - Input gate. While a storage read is in flight, no other event (stub.fetch(), an alarm)
6722
+ // enters the object; the reader's continuation runs first. Reads here are synchronous, so
6723
+ // the gate stays shut until the next timer turn, by which point every microtask the read
6724
+ // woke (the reader's code up to its next real await) has run. So
6725
+ // `const v = await storage.get("n"); await storage.put("n", v + 1)` is safe under
6726
+ // concurrent calls; it used to lose updates (10 concurrent increments ended at 1). An
6727
+ // event that awaits anything else (fetch, a timer) lets the next one in, as in workerd.
6728
+ // blockConcurrencyWhile() and transaction() shut it until they settle.
6729
+ // - Storage methods return Promises.
6730
+ // - Values are structured-cloned (Date, Map, Set, RegExp, ArrayBuffer and views, BigInt,
6731
+ // undefined inside containers, NaN and the infinities), not JSON: put(k, undefined) and
6732
+ // functions throw, and list() takes start, startAfter, end and reverse.
6733
+ //
6734
+ // Before that date storage is synchronous JSON and events interleave at every await.
6735
+ // Values written in the new format start with __sbDOTag (no JSON text starts with "#"),
6736
+ // so either side reads both.
6737
+ function __sbDOGated() {
6738
+ return globalThis.__sbCompat >= "2026-10-28";
6739
+ }
6740
+
6741
+ function __sbDOMakeGate() {
6742
+ return { locks: 0, waiting: [] };
6743
+ }
6744
+
6745
+ // Deliver queued events while the gate is open. A delivered event that reads storage shuts
6746
+ // the gate again, which stops the loop.
6747
+ function __sbDOPump(gate) {
6748
+ while (gate.locks === 0 && gate.waiting.length > 0) gate.waiting.shift()();
6749
+ }
6750
+
6751
+ // Shut the gate until the next timer turn (see above).
6752
+ function __sbDOHold(gate) {
6753
+ gate.locks++;
6754
+ setTimeout(function () {
6755
+ gate.locks--;
6756
+ __sbDOPump(gate);
6757
+ }, 0);
6758
+ }
6759
+
6760
+ // Run an event now if the gate is open and nothing is queued ahead of it, else queue it.
6761
+ // Always a Promise, like workerd's stub.fetch().
6762
+ function __sbDOEnter(gate, run) {
6763
+ if (gate.locks === 0 && gate.waiting.length === 0) {
6764
+ try {
6765
+ return Promise.resolve(run());
6766
+ } catch (error) {
6767
+ return Promise.reject(error);
6768
+ }
6769
+ }
6770
+ return new Promise(function (resolve, reject) {
6771
+ gate.waiting.push(function () {
6772
+ try {
6773
+ resolve(run());
6774
+ } catch (error) {
6775
+ reject(error);
6776
+ }
6777
+ });
6778
+ });
6779
+ }
6780
+
6781
+ const __sbDOTag = "#sb1:";
6782
+
6783
+ function __sbDOCloneError(value) {
6784
+ let text = "value";
6785
+ try {
6786
+ text = String(value);
6787
+ } catch {
6788
+ /* a value whose String() throws still gets the error below */
6789
+ }
6790
+ const error = new Error(text + " could not be cloned.");
6791
+ error.name = "DataCloneError";
6792
+ return error;
6793
+ }
6794
+
6795
+ function __sbDOHex(bytes) {
6796
+ const digits = "0123456789abcdef";
6797
+ let out = "";
6798
+ for (let i = 0; i < bytes.length; i++) out += digits[bytes[i] >> 4] + digits[bytes[i] & 15];
6799
+ return out;
6800
+ }
6801
+
6802
+ function __sbDOUnhex(text) {
6803
+ const out = new Uint8Array(text.length / 2);
6804
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
6805
+ return out;
6806
+ }
6807
+
6808
+ function __sbDOView(name, bytes) {
6809
+ const buf = bytes.buffer;
6810
+ if (name === "Uint8Array") return bytes;
6811
+ if (name === "Int8Array") return new Int8Array(buf);
6812
+ if (name === "Uint8ClampedArray") return new Uint8ClampedArray(buf);
6813
+ if (name === "Uint16Array") return new Uint16Array(buf);
6814
+ if (name === "Int16Array") return new Int16Array(buf);
6815
+ if (name === "Uint32Array") return new Uint32Array(buf);
6816
+ if (name === "Int32Array") return new Int32Array(buf);
6817
+ if (name === "Float32Array") return new Float32Array(buf);
6818
+ if (name === "Float64Array") return new Float64Array(buf);
6819
+ if (name === "DataView") return new DataView(buf);
6820
+ return bytes;
6821
+ }
6822
+
6823
+ // A value as JSON: null, booleans, strings and finite numbers are themselves; everything
6824
+ // else is an array whose first element names its type, so no plain value is ambiguous.
6825
+ /* oxlint-disable anti-slop/no-runtime-typeof -- structured clone branches on the value's type, as the HTML spec's StructuredSerialize does. */
6826
+ function __sbDOPack(v, seen) {
6827
+ if (v === null || v === true || v === false || __sbIsStr(v)) return v;
6828
+ const t = typeof v;
6829
+ if (t === "number") {
6830
+ if (v === 0 && 1 / v < 0) return ["N", "-0"];
6831
+ return Number.isFinite(v) ? v : ["N", String(v)];
6832
+ }
6833
+ if (t === "bigint") return ["B", String(v)];
6834
+ if (t === "undefined") return ["U"];
6835
+ if (t !== "object") throw __sbDOCloneError(v);
6836
+ if (seen.indexOf(v) !== -1) throw __sbDOCloneError("a circular value");
6837
+ seen.push(v);
6838
+ let out;
6839
+ if (v instanceof Date) out = ["D", v.getTime()];
6840
+ else if (v instanceof Map) {
6841
+ out = ["M"];
6842
+ v.forEach((x, k) => {
6843
+ out.push(__sbDOPack(k, seen), __sbDOPack(x, seen));
6844
+ });
6845
+ } else if (v instanceof Set) {
6846
+ out = ["S"];
6847
+ v.forEach((x) => {
6848
+ out.push(__sbDOPack(x, seen));
6849
+ });
6850
+ } else if (v instanceof ArrayBuffer) out = ["AB", __sbDOHex(new Uint8Array(v))];
6851
+ else if (ArrayBuffer.isView(v))
6852
+ out = ["V", Object.prototype.toString.call(v).slice(8, -1), __sbDOHex(new Uint8Array(v.buffer, v.byteOffset, v.byteLength))];
6853
+ else if (v instanceof RegExp) out = ["R", v.source, v.flags];
6854
+ else if (Array.isArray(v)) {
6855
+ out = ["A"];
6856
+ for (let i = 0; i < v.length; i++) out.push(__sbDOPack(v[i], seen));
6857
+ } else if (v instanceof Error) out = ["E", String(v.name), String(v.message)];
6858
+ else {
6859
+ out = ["O"];
6860
+ const keys = Object.keys(v);
6861
+ for (let i = 0; i < keys.length; i++) out.push(keys[i], __sbDOPack(v[keys[i]], seen));
6862
+ }
6863
+ seen.pop();
6864
+ return out;
6865
+ }
6866
+ /* oxlint-enable anti-slop/no-runtime-typeof */
6867
+
6868
+ function __sbDOUnpack(p) {
6869
+ if (!Array.isArray(p)) return p;
6870
+ const tag = p[0];
6871
+ if (tag === "U") return undefined;
6872
+ if (tag === "N") return p[1] === "-0" ? -0 : Number(p[1]);
6873
+ if (tag === "B") return BigInt(p[1]);
6874
+ if (tag === "D") return new Date(p[1]);
6875
+ if (tag === "AB") return __sbDOUnhex(p[1]).buffer;
6876
+ if (tag === "V") return __sbDOView(p[1], __sbDOUnhex(p[2]));
6877
+ if (tag === "R") return new RegExp(p[1], p[2]);
6878
+ if (tag === "E") {
6879
+ const e = new Error(p[2]);
6880
+ e.name = p[1];
6881
+ return e;
6882
+ }
6883
+ if (tag === "M") {
6884
+ const m = new Map();
6885
+ for (let i = 1; i + 1 < p.length; i += 2) m.set(__sbDOUnpack(p[i]), __sbDOUnpack(p[i + 1]));
6886
+ return m;
6887
+ }
6888
+ if (tag === "S") {
6889
+ const s = new Set();
6890
+ for (let i = 1; i < p.length; i++) s.add(__sbDOUnpack(p[i]));
6891
+ return s;
6892
+ }
6893
+ if (tag === "A") {
6894
+ const a = [];
6895
+ for (let i = 1; i < p.length; i++) a.push(__sbDOUnpack(p[i]));
6896
+ return a;
6897
+ }
6898
+ const o = {};
6899
+ for (let i = 1; i + 1 < p.length; i += 2) o[p[i]] = __sbDOUnpack(p[i + 1]);
6900
+ return o;
6901
+ }
6902
+
6903
+ function __sbDOEncode(value) {
6904
+ if (value === undefined) throw new TypeError("put() called with undefined value.");
6905
+ return __sbDOTag + JSON.stringify(__sbDOPack(value, []));
6906
+ }
6907
+
6908
+ // Reads both formats at every date: tagged (2026-10-28 writes) and plain JSON (earlier ones).
6909
+ function __sbDODecode(text) {
6910
+ if (__sbIsStr(text) && text.indexOf(__sbDOTag) === 0) return __sbDOUnpack(JSON.parse(text.slice(__sbDOTag.length)));
6911
+ return JSON.parse(text);
6912
+ }
6913
+
6914
+ function __sbDOListLimit(options) {
6915
+ const o = options || {};
6916
+ if (o.limit != null && !(Number(o.limit) > 0)) throw new TypeError("List limit must be positive.");
6917
+ return o;
6918
+ }
6919
+
6920
+ function __sbDOInRange(key, o) {
6921
+ if (o.prefix != null && key.indexOf(String(o.prefix)) !== 0) return false;
6922
+ if (o.start != null && key < String(o.start)) return false;
6923
+ if (o.startAfter != null && key <= String(o.startAfter)) return false;
6924
+ if (o.end != null && key >= String(o.end)) return false;
6925
+ return true;
6926
+ }
6927
+
6928
+ // The synchronous core of the 2026-10-28 storage: structured values, list() ranges.
6929
+ function __sbDOStrictStorage(cls, id) {
6930
+ const get = (key) => {
6931
+ const r = __sbRpc("do.storage.get", { cls, id, key: String(key) });
6932
+ return r.found ? __sbDODecode(r.value) : undefined;
6933
+ };
6934
+ const put = (key, value) => {
6935
+ __sbRpc("do.storage.put", { cls, id, key: String(key), value: __sbDOEncode(value) });
6936
+ };
6937
+ const del = (key) => !!__sbRpc("do.storage.delete", { cls, id, key: String(key) }).deleted;
6938
+ return {
6939
+ get(key) {
6940
+ if (!Array.isArray(key)) return get(key);
6941
+ const out = new Map();
6942
+ for (let i = 0; i < key.length; i++) {
6943
+ const v = get(key[i]);
6944
+ if (v !== undefined) out.set(key[i], v);
6945
+ }
6946
+ return out;
6947
+ },
6948
+ put(key, value) {
6949
+ if (key != null && __sbIsObj(key)) {
6950
+ // Encode every value before writing any, so one bad value writes nothing.
6951
+ const keys = Object.keys(key);
6952
+ const encoded = [];
6953
+ for (let i = 0; i < keys.length; i++) encoded.push(__sbDOEncode(key[keys[i]]));
6954
+ for (let i = 0; i < keys.length; i++) __sbRpc("do.storage.put", { cls, id, key: keys[i], value: encoded[i] });
6955
+ return;
6956
+ }
6957
+ put(key, value);
6958
+ },
6959
+ delete(key) {
6960
+ if (!Array.isArray(key)) return del(key);
6961
+ let n = 0;
6962
+ for (let i = 0; i < key.length; i++) n += del(key[i]) ? 1 : 0;
6963
+ return n;
6964
+ },
6965
+ deleteAll() {
6966
+ // workerd's deleteAll() also drops the alarm.
6967
+ __sbRpc("do.storage.delete_all", { cls, id });
6968
+ __sbRpc("do.alarm.delete", { cls, id });
6969
+ },
6970
+ setAlarm(when) {
6971
+ const at = when instanceof Date ? when.getTime() : when;
6972
+ if (!Number.isFinite(at))
6973
+ throw new TypeError("Failed to execute 'setAlarm' on 'DurableObjectStorage': parameter 1 is not of type 'date'.");
6974
+ __sbRpc("do.alarm.set", { cls, id, at: Number(at) });
6975
+ },
6976
+ getAlarm() {
6977
+ const r = __sbRpc("do.alarm.get", { cls, id });
6978
+ return r.at == null ? null : r.at;
6979
+ },
6980
+ deleteAlarm() {
6981
+ __sbRpc("do.alarm.delete", { cls, id });
6982
+ },
6983
+ list(options) {
6984
+ const o = __sbDOListLimit(options);
6985
+ const r = __sbRpc("do.storage.list", {
6986
+ cls,
6987
+ id,
6988
+ prefix: o.prefix == null ? "" : String(o.prefix),
6989
+ start: o.start == null ? null : String(o.start),
6990
+ startAfter: o.startAfter == null ? null : String(o.startAfter),
6991
+ end: o.end == null ? null : String(o.end),
6992
+ reverse: !!o.reverse,
6993
+ limit: o.limit == null ? 10000 : Number(o.limit),
6994
+ });
6995
+ const out = new Map();
6996
+ const entries = r.entries || [];
6997
+ // A transport that predates the range fields returns a superset: filter it here.
6998
+ for (let i = 0; i < entries.length; i++)
6999
+ if (__sbDOInRange(entries[i][0], o)) out.set(entries[i][0], __sbDODecode(entries[i][1]));
6620
7000
  return out;
6621
7001
  },
6622
7002
  };
6623
7003
  }
6624
7004
 
7005
+ // The 2026-10-28 storage: every method returns a Promise; reads shut the input gate.
7006
+ function __sbDOAsyncStorage(cls, id, gate) {
7007
+ const s = __sbDOStrictStorage(cls, id);
7008
+ const settle = (f) => {
7009
+ try {
7010
+ return Promise.resolve(f());
7011
+ } catch (error) {
7012
+ return Promise.reject(error);
7013
+ }
7014
+ };
7015
+ const read = (f) => {
7016
+ __sbDOHold(gate);
7017
+ return settle(f);
7018
+ };
7019
+ return {
7020
+ get: (key) => read(() => s.get(key)),
7021
+ list: (options) => read(() => s.list(options)),
7022
+ getAlarm: () => read(() => s.getAlarm()),
7023
+ put: (key, value) => settle(() => s.put(key, value)),
7024
+ delete: (key) => settle(() => s.delete(key)),
7025
+ deleteAll: () => settle(() => s.deleteAll()),
7026
+ setAlarm: (when) => settle(() => s.setAlarm(when)),
7027
+ deleteAlarm: () => settle(() => s.deleteAlarm()),
7028
+ sync: () => Promise.resolve(),
7029
+ transaction: (fn) => __sbDOTransaction(cls, id, s, gate, fn),
7030
+ };
7031
+ }
7032
+
7033
+ // storage.transaction(fn): fn gets a txn with the storage methods; its writes stay in
7034
+ // memory (read back by its own get and list) and are written when fn resolves, unless it
7035
+ // threw or called txn.rollback(). The input gate stays shut meanwhile, so no other event
7036
+ // sees a half-written transaction.
7037
+ function __sbDOTransaction(cls, id, s, gate, fn) {
7038
+ const writes = new Map(); // key -> encoded value, or null for a delete
7039
+ let rolledBack = false;
7040
+ const getOne = (key) => {
7041
+ const k = String(key);
7042
+ if (!writes.has(k)) return s.get(k);
7043
+ const v = writes.get(k);
7044
+ return v === null ? undefined : __sbDODecode(v);
7045
+ };
7046
+ const putOne = (key, value) => {
7047
+ writes.set(String(key), __sbDOEncode(value));
7048
+ };
7049
+ const delOne = (key) => {
7050
+ const existed = getOne(key) !== undefined;
7051
+ writes.set(String(key), null);
7052
+ return existed;
7053
+ };
7054
+ const txn = {
7055
+ get(key) {
7056
+ if (!Array.isArray(key)) return Promise.resolve(getOne(key));
7057
+ const out = new Map();
7058
+ for (let i = 0; i < key.length; i++) {
7059
+ const v = getOne(key[i]);
7060
+ if (v !== undefined) out.set(key[i], v);
7061
+ }
7062
+ return Promise.resolve(out);
7063
+ },
7064
+ put(key, value) {
7065
+ try {
7066
+ if (key != null && __sbIsObj(key)) {
7067
+ const keys = Object.keys(key);
7068
+ for (let i = 0; i < keys.length; i++) putOne(keys[i], key[keys[i]]);
7069
+ } else putOne(key, value);
7070
+ return Promise.resolve();
7071
+ } catch (error) {
7072
+ return Promise.reject(error);
7073
+ }
7074
+ },
7075
+ delete(key) {
7076
+ if (!Array.isArray(key)) return Promise.resolve(delOne(key));
7077
+ let n = 0;
7078
+ for (let i = 0; i < key.length; i++) n += delOne(key[i]) ? 1 : 0;
7079
+ return Promise.resolve(n);
7080
+ },
7081
+ list(options) {
7082
+ try {
7083
+ const o = __sbDOListLimit(options);
7084
+ const merged = s.list({ prefix: o.prefix, start: o.start, startAfter: o.startAfter, end: o.end });
7085
+ writes.forEach((v, k) => {
7086
+ if (!__sbDOInRange(k, o)) return;
7087
+ if (v === null) merged.delete(k);
7088
+ else merged.set(k, __sbDODecode(v));
7089
+ });
7090
+ const keys = [];
7091
+ merged.forEach((v, k) => keys.push(k));
7092
+ keys.sort();
7093
+ if (o.reverse) keys.reverse();
7094
+ const out = new Map();
7095
+ const limit = o.limit == null ? keys.length : Number(o.limit);
7096
+ for (let i = 0; i < keys.length && i < limit; i++) out.set(keys[i], merged.get(keys[i]));
7097
+ return Promise.resolve(out);
7098
+ } catch (error) {
7099
+ return Promise.reject(error);
7100
+ }
7101
+ },
7102
+ rollback() {
7103
+ rolledBack = true;
7104
+ },
7105
+ getAlarm: () => Promise.resolve(s.getAlarm()),
7106
+ setAlarm: (when) => Promise.resolve(s.setAlarm(when)),
7107
+ deleteAlarm: () => Promise.resolve(s.deleteAlarm()),
7108
+ };
7109
+ gate.locks++;
7110
+ const done = () => {
7111
+ gate.locks--;
7112
+ __sbDOPump(gate);
7113
+ };
7114
+ let r;
7115
+ try {
7116
+ r = fn(txn);
7117
+ } catch (error) {
7118
+ done();
7119
+ return Promise.reject(error);
7120
+ }
7121
+ return Promise.resolve(r).then(
7122
+ (v) => {
7123
+ try {
7124
+ if (!rolledBack)
7125
+ writes.forEach((value, key) => {
7126
+ if (value === null) __sbRpc("do.storage.delete", { cls, id, key });
7127
+ else __sbRpc("do.storage.put", { cls, id, key, value });
7128
+ });
7129
+ } finally {
7130
+ done();
7131
+ }
7132
+ return v;
7133
+ },
7134
+ (error) => {
7135
+ done();
7136
+ throw error;
7137
+ },
7138
+ );
7139
+ }
7140
+
6625
7141
  // ---------------------------------------------------------------------------
6626
7142
  // Trigger dispatch. The compiled server only ever calls `fetch(request)`; this
6627
7143
  // routes the internal `x-sb-trigger` requests (sent by the broker, authenticated
@@ -7031,11 +7547,133 @@ async function __sbQueueWithDrain(result, tasks) {
7031
7547
  return new Response(JSON.stringify(r), { headers: { "content-type": "application/json" } });
7032
7548
  }
7033
7549
 
7550
+ // Streamed request bodies, from compatibility date 2026-10-27, on a native server that
7551
+ // streams them (a standalone or platform sprout; request-streaming.ts in the toolchain).
7552
+ // The handler runs once the headers are in, and request.body hands out the bytes as they
7553
+ // arrive: the server pauses the socket while more than 1 MiB waits unread. Before that
7554
+ // date the server buffers the whole body and the handler gets it at once, as before.
7555
+ // op: "e" enable (at startup), "t" take the current request's body id, "p" poll (a byte
7556
+ // count, "0" nothing yet, "-1" ended, "-2" failed), "i" copy into the Uint8Array in data,
7557
+ // "w" wait on the timer id in data, "c" close. Returns a short string.
7558
+ // oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
7559
+ function __sbReqBodyOp(op, id, data) {
7560
+ let out = "";
7561
+ // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
7562
+ Porffor.c`
7563
+ extern int sb_reqbody_enable(void);
7564
+ extern unsigned int sb_reqbody_take_current(void);
7565
+ extern long long sb_reqbody_poll(unsigned int);
7566
+ extern size_t sb_reqbody_take(unsigned int, char*, size_t);
7567
+ extern void sb_reqbody_wait(unsigned int, unsigned int);
7568
+ extern void sb_reqbody_close(unsigned int);
7569
+ const char* __o; size_t __ol; char* __oo = 0;
7570
+ porf_native_fetch_read_value(op, &__o, &__ol, &__oo);
7571
+ char __op = __ol ? __o[0] : 0;
7572
+ if (__oo) free(__oo);
7573
+ const unsigned int __id = id.type == 1 && id.val > 0 ? (unsigned int)id.val : 0;
7574
+ char __r[32]; __r[0] = 0;
7575
+ if (__op == 'e') snprintf(__r, sizeof(__r), "%d", sb_reqbody_enable());
7576
+ else if (__op == 't') snprintf(__r, sizeof(__r), "%u", sb_reqbody_take_current());
7577
+ else if (__op == 'p') snprintf(__r, sizeof(__r), "%lld", sb_reqbody_poll(__id));
7578
+ else if (__op == 'i' && data.type == 81) {
7579
+ const u32 __ta = (u32)data.val;
7580
+ const int32_t __len = *(int32_t*)(MEM + __ta);
7581
+ if (__len > 0) {
7582
+ const u32 __at = *(u32*)(MEM + __ta + 4) + 4;
7583
+ snprintf(__r, sizeof(__r), "%zu", sb_reqbody_take(__id, (char*)(MEM + __at), (size_t)__len));
7584
+ }
7585
+ } else if (__op == 'w' && data.type == 1 && data.val > 0) sb_reqbody_wait(__id, (unsigned int)data.val);
7586
+ else if (__op == 'c') sb_reqbody_close(__id);
7587
+ out = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
7588
+ `;
7589
+ return out;
7590
+ }
7591
+
7592
+ // Turns streaming on in the native server, from 2026-10-27. Called once at startup, after
7593
+ // the compatibility date is set (__sbInstallFetch).
7594
+ function __sbIngressStreamingSetup() {
7595
+ if (globalThis.__sbCompat >= "2026-10-27") __sbReqBodyOp("e", 0, "");
7596
+ }
7597
+
7598
+ // request.body for a streamed request: chunks as the server receives them.
7599
+ function __sbIngressStream(request, id) {
7600
+ let closed = false;
7601
+ const close = () => {
7602
+ if (!closed) {
7603
+ closed = true;
7604
+ __sbReqBodyOp("c", id, "");
7605
+ }
7606
+ };
7607
+ const step = (controller, resolve) => {
7608
+ const state = Number(__sbReqBodyOp("p", id, ""));
7609
+ if (state === 0) {
7610
+ // Nothing yet: the server fires this timer when bytes, the end or an error arrive.
7611
+ const timer = setTimeout(() => step(controller, resolve), 2147483647);
7612
+ __sbReqBodyOp("w", id, timer);
7613
+ return;
7614
+ }
7615
+ if (state > 0) {
7616
+ const chunk = new Uint8Array(state);
7617
+ __sbReqBodyOp("i", id, chunk);
7618
+ controller.enqueue(chunk);
7619
+ } else {
7620
+ close();
7621
+ if (state === -1) controller.close();
7622
+ else controller.error(new TypeError("Network connection lost."));
7623
+ }
7624
+ resolve();
7625
+ };
7626
+ const stream = new ReadableStream({
7627
+ pull(controller) {
7628
+ return new Promise((resolve) => step(controller, resolve));
7629
+ },
7630
+ cancel() {
7631
+ close();
7632
+ },
7633
+ });
7634
+ request.body = stream;
7635
+ // The bytes of the whole body, read through the stream (so bodyUsed and locking hold).
7636
+ const all = async () => {
7637
+ const body = request.body;
7638
+ if (request.bodyUsed) throw new TypeError("Body has already been used. It can only be used once. Use tee() first if you need to read it twice.");
7639
+ request._bodyUsed = true;
7640
+ const reader = body.getReader();
7641
+ const chunks = [];
7642
+ let size = 0;
7643
+ for (;;) {
7644
+ const { value, done } = await reader.read();
7645
+ if (done) break;
7646
+ chunks.push(value);
7647
+ size += value.length;
7648
+ }
7649
+ if (chunks.length === 1) return chunks[0];
7650
+ const out = new Uint8Array(size);
7651
+ let at = 0;
7652
+ for (const chunk of chunks) {
7653
+ out.set(chunk, at);
7654
+ at += chunk.length;
7655
+ }
7656
+ return out;
7657
+ };
7658
+ request.text = async () => __sbFromUtf8(await all());
7659
+ request.json = async () => JSON.parse(__sbFromUtf8(await all()));
7660
+ request.arrayBuffer = async () => {
7661
+ const bytes = await all();
7662
+ return bytes.byteLength === bytes.buffer.byteLength ? bytes.buffer : bytes.slice().buffer;
7663
+ };
7664
+ request.bytes = async () => await all();
7665
+ }
7666
+
7034
7667
  // Native HTTP ingress stores the body as one character per raw byte. Keep
7035
7668
  // that representation for binary consumers, but decode text lazily before
7036
7669
  // handing the request to user code. Request.text() in Porffor otherwise treats
7037
7670
  // the raw bytes as already-decoded characters and corrupts Unicode/HMAC input.
7038
7671
  function __sbDecodeIngress(request) {
7672
+ const streamed = globalThis.__sbCompat >= "2026-10-27" ? Number(__sbReqBodyOp("t", 0, "")) : 0;
7673
+ if (streamed > 0) {
7674
+ __sbIngressStream(request, streamed);
7675
+ return;
7676
+ }
7039
7677
  const raw = __sbRawBody(request);
7040
7678
  if (raw == null) return;
7041
7679
  // A GET or HEAD has no body, as in workerd (from 2026-10-23): request.body is null, and
@@ -7169,7 +7807,7 @@ globalThis.__sbEntry = function (handlers, request) {
7169
7807
  if (!__sbIsFn(inst.alarm)) return new Response("no alarm handler", { status: 404 });
7170
7808
  let __ares;
7171
7809
  try {
7172
- __ares = inst.alarm();
7810
+ __ares = __sbDOGated() ? __sbDOEnter(inst.__sbGate, () => inst.alarm()) : inst.alarm();
7173
7811
  } catch (error) {
7174
7812
  // #179 — same rule as fetch: don't let a throw here crash the process.
7175
7813
  __sbLogUncaught("alarm", error);
@@ -7194,6 +7832,7 @@ globalThis.__sbEntry = function (handlers, request) {
7194
7832
  async function __sbRunQueueBatch(handlers, body, ctx) {
7195
7833
  const acked = [];
7196
7834
  const retried = [];
7835
+ let threw = false;
7197
7836
  const raw = body.messages || [];
7198
7837
  const messages = [];
7199
7838
  for (let i = 0; i < raw.length; i++) {
@@ -7230,15 +7869,18 @@ async function __sbRunQueueBatch(handlers, body, ctx) {
7230
7869
  // the response go out) before its own `await`s did, so any ack()/retry()
7231
7870
  // past the first one never counted.
7232
7871
  if (__qres && __sbIsFn(__qres.then)) await __qres;
7233
- } catch {
7872
+ } catch (error) {
7234
7873
  // #179 — a throw here must not crash the process. Whatever ack()/retry()
7235
- // calls the handler made before throwing still count; anything it never
7236
- // touched falls through to the default-ack pass below, same as a handler
7237
- // that returns without calling ack()/retry() on every message.
7874
+ // calls the handler made before throwing still count.
7875
+ threw = true;
7876
+ __sbLogUncaught("queue", error);
7238
7877
  }
7239
- // default: any message neither acked nor retried is treated as acked
7878
+ // A message neither acked nor retried is acked when the handler returns, and retried
7879
+ // when it throws, as on Workers. A throw used to ack the whole batch on the embedded
7880
+ // transport (standalone, sproutd), so a failing consumer lost its messages.
7240
7881
  for (let i = 0; i < messages.length; i++) {
7241
- if (acked.indexOf(messages[i].id) === -1 && retried.indexOf(messages[i].id) === -1) acked.push(messages[i].id);
7882
+ if (acked.indexOf(messages[i].id) === -1 && retried.indexOf(messages[i].id) === -1)
7883
+ (threw ? retried : acked).push(messages[i].id);
7242
7884
  }
7243
7885
  return { ack: acked, retry: retried };
7244
7886
  }