@sproutboat/runtime 0.31.0 → 0.33.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.
@@ -4295,6 +4295,9 @@ int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_
4295
4295
  int porf_native_fetch_read_raw_bytes(jsval value, const char** out_buf, size_t* out_len);
4296
4296
  // B22: set only by sproutd's worker host (native-host-bridge.c, at init); null everywhere else.
4297
4297
  void (*sb_host_note_hook)(const char* text, size_t length) = 0;
4298
+ // WebSockets (from 2026-10-30): set only by a host that carries client sockets (sproutd's
4299
+ // native-host-bridge.c, at init); null everywhere else. See __sbWsOp for the ops.
4300
+ long long (*sb_ws_hook)(int op, unsigned int id, const char* data, size_t length, char* out, size_t cap) = 0;
4298
4301
 
4299
4302
  // --- #133: SHA-2 + HMAC ---------------------------------------------------
4300
4303
  // Standalone links BearSSL, but a deployed sprout does not, and this inline C
@@ -5751,9 +5754,182 @@ function __sbAssetResponse(path, r) {
5751
5754
  // object from compile.ts (a `const`, but mutable); we add the binding accessors
5752
5755
  // to it in place. compile.ts emits `__sbInstallBindings(env, {...})` right after
5753
5756
  // the `const env = {...}` line.
5757
+ // Queues from compatibility date 2026-10-31, as Cloudflare documents them:
5758
+ // - send() and sendBatch() return Promises of { metadata: { metrics } } (the queue's backlog
5759
+ // after the send) and check their arguments with workerd's errors: a body over 128 KiB, an
5760
+ // empty batch, more than 100 messages or 256 KiB, a batch that isn't iterable;
5761
+ // - bodies keep their content type: "json" (the default), "text", "bytes" or "v8" (structured
5762
+ // clone, with the Durable Object storage codec), so a string that looks like JSON arrives as
5763
+ // the same string;
5764
+ // - a message is { id, timestamp (a Date: when it was sent), body, attempts } with ack() and
5765
+ // retry({ delaySeconds }) on its prototype; the first of ack() and retry() wins, as a
5766
+ // message's own call wins over ackAll() and retryAll(); a batch has queue (the queue's name),
5767
+ // messages and metadata;
5768
+ // - by default a message is retried at once, 3 times, then dropped (or moved to the dead
5769
+ // letter queue); earlier dates wait 5 s and try 5 times.
5770
+ // The queue name and consumer settings (bindings.queueConfig, from `queues` in
5771
+ // sproutboat.jsonc) apply at every date: they are opt-in.
5772
+ function __sbQueuesStrict() {
5773
+ return globalThis.__sbCompat >= "2026-10-31";
5774
+ }
5775
+
5776
+ // binding -> { binding, queue, maxBatchSize?, maxBatchTimeout?, maxRetries?, retryDelay?, deadLetterQueue? }
5777
+ var __sbQueueConf = {};
5778
+ // queue name -> the binding that stores it
5779
+ var __sbQueueByName = {};
5780
+ function __sbQueueSetup(bindings) {
5781
+ const queues = bindings.queues || [];
5782
+ const config = bindings.queueConfig || [];
5783
+ for (let i = 0; i < queues.length; i++) __sbQueueConf[queues[i]] = { binding: queues[i], queue: queues[i] };
5784
+ for (let i = 0; i < config.length; i++)
5785
+ if (config[i] && __sbQueueConf[config[i].binding]) __sbQueueConf[config[i].binding] = config[i];
5786
+ for (let i = 0; i < queues.length; i++) __sbQueueByName[__sbQueueConf[queues[i]].queue] = queues[i];
5787
+ }
5788
+ function __sbQueueSettings(binding) {
5789
+ return __sbQueueConf[binding] || { binding, queue: binding };
5790
+ }
5791
+ // Where the queue named `name` is stored: its binding, or "#name" for a dead letter queue
5792
+ // this worker doesn't bind (its messages wait there; "#" can't begin a binding name).
5793
+ function __sbQueuePart(name) {
5794
+ return __sbQueueByName[name] || "#" + name;
5795
+ }
5796
+
5797
+ const __sbQueueTag = "#sbq1:";
5798
+ const __sbQueueMaxMessage = 128 * 1024;
5799
+ const __sbQueueMaxBatch = 256 * 1024;
5800
+
5801
+ function __sbQueueBytesOf(value) {
5802
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
5803
+ if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
5804
+ return null;
5805
+ }
5806
+ function __sbQueueTypeName(value) {
5807
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- workerd's error names the received type
5808
+ return value === null ? "null" : typeof value;
5809
+ }
5810
+
5811
+ // A body as the stored text "#sbq1:TYPE:PAYLOAD", and its size as Queues counts it.
5812
+ function __sbQueueEncode(body, contentType) {
5813
+ const type = contentType === undefined ? "json" : contentType;
5814
+ let payload;
5815
+ let size;
5816
+ if (type === "json") {
5817
+ if (body === undefined) throw new TypeError("Message body cannot be undefined");
5818
+ try {
5819
+ payload = JSON.stringify(body);
5820
+ } catch (error) {
5821
+ if (error instanceof TypeError && /BigInt/i.test(String(error.message)))
5822
+ throw new TypeError("Do not know how to serialize a BigInt");
5823
+ throw error;
5824
+ }
5825
+ if (payload === undefined) throw new TypeError("Message body cannot be undefined");
5826
+ size = new TextEncoder().encode(payload).length;
5827
+ } else if (type === "text") {
5828
+ if (!__sbIsStr(body))
5829
+ throw new TypeError('Content Type "text" requires a value of type string, but received: ' + __sbQueueTypeName(body));
5830
+ payload = body;
5831
+ size = new TextEncoder().encode(body).length;
5832
+ } else if (type === "bytes") {
5833
+ const bytes = __sbQueueBytesOf(body);
5834
+ if (bytes === null)
5835
+ throw new TypeError('Content Type "bytes" requires a value of type ArrayBuffer, but received: ' + __sbQueueTypeName(body));
5836
+ payload = __sbDOHex(bytes);
5837
+ size = bytes.length;
5838
+ } else if (type === "v8") {
5839
+ payload = JSON.stringify(__sbDOPack(body, []));
5840
+ size = new TextEncoder().encode(payload).length;
5841
+ } else throw new TypeError("Unsupported queue message content type: " + type);
5842
+ if (size > __sbQueueMaxMessage)
5843
+ throw new Error("Queue send failed: message is " + size + " bytes, over the limit of " + __sbQueueMaxMessage);
5844
+ return { text: __sbQueueTag + type + ":" + payload, size };
5845
+ }
5846
+
5847
+ // Reads both formats at every date: tagged (2026-10-31 sends) and the earlier text.
5848
+ function __sbQueueDecode(raw) {
5849
+ if (__sbIsStr(raw) && raw.indexOf(__sbQueueTag) === 0) {
5850
+ const rest = raw.slice(__sbQueueTag.length);
5851
+ const colon = rest.indexOf(":");
5852
+ const type = rest.slice(0, colon);
5853
+ const payload = rest.slice(colon + 1);
5854
+ if (type === "text") return payload;
5855
+ if (type === "json") return JSON.parse(payload);
5856
+ if (type === "bytes") return __sbDOUnhex(payload).buffer;
5857
+ if (type === "v8") return __sbDOUnpack(JSON.parse(payload));
5858
+ }
5859
+ return __sbTryParse(raw);
5860
+ }
5861
+
5862
+ function __sbQueueMetrics(metrics) {
5863
+ const m = metrics || {};
5864
+ const oldest = Number(m.oldestMessageTimestamp) || 0;
5865
+ return {
5866
+ backlogCount: Number(m.backlogCount) || 0,
5867
+ backlogBytes: Number(m.backlogBytes) || 0,
5868
+ // Milliseconds since the epoch, as Cloudflare documents it; none for an empty queue.
5869
+ oldestMessageTimestamp: oldest > 0 ? oldest : undefined,
5870
+ };
5871
+ }
5872
+
5873
+ function __sbQueueDelay(value) {
5874
+ return value == null ? 0 : Math.max(0, Number(value) || 0);
5875
+ }
5876
+
5877
+ function __sbMakeQueue(binding) {
5878
+ return {
5879
+ send(body, options) {
5880
+ try {
5881
+ const o = options || {};
5882
+ const encoded = __sbQueueEncode(body, o.contentType);
5883
+ const reply = __sbRpc("queue.send", {
5884
+ queue: binding,
5885
+ body: encoded.text,
5886
+ delaySeconds: __sbQueueDelay(o.delaySeconds),
5887
+ metrics: true,
5888
+ });
5889
+ return Promise.resolve({ metadata: { metrics: __sbQueueMetrics(reply.metrics) } });
5890
+ } catch (error) {
5891
+ return Promise.reject(error);
5892
+ }
5893
+ },
5894
+ sendBatch(messages, options) {
5895
+ try {
5896
+ if (messages == null || !__sbIsFn(messages[Symbol.iterator]))
5897
+ throw new TypeError("Failed to execute 'sendBatch' on 'WorkerQueue': parameter 1 is not of type 'Sequence'.");
5898
+ const batchDelay = __sbQueueDelay(options && options.delaySeconds);
5899
+ const list = [];
5900
+ let total = 0;
5901
+ for (const m of messages) {
5902
+ const item = m || {};
5903
+ const encoded = __sbQueueEncode(item.body, item.contentType);
5904
+ total += encoded.size;
5905
+ // A message delay of 0 (or none) takes the batch delay, as miniflare does.
5906
+ list.push({ body: encoded.text, delaySeconds: __sbQueueDelay(item.delaySeconds) || batchDelay });
5907
+ }
5908
+ if (list.length === 0) throw new TypeError("sendBatch() requires at least one message");
5909
+ if (list.length > 100) throw new Error("Queue sendBatch failed: " + list.length + " messages, over the limit of 100");
5910
+ if (total > __sbQueueMaxBatch)
5911
+ throw new Error("Queue sendBatch failed: " + total + " bytes, over the limit of " + __sbQueueMaxBatch);
5912
+ const reply = __sbRpc("queue.send_batch", { queue: binding, messages: list, metrics: true });
5913
+ return Promise.resolve({ metadata: { metrics: __sbQueueMetrics(reply.metrics) } });
5914
+ } catch (error) {
5915
+ return Promise.reject(error);
5916
+ }
5917
+ },
5918
+ metrics() {
5919
+ try {
5920
+ return Promise.resolve(__sbQueueMetrics(__sbRpc("queue.metrics", { queue: binding }).metrics));
5921
+ } catch (error) {
5922
+ return Promise.reject(error);
5923
+ }
5924
+ },
5925
+ };
5926
+ }
5927
+
5754
5928
  globalThis.__sbInstallBindings = function (target, bindings) {
5755
5929
  if (!target) return;
5756
5930
 
5931
+ __sbQueueSetup(bindings);
5932
+
5757
5933
  for (let i = 0; i < (bindings.kv || []).length; i++) {
5758
5934
  const ns = bindings.kv[i];
5759
5935
  target[ns] = __sbKvStrict() ? __sbMakeKv(ns) : {
@@ -5898,7 +6074,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
5898
6074
 
5899
6075
  for (let i = 0; i < (bindings.queues || []).length; i++) {
5900
6076
  const name = bindings.queues[i];
5901
- target[name] = {
6077
+ target[name] = __sbQueuesStrict() ? __sbMakeQueue(name) : {
5902
6078
  send(body, options) {
5903
6079
  const o = options || {};
5904
6080
  __sbRpc("queue.send", {
@@ -6498,6 +6674,7 @@ function __sbUpstream(op, msg, label, signal, always) {
6498
6674
  // prelude must not replace fetch just by being evaluated.
6499
6675
  globalThis.__sbInstallFetch = function () {
6500
6676
  __sbIngressStreamingSetup();
6677
+ __sbInstallWebSockets();
6501
6678
  globalThis.fetch = function (input, init) {
6502
6679
  let parts;
6503
6680
  try {
@@ -6859,7 +7036,11 @@ function __sbGetDOInstance(cls, idText, idObj) {
6859
7036
  if (p && __sbIsFn(p.then)) __sbTasks.push(p);
6860
7037
  },
6861
7038
  };
7039
+ // The hibernation API, from 2026-10-30 (where WebSocketPair exists).
7040
+ const holder = { inst: null, auto: null };
7041
+ if (__sbWsEnabled()) __sbDOWebSocketState(state, holder);
6862
7042
  inst = new Ctor(state, globalThis.env);
7043
+ holder.inst = inst;
6863
7044
  inst.__sbTasks = __sbTasks;
6864
7045
  inst.__sbGate = gate;
6865
7046
  __sbDOInstances[cacheKey] = inst;
@@ -7841,10 +8022,414 @@ function __sbStreamResponse(response) {
7841
8022
  return response;
7842
8023
  }
7843
8024
 
7844
- async function __sbFetchWithDrain(res, tasks, bindingCall, rethrow) {
8025
+ // ---------------------------------------------------------------------------
8026
+ // WebSockets, from compatibility date 2026-10-30, as in workerd: WebSocketPair, accept(),
8027
+ // send(), close(), message and close events, new Response(null, { status: 101, webSocket })
8028
+ // and, for Durable Objects, the hibernation API (__sbDOWebSocketState).
8029
+ //
8030
+ // A pair whose ends both stay in this worker (a Worker and its Durable Object) is plain
8031
+ // JavaScript, so it works on every host. The client end of a 101 response becomes a real
8032
+ // socket only on a host that carries it (sproutd: its bridge sets sb_ws_hook); the host
8033
+ // then stands in for that end: what the app sends to it goes out, and what the remote
8034
+ // client sends arrives as if that end had sent it.
8035
+ //
8036
+ // __sbWsOp(op, id, data, n), on the host socket `id`:
8037
+ // "o" open one (returns its id, "0" without a host), "t" send text data, "b" send the
8038
+ // bytes in data, "c" close with code n and reason data, "p" poll the next event ("" for
8039
+ // none, else "KIND LENGTH CODE": 0 text, 1 binary, 2 close, 3 connection lost), "i" take
8040
+ // that event's payload into the Uint8Array data, "k" acknowledge n bytes dispatched,
8041
+ // "w" fire timer n when an event arrives, "r" release. Returns a short string.
8042
+ // oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
8043
+ function __sbWsOp(op, id, data, n) {
8044
+ let out = "";
8045
+ // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
8046
+ Porffor.c`
8047
+ const char* __o; size_t __ol; char* __oo = 0;
8048
+ porf_native_fetch_read_value(op, &__o, &__ol, &__oo);
8049
+ char __op = __ol ? __o[0] : 0;
8050
+ if (__oo) free(__oo);
8051
+ const unsigned int __id = id.type == 1 && id.val > 0 ? (unsigned int)id.val : 0;
8052
+ const size_t __n = n.type == 1 && n.val > 0 ? (size_t)n.val : 0;
8053
+ char __r[64]; __r[0] = 0;
8054
+ if (sb_ws_hook) {
8055
+ long long __w = -1;
8056
+ if (__op == 'p') {
8057
+ sb_ws_hook('p', __id, 0, 0, __r, sizeof(__r));
8058
+ } else if (__op == 'i') {
8059
+ char* __at = 0; size_t __cap = 0;
8060
+ if (data.type == 81) {
8061
+ const u32 __ta = (u32)data.val;
8062
+ const int32_t __len = *(int32_t*)(MEM + __ta);
8063
+ if (__len > 0) { __at = (char*)(MEM + *(u32*)(MEM + __ta + 4) + 4); __cap = (size_t)__len; }
8064
+ }
8065
+ __w = sb_ws_hook('i', __id, 0, 0, __at, __cap);
8066
+ snprintf(__r, sizeof(__r), "%lld", __w);
8067
+ } else if (__op == 'b') {
8068
+ const char* __d = 0; size_t __dl = 0;
8069
+ if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_ws_hook('b', __id, __d, __dl, 0, 0);
8070
+ snprintf(__r, sizeof(__r), "%lld", __w);
8071
+ } else if (__op == 't' || __op == 'c') {
8072
+ const char* __d; size_t __dl; char* __do = 0;
8073
+ porf_native_fetch_read_value(data, &__d, &__dl, &__do);
8074
+ __w = sb_ws_hook(__op, __id, __d, __dl, 0, __n);
8075
+ if (__do) free(__do);
8076
+ snprintf(__r, sizeof(__r), "%lld", __w);
8077
+ } else {
8078
+ __w = sb_ws_hook(__op, __id, 0, 0, 0, __n);
8079
+ snprintf(__r, sizeof(__r), "%lld", __w);
8080
+ }
8081
+ } else if (__op == 'o') snprintf(__r, sizeof(__r), "0");
8082
+ out = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
8083
+ `;
8084
+ return out;
8085
+ }
8086
+
8087
+ const __SB_WS_OPEN = 1;
8088
+ const __SB_WS_CLOSING = 2;
8089
+ const __SB_WS_CLOSED = 3;
8090
+ const __sbWsKey = {};
8091
+
8092
+ // One end of a WebSocketPair. Events are plain objects with the fields apps read (type,
8093
+ // data; code, reason, wasClean), dispatched to on<type> (when assigned) and then to the
8094
+ // addEventListener listeners, as workerd does. Binary messages arrive as a Blob unless
8095
+ // binaryType is "arraybuffer" (workerd's standard binary type).
8096
+ class __SbWebSocket {
8097
+ constructor(key) {
8098
+ // Outbound WebSockets (new WebSocket(url)) are not supported; the CLI refuses them too.
8099
+ if (key !== __sbWsKey) throw new TypeError("WebSocket: only WebSocketPair can make a WebSocket here");
8100
+ this._peer = null;
8101
+ this._accepted = false;
8102
+ this._inbox = [];
8103
+ this._flushing = false;
8104
+ this._listeners = {};
8105
+ this._state = __SB_WS_OPEN;
8106
+ this._closeSent = false;
8107
+ this._closeSeen = false;
8108
+ // The host socket this end stands for (the client end of a 101 response), or 0.
8109
+ this._host = 0;
8110
+ // { holder, tags } once a Durable Object took it with ctx.acceptWebSocket().
8111
+ this._hib = null;
8112
+ this._attachment = undefined;
8113
+ this.binaryType = "blob";
8114
+ this.url = null;
8115
+ this.protocol = "";
8116
+ this.extensions = "";
8117
+ }
8118
+ get readyState() {
8119
+ return this._state;
8120
+ }
8121
+ get bufferedAmount() {
8122
+ return 0;
8123
+ }
8124
+ accept() {
8125
+ if (this._host !== 0) throw new TypeError("Can't accept() a WebSocket that was already used in a response.");
8126
+ if (this._hib !== null) throw new TypeError("Can't accept() a WebSocket that was accepted with acceptWebSocket().");
8127
+ if (this._accepted) return;
8128
+ this._accepted = true;
8129
+ __sbWsSchedule(this);
8130
+ }
8131
+ send(message) {
8132
+ if (!this._accepted && this._hib === null)
8133
+ throw new TypeError("You must call one of accept() or state.acceptWebSocket() on this WebSocket before sending messages.");
8134
+ if (this._closeSent) throw new TypeError("Can't call WebSocket send() after close().");
8135
+ if (this._state === __SB_WS_CLOSED || this._peer === null) return;
8136
+ let event;
8137
+ if (__sbIsStr(message)) event = { kind: 0, data: message };
8138
+ else if (message instanceof ArrayBuffer) event = { kind: 1, data: message.slice(0) };
8139
+ else if (ArrayBuffer.isView(message))
8140
+ event = { kind: 1, data: new Uint8Array(message.buffer, message.byteOffset, message.byteLength).slice().buffer };
8141
+ else event = { kind: 0, data: String(message) };
8142
+ __sbWsDeliver(this._peer, event);
8143
+ }
8144
+ close(code, reason) {
8145
+ if (code !== undefined) {
8146
+ code = Number(code);
8147
+ if (!(code === 1000 || (code >= 3000 && code <= 4999))) throw __sbCryptoError("InvalidAccessError", "Invalid WebSocket close code: " + code + ".");
8148
+ }
8149
+ const text = reason === undefined ? "" : String(reason);
8150
+ if (__sbToBytes(text).length > 123)
8151
+ throw __sbCryptoError("SyntaxError", "WebSocket close reason must not be longer than 123 bytes when UTF-8 encoded.");
8152
+ if (this._closeSent) return;
8153
+ this._closeSent = true;
8154
+ this._state = this._closeSeen ? __SB_WS_CLOSED : __SB_WS_CLOSING;
8155
+ if (this._peer !== null) __sbWsDeliver(this._peer, { kind: 2, code: code === undefined ? 1005 : code, reason: text, clean: true });
8156
+ }
8157
+ addEventListener(type, listener) {
8158
+ if (listener == null) return;
8159
+ const list = this._listeners[type] || (this._listeners[type] = []);
8160
+ for (let i = 0; i < list.length; i++) if (list[i] === listener) return;
8161
+ list.push(listener);
8162
+ }
8163
+ removeEventListener(type, listener) {
8164
+ const list = this._listeners[type];
8165
+ if (!list) return;
8166
+ for (let i = 0; i < list.length; i++) {
8167
+ if (list[i] === listener) {
8168
+ list.splice(i, 1);
8169
+ return;
8170
+ }
8171
+ }
8172
+ }
8173
+ dispatchEvent(event) {
8174
+ __sbWsFire(this, event);
8175
+ return true;
8176
+ }
8177
+ serializeAttachment(value) {
8178
+ this._attachment = value === undefined ? undefined : structuredClone(value);
8179
+ }
8180
+ deserializeAttachment() {
8181
+ return this._attachment === undefined ? null : structuredClone(this._attachment);
8182
+ }
8183
+ }
8184
+ __SbWebSocket.CONNECTING = 0;
8185
+ __SbWebSocket.OPEN = __SB_WS_OPEN;
8186
+ __SbWebSocket.CLOSING = __SB_WS_CLOSING;
8187
+ __SbWebSocket.CLOSED = __SB_WS_CLOSED;
8188
+ __SbWebSocket.READY_STATE_CONNECTING = 0;
8189
+ __SbWebSocket.READY_STATE_OPEN = __SB_WS_OPEN;
8190
+ __SbWebSocket.READY_STATE_CLOSING = __SB_WS_CLOSING;
8191
+ __SbWebSocket.READY_STATE_CLOSED = __SB_WS_CLOSED;
8192
+
8193
+ class __SbWebSocketPair {
8194
+ constructor() {
8195
+ const client = new __SbWebSocket(__sbWsKey);
8196
+ const server = new __SbWebSocket(__sbWsKey);
8197
+ client._peer = server;
8198
+ server._peer = client;
8199
+ this[0] = client;
8200
+ this[1] = server;
8201
+ }
8202
+ }
8203
+
8204
+ class __SbWebSocketRequestResponsePair {
8205
+ constructor(request, response) {
8206
+ if (!__sbIsStr(request) || !__sbIsStr(response)) throw new TypeError("WebSocketRequestResponsePair needs two strings");
8207
+ this.request = request;
8208
+ this.response = response;
8209
+ }
8210
+ }
8211
+
8212
+ function __sbWsFire(ws, event) {
8213
+ const handler = ws["on" + event.type];
8214
+ if (__sbIsFn(handler)) {
8215
+ try {
8216
+ handler.call(ws, event);
8217
+ } catch (error) {
8218
+ __sbLogUncaught("websocket", error);
8219
+ }
8220
+ }
8221
+ const list = ws._listeners[event.type];
8222
+ if (!list) return;
8223
+ const copy = list.slice();
8224
+ for (let i = 0; i < copy.length; i++) {
8225
+ const fn = copy[i];
8226
+ try {
8227
+ if (__sbIsFn(fn)) fn.call(ws, event);
8228
+ else if (fn && __sbIsFn(fn.handleEvent)) fn.handleEvent(event);
8229
+ } catch (error) {
8230
+ __sbLogUncaught("websocket", error);
8231
+ }
8232
+ }
8233
+ }
8234
+
8235
+ // An event arrives at `ws`, sent by its peer: to the host when `ws` is a host socket,
8236
+ // else into ws's inbox, dispatched once it is accepted.
8237
+ function __sbWsDeliver(ws, event) {
8238
+ if (ws._host !== 0) {
8239
+ const id = ws._host;
8240
+ if (event.kind === 0) __sbWsOp("t", id, event.data, 0);
8241
+ else if (event.kind === 1) __sbWsOp("b", id, __sbToBytes(new Uint8Array(event.data)), 0);
8242
+ else if (!ws._closeSeen) {
8243
+ ws._closeSeen = true;
8244
+ __sbWsOp("c", id, event.reason, event.code);
8245
+ }
8246
+ return;
8247
+ }
8248
+ ws._inbox.push(event);
8249
+ __sbWsSchedule(ws);
8250
+ }
8251
+
8252
+ // One event per turn, as workerd's receive loop does: a reply the handler sends reaches the
8253
+ // other end before this end's next message.
8254
+ function __sbWsSchedule(ws) {
8255
+ if (ws._flushing || !ws._inbox.length || (!ws._accepted && ws._hib === null)) return;
8256
+ ws._flushing = true;
8257
+ Promise.resolve().then(() => {
8258
+ ws._flushing = false;
8259
+ if (ws._inbox.length && (ws._accepted || ws._hib !== null)) __sbWsDispatch(ws, ws._inbox.shift());
8260
+ __sbWsSchedule(ws);
8261
+ });
8262
+ }
8263
+
8264
+ function __sbWsFlush(ws) {
8265
+ while (ws._inbox.length && (ws._accepted || ws._hib !== null)) __sbWsDispatch(ws, ws._inbox.shift());
8266
+ }
8267
+
8268
+ // Dispatches one event that arrived at ws: to its listeners, or to its Durable Object's
8269
+ // webSocketMessage / webSocketClose. A close is answered with a close (workerd replies to
8270
+ // a close automatically), and ends the socket.
8271
+ function __sbWsDispatch(ws, event) {
8272
+ if (event.kind === 2) {
8273
+ ws._closeSeen = true;
8274
+ if (!ws._closeSent) {
8275
+ ws._closeSent = true;
8276
+ if (ws._peer !== null) __sbWsDeliver(ws._peer, { kind: 2, code: event.code, reason: event.reason, clean: event.clean });
8277
+ }
8278
+ ws._state = __SB_WS_CLOSED;
8279
+ if (ws._hib !== null) return __sbWsHibernated(ws, "webSocketClose", [ws, event.code, event.reason, event.clean]);
8280
+ return __sbWsFire(ws, { type: "close", code: event.code, reason: event.reason, wasClean: event.clean, target: ws });
8281
+ }
8282
+ if (ws._hib !== null) {
8283
+ const auto = ws._hib.holder.auto;
8284
+ if (auto !== null && event.kind === 0 && event.data === auto.request) {
8285
+ ws.send(auto.response);
8286
+ return;
8287
+ }
8288
+ return __sbWsHibernated(ws, "webSocketMessage", [ws, event.data]);
8289
+ }
8290
+ const data = event.kind === 1 && ws.binaryType !== "arraybuffer" ? new Blob([event.data]) : event.data;
8291
+ __sbWsFire(ws, { type: "message", data, target: ws });
8292
+ }
8293
+
8294
+ // A hibernatable socket's event runs the object's handler through its input gate.
8295
+ function __sbWsHibernated(ws, name, args) {
8296
+ const inst = ws._hib.holder.inst;
8297
+ if (inst == null || !__sbIsFn(inst[name])) return;
8298
+ const run = () => {
8299
+ if (name === "webSocketClose") return inst.webSocketClose(args[0], args[1], args[2], args[3]);
8300
+ return inst.webSocketMessage(args[0], args[1]);
8301
+ };
8302
+ const done = (p) => {
8303
+ if (p && __sbIsFn(p.then)) p.then(undefined, (error) => __sbLogUncaught("websocket", error));
8304
+ };
8305
+ try {
8306
+ done(__sbDOGated() ? __sbDOEnter(inst.__sbGate, run) : run());
8307
+ } catch (error) {
8308
+ __sbLogUncaught("websocket", error);
8309
+ }
8310
+ }
8311
+
8312
+ // ctx.acceptWebSocket() and the rest of the hibernation API, added to a Durable Object's
8313
+ // state from 2026-10-30. `holder.inst` is the object once it exists.
8314
+ function __sbDOWebSocketState(state, holder) {
8315
+ const sockets = [];
8316
+ const live = () => {
8317
+ for (let i = sockets.length - 1; i >= 0; i--) if (sockets[i]._state === __SB_WS_CLOSED) sockets.splice(i, 1);
8318
+ return sockets;
8319
+ };
8320
+ state.acceptWebSocket = function (ws, tags) {
8321
+ if (!(ws instanceof __SbWebSocket)) throw new TypeError("acceptWebSocket needs a WebSocket");
8322
+ if (ws._accepted || ws._hib !== null || ws._host !== 0) throw new TypeError("This WebSocket was already accepted.");
8323
+ const list = [];
8324
+ if (tags != null) for (const t of tags) list.push(String(t));
8325
+ if (list.length > 10) throw new TypeError("A WebSocket can have at most 10 tags.");
8326
+ ws._hib = { holder, tags: list };
8327
+ live().push(ws);
8328
+ __sbWsSchedule(ws);
8329
+ };
8330
+ state.getWebSockets = function (tag) {
8331
+ const out = [];
8332
+ const all = live();
8333
+ for (let i = 0; i < all.length; i++) if (tag === undefined || all[i]._hib.tags.indexOf(String(tag)) >= 0) out.push(all[i]);
8334
+ return out;
8335
+ };
8336
+ state.getTags = function (ws) {
8337
+ if (!(ws instanceof __SbWebSocket) || ws._hib === null) throw new TypeError("getTags: not a WebSocket accepted by this object");
8338
+ return ws._hib.tags.slice();
8339
+ };
8340
+ state.setWebSocketAutoResponse = function (pair) {
8341
+ if (pair != null && !(pair instanceof __SbWebSocketRequestResponsePair))
8342
+ throw new TypeError("setWebSocketAutoResponse needs a WebSocketRequestResponsePair");
8343
+ holder.auto = pair == null ? null : pair;
8344
+ };
8345
+ state.getWebSocketAutoResponse = function () {
8346
+ return holder.auto;
8347
+ };
8348
+ state.getWebSocketAutoResponseTimestamp = function () {
8349
+ return null;
8350
+ };
8351
+ state.setHibernatableWebSocketEventTimeout = function () {};
8352
+ state.getHibernatableWebSocketEventTimeout = function () {
8353
+ return null;
8354
+ };
8355
+ }
8356
+
8357
+ function __sbWsEnabled() {
8358
+ return globalThis.WebSocketPair === __SbWebSocketPair;
8359
+ }
8360
+ function __sbHasWebSocket(r) {
8361
+ return r != null && r._webSocket != null && __sbWsEnabled();
8362
+ }
8363
+
8364
+ // The client end of a 101 response becomes a host socket: marked x-sb-websocket for the
8365
+ // host (which upgrades the connection), with what the app already sent going out after it.
8366
+ function __sbWsHostResponse(r) {
8367
+ const ws = r._webSocket;
8368
+ if (!(ws instanceof __SbWebSocket) || ws._host !== 0 || ws._accepted || ws._hib !== null) {
8369
+ return __sbErrorResponse(new TypeError("A 101 response needs the unaccepted client end of a WebSocketPair."), false);
8370
+ }
8371
+ const id = Number(__sbWsOp("o", 0, "", 0));
8372
+ if (!(id > 0)) return new Response("WebSockets are not supported by this host yet", { status: 501 });
8373
+ ws._host = id;
8374
+ r.headers._immutable = false;
8375
+ r.headers.set("x-sb-websocket", String(id));
8376
+ const held = ws._inbox;
8377
+ ws._inbox = [];
8378
+ for (let i = 0; i < held.length; i++) __sbWsDeliver(ws, held[i]);
8379
+ __sbWsPump(ws);
8380
+ return r;
8381
+ }
8382
+
8383
+ // Takes what the host received on ws's socket and dispatches it to the app's end, then
8384
+ // waits (a parked timer the host fires) for more.
8385
+ function __sbWsPump(ws) {
8386
+ const id = ws._host;
8387
+ for (;;) {
8388
+ const head = __sbWsOp("p", id, "", 0);
8389
+ if (head === "") {
8390
+ const timer = setTimeout(() => __sbWsPump(ws), 2147483647);
8391
+ __sbWsOp("w", id, "", timer);
8392
+ return;
8393
+ }
8394
+ const parts = head.split(" ");
8395
+ const kind = Number(parts[0]);
8396
+ const length = Number(parts[1]);
8397
+ const bytes = new Uint8Array(length);
8398
+ __sbWsOp("i", id, bytes, 0);
8399
+ let event;
8400
+ if (kind === 1) event = { kind: 1, data: bytes.buffer };
8401
+ else if (kind === 0 || kind === 2) {
8402
+ let text = length > 0 ? __sbFromUtf8Native(bytes) : "";
8403
+ if (text === "" && length > 0) text = new TextDecoder().decode(bytes);
8404
+ event = kind === 0 ? { kind: 0, data: text } : { kind: 2, code: Number(parts[2]), reason: text, clean: true };
8405
+ } else event = { kind: 2, code: 1006, reason: "WebSocket disconnected without sending Close frame.", clean: false };
8406
+ if (event.kind === 2) ws._closeSeen = true;
8407
+ const peer = ws._peer;
8408
+ if (peer !== null) {
8409
+ peer._inbox.push(event);
8410
+ // Dispatched now, so the acknowledgement below means the handler has run.
8411
+ if (!peer._flushing) __sbWsFlush(peer);
8412
+ }
8413
+ __sbWsOp("k", id, "", length);
8414
+ if (event.kind === 2) {
8415
+ __sbWsOp("r", id, "", 0);
8416
+ return;
8417
+ }
8418
+ }
8419
+ }
8420
+
8421
+ function __sbInstallWebSockets() {
8422
+ if (globalThis.__sbCompat < "2026-10-30") return;
8423
+ globalThis.WebSocketPair = __SbWebSocketPair;
8424
+ globalThis.WebSocket = __SbWebSocket;
8425
+ globalThis.WebSocketRequestResponsePair = __SbWebSocketRequestResponsePair;
8426
+ }
8427
+
8428
+ async function __sbFetchWithDrain(res, tasks, bindingCall, rethrow, top) {
7845
8429
  let r;
7846
8430
  try {
7847
8431
  r = res && __sbIsFn(res.then) ? await res : res;
8432
+ if (top && __sbHasWebSocket(r)) r = __sbWsHostResponse(r);
7848
8433
  if (__sbHasStreamBody(r)) {
7849
8434
  if (__sbStreamingSupported()) __sbStreamResponse(r);
7850
8435
  else r.body = await __sbCollectBody(__sbRawBody(r));
@@ -8141,7 +8726,9 @@ globalThis.__sbEntry = function (handlers, request) {
8141
8726
  }
8142
8727
  // Same promise-identity rule as above applies to `__sbFetchWithDrain`'s
8143
8728
  // own return, which is why it's tail-called rather than chained on here.
8144
- if (__sbTasks.length || (__res && __sbIsFn(__res.then)) || __sbHasStreamBody(__res)) return __sbFetchWithDrain(__res, __sbTasks, bindingCall);
8729
+ if (__sbTasks.length || (__res && __sbIsFn(__res.then)) || __sbHasStreamBody(__res)) return __sbFetchWithDrain(__res, __sbTasks, bindingCall, false, true);
8730
+ // A WebSocket handshake answer (from 2026-10-30): its client end becomes the host socket.
8731
+ if (__sbHasWebSocket(__res)) return __sbWsHostResponse(__res);
8145
8732
  return __sbTagCpu(__res, __t0);
8146
8733
  }
8147
8734
  if (!__sbTriggerAuthed(request)) return new Response("forbidden", { status: 403 });
@@ -8226,37 +8813,64 @@ globalThis.__sbEntry = function (handlers, request) {
8226
8813
  * timer in an embedded binary that has no broker to be delivered from.
8227
8814
  */
8228
8815
  async function __sbRunQueueBatch(handlers, body, ctx) {
8816
+ const strict = __sbQueuesStrict();
8817
+ const binding = String(body.queue || "");
8818
+ const settings = __sbQueueSettings(binding);
8229
8819
  const acked = [];
8230
8820
  const retried = [];
8821
+ // id -> seconds, from retry({ delaySeconds }) (2026-10-31)
8822
+ const delays = {};
8231
8823
  let threw = false;
8232
8824
  const raw = body.messages || [];
8233
8825
  const messages = [];
8234
- for (let i = 0; i < raw.length; i++) {
8235
- const m = raw[i];
8236
- const msg = {
8237
- id: m.id,
8238
- timestamp: m.timestamp,
8239
- attempts: m.attempts || 1,
8240
- body: __sbTryParse(m.body),
8241
- ack() {
8242
- if (acked.indexOf(m.id) === -1) acked.push(m.id);
8826
+ let batch;
8827
+ if (strict) {
8828
+ // The first of ack() and retry() on a message wins; ackAll() and retryAll() decide the
8829
+ // messages nothing has decided yet.
8830
+ const decided = {};
8831
+ const ack = (id) => {
8832
+ if (decided[id] !== true) {
8833
+ decided[id] = true;
8834
+ acked.push(id);
8835
+ }
8836
+ };
8837
+ const retry = (id, options) => {
8838
+ if (decided[id] !== true) {
8839
+ decided[id] = true;
8840
+ retried.push(id);
8841
+ if (options && options.delaySeconds != null) delays[id] = __sbQueueDelay(options.delaySeconds);
8842
+ }
8843
+ };
8844
+ for (let i = 0; i < raw.length; i++) messages.push(new __SbQueueMessage(raw[i], ack, retry));
8845
+ batch = new __SbQueueBatch(settings.queue, messages, body.metadata, ack, retry);
8846
+ } else {
8847
+ for (let i = 0; i < raw.length; i++) {
8848
+ const m = raw[i];
8849
+ const msg = {
8850
+ id: m.id,
8851
+ timestamp: m.timestamp,
8852
+ attempts: m.attempts || 1,
8853
+ body: __sbQueueDecode(m.body),
8854
+ ack() {
8855
+ if (acked.indexOf(m.id) === -1) acked.push(m.id);
8856
+ },
8857
+ retry() {
8858
+ if (retried.indexOf(m.id) === -1) retried.push(m.id);
8859
+ },
8860
+ };
8861
+ messages.push(msg);
8862
+ }
8863
+ batch = {
8864
+ queue: settings.queue,
8865
+ messages,
8866
+ ackAll() {
8867
+ for (let i = 0; i < messages.length; i++) messages[i].ack();
8243
8868
  },
8244
- retry() {
8245
- if (retried.indexOf(m.id) === -1) retried.push(m.id);
8869
+ retryAll() {
8870
+ for (let i = 0; i < messages.length; i++) messages[i].retry();
8246
8871
  },
8247
8872
  };
8248
- messages.push(msg);
8249
8873
  }
8250
- const batch = {
8251
- queue: body.queue || "",
8252
- messages,
8253
- ackAll() {
8254
- for (let i = 0; i < messages.length; i++) messages[i].ack();
8255
- },
8256
- retryAll() {
8257
- for (let i = 0; i < messages.length; i++) messages[i].retry();
8258
- },
8259
- };
8260
8874
  try {
8261
8875
  const __qres = globalThis.__sbCompat >= "2026-09-28"
8262
8876
  ? handlers.queue(batch, globalThis.env, ctx)
@@ -8274,12 +8888,65 @@ async function __sbRunQueueBatch(handlers, body, ctx) {
8274
8888
  // A message neither acked nor retried is acked when the handler returns, and retried
8275
8889
  // when it throws, as on Workers. A throw used to ack the whole batch on the embedded
8276
8890
  // transport (standalone, sproutd), so a failing consumer lost its messages.
8277
- for (let i = 0; i < messages.length; i++) {
8278
- if (acked.indexOf(messages[i].id) === -1 && retried.indexOf(messages[i].id) === -1)
8279
- (threw ? retried : acked).push(messages[i].id);
8891
+ for (let i = 0; i < raw.length; i++) {
8892
+ if (acked.indexOf(raw[i].id) === -1 && retried.indexOf(raw[i].id) === -1)
8893
+ (threw ? retried : acked).push(raw[i].id);
8894
+ }
8895
+ const out = { ack: acked, retry: retried };
8896
+ // Retry delays and what happens past the last retry. Sent only where they differ from
8897
+ // what a store does by itself (5 s, 5 attempts, then marked dead), so earlier dates
8898
+ // without settings answer as before. A store that predates these fields (an older
8899
+ // broker) ignores them and retries the messages listed in `retry`.
8900
+ if (strict || settings.maxRetries !== undefined || settings.retryDelay !== undefined || settings.deadLetterQueue !== undefined) {
8901
+ const maxRetries = settings.maxRetries !== undefined ? settings.maxRetries : strict ? 3 : 4;
8902
+ const retryDelay = settings.retryDelay !== undefined ? settings.retryDelay : strict ? 0 : 5;
8903
+ const attempts = {};
8904
+ for (let i = 0; i < raw.length; i++) attempts[raw[i].id] = Number(raw[i].attempts) || 1;
8905
+ out.delays = {};
8906
+ out.dead = [];
8907
+ for (let i = 0; i < retried.length; i++) {
8908
+ const id = retried[i];
8909
+ if (attempts[id] > maxRetries) out.dead.push(id);
8910
+ else out.delays[id] = delays[id] !== undefined ? delays[id] : retryDelay;
8911
+ }
8912
+ if (out.dead.length && settings.deadLetterQueue !== undefined) out.deadLetter = __sbQueuePart(settings.deadLetterQueue);
8280
8913
  }
8281
- return { ack: acked, retry: retried };
8914
+ return out;
8915
+ }
8916
+
8917
+ // A delivered message and batch from 2026-10-31: their data are own properties, as in
8918
+ // workerd, and ack/retry live on the prototype, out of Object.keys().
8919
+ function __SbQueueMessage(m, ack, retry) {
8920
+ this.id = String(m.id);
8921
+ this.timestamp = new Date(Number(m.timestamp) || Date.now());
8922
+ let decoded;
8923
+ try {
8924
+ decoded = __sbQueueDecode(m.body);
8925
+ } catch {
8926
+ decoded = m.body;
8927
+ }
8928
+ this.body = decoded;
8929
+ this.attempts = Number(m.attempts) || 1;
8930
+ Object.defineProperty(this, "__sbDecide", { value: { ack, retry }, enumerable: false });
8282
8931
  }
8932
+ __SbQueueMessage.prototype.ack = function () {
8933
+ this.__sbDecide.ack(this.id);
8934
+ };
8935
+ __SbQueueMessage.prototype.retry = function (options) {
8936
+ this.__sbDecide.retry(this.id, options);
8937
+ };
8938
+ function __SbQueueBatch(queue, messages, metadata, ack, retry) {
8939
+ this.queue = queue;
8940
+ this.messages = messages;
8941
+ this.metadata = { metrics: __sbQueueMetrics(metadata && metadata.metrics) };
8942
+ Object.defineProperty(this, "__sbDecide", { value: { ack, retry }, enumerable: false });
8943
+ }
8944
+ __SbQueueBatch.prototype.ackAll = function () {
8945
+ for (let i = 0; i < this.messages.length; i++) this.__sbDecide.ack(this.messages[i].id);
8946
+ };
8947
+ __SbQueueBatch.prototype.retryAll = function (options) {
8948
+ for (let i = 0; i < this.messages.length; i++) this.__sbDecide.retry(this.messages[i].id, options);
8949
+ };
8283
8950
 
8284
8951
  function __sbReadJson(request) {
8285
8952
  try {