@sproutboat/runtime 0.33.0 → 0.34.1
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 +17 -0
- package/package.json +1 -1
- package/src/entry-crash.test.js +53 -4
- package/src/native-fetch-prelude.js +528 -11
- package/src/source.ts +12 -2
- package/src/transport-embedded.js +639 -0
- package/test/native-unhandled-rejection.ts +133 -0
- package/test/native-websockets-outbound.ts +244 -0
- package/test/native-websockets.ts +86 -3
|
@@ -3789,6 +3789,22 @@ if (typeof globalThis.EventTarget !== "function") {
|
|
|
3789
3789
|
globalThis.EventTarget = EventTarget;
|
|
3790
3790
|
globalThis.AbortSignal = AbortSignal;
|
|
3791
3791
|
globalThis.AbortController = AbortController;
|
|
3792
|
+
// Workers' global event target: `addEventListener("unhandledrejection", fn)` hears every
|
|
3793
|
+
// promise that rejected with nobody waiting on it (see __sbOnUnhandledRejection).
|
|
3794
|
+
class PromiseRejectionEvent extends Event {
|
|
3795
|
+
constructor(type, init) {
|
|
3796
|
+
super(type, init);
|
|
3797
|
+
this.promise = init ? init.promise : undefined;
|
|
3798
|
+
this.reason = init ? init.reason : undefined;
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
const globalTarget = new EventTarget();
|
|
3802
|
+
globalThis.PromiseRejectionEvent = PromiseRejectionEvent;
|
|
3803
|
+
if (typeof globalThis.addEventListener !== "function") {
|
|
3804
|
+
globalThis.addEventListener = (type, listener, options) => globalTarget.addEventListener(type, listener, options);
|
|
3805
|
+
globalThis.removeEventListener = (type, listener, options) => globalTarget.removeEventListener(type, listener, options);
|
|
3806
|
+
globalThis.dispatchEvent = (event) => globalTarget.dispatchEvent(event);
|
|
3807
|
+
}
|
|
3792
3808
|
}
|
|
3793
3809
|
|
|
3794
3810
|
// TextEncoderStream and TextDecoderStream over the Web Streams ponyfill (W2 conformance).
|
|
@@ -4298,6 +4314,10 @@ void (*sb_host_note_hook)(const char* text, size_t length) = 0;
|
|
|
4298
4314
|
// WebSockets (from 2026-10-30): set only by a host that carries client sockets (sproutd's
|
|
4299
4315
|
// native-host-bridge.c, at init); null everywhere else. See __sbWsOp for the ops.
|
|
4300
4316
|
long long (*sb_ws_hook)(int op, unsigned int id, const char* data, size_t length, char* out, size_t cap) = 0;
|
|
4317
|
+
// Outbound WebSockets (from 2026-11-01): set by the embedded transport (a constructor there);
|
|
4318
|
+
// null with the broker transport. Ids with the top bit set belong to it. See __sbWsOp.
|
|
4319
|
+
long long (*sb_wsc_hook)(int op, unsigned int id, const char* data, size_t length, size_t n, char* out, size_t cap) = 0;
|
|
4320
|
+
int (*sb_wsc_wakeable_hook)(void) = 0;
|
|
4301
4321
|
|
|
4302
4322
|
// --- #133: SHA-2 + HMAC ---------------------------------------------------
|
|
4303
4323
|
// Standalone links BearSSL, but a deployed sprout does not, and this inline C
|
|
@@ -6684,6 +6704,7 @@ globalThis.__sbInstallFetch = function () {
|
|
|
6684
6704
|
if (globalThis.__sbCompat >= "2026-10-23") return Promise.reject(error);
|
|
6685
6705
|
throw error;
|
|
6686
6706
|
}
|
|
6707
|
+
if (__sbWsOutboundOn() && __sbWsIsUpgrade(parts)) return __sbWsFetchUpgrade(parts);
|
|
6687
6708
|
const send = (body) => {
|
|
6688
6709
|
const msg = { url: parts.url, method: parts.method, headers: parts.headers, ...body };
|
|
6689
6710
|
return __sbFetchFollowing("fetch", msg, "sproutboat fetch", parts.redirect, parts.signal);
|
|
@@ -7037,13 +7058,17 @@ function __sbGetDOInstance(cls, idText, idObj) {
|
|
|
7037
7058
|
},
|
|
7038
7059
|
};
|
|
7039
7060
|
// The hibernation API, from 2026-10-30 (where WebSocketPair exists).
|
|
7040
|
-
const holder = { inst: null, auto: null };
|
|
7061
|
+
const holder = { inst: null, auto: null, cls, id, restored: [] };
|
|
7041
7062
|
if (__sbWsEnabled()) __sbDOWebSocketState(state, holder);
|
|
7042
7063
|
inst = new Ctor(state, globalThis.env);
|
|
7043
7064
|
holder.inst = inst;
|
|
7044
7065
|
inst.__sbTasks = __sbTasks;
|
|
7045
7066
|
inst.__sbGate = gate;
|
|
7046
7067
|
__sbDOInstances[cacheKey] = inst;
|
|
7068
|
+
// Sockets restored from hibernation start delivering now that the object exists.
|
|
7069
|
+
const restored = holder.restored;
|
|
7070
|
+
holder.restored = [];
|
|
7071
|
+
for (let i = 0; i < restored.length; i++) __sbWsPump(restored[i]);
|
|
7047
7072
|
}
|
|
7048
7073
|
return inst;
|
|
7049
7074
|
}
|
|
@@ -8034,7 +8059,8 @@ function __sbStreamResponse(response) {
|
|
|
8034
8059
|
// client sends arrives as if that end had sent it.
|
|
8035
8060
|
//
|
|
8036
8061
|
// __sbWsOp(op, id, data, n), on the host socket `id`:
|
|
8037
|
-
// "o" open one (returns its id, "0" without a host
|
|
8062
|
+
// "o" open one for the app at compatibility date data (returns its id, "0" without a host or
|
|
8063
|
+
// when the host does not carry sockets at that date), "t" send text data, "b" send the
|
|
8038
8064
|
// bytes in data, "c" close with code n and reason data, "p" poll the next event ("" for
|
|
8039
8065
|
// none, else "KIND LENGTH CODE": 0 text, 1 binary, 2 close, 3 connection lost), "i" take
|
|
8040
8066
|
// that event's payload into the Uint8Array data, "k" acknowledge n bytes dispatched,
|
|
@@ -8051,7 +8077,36 @@ function __sbWsOp(op, id, data, n) {
|
|
|
8051
8077
|
const unsigned int __id = id.type == 1 && id.val > 0 ? (unsigned int)id.val : 0;
|
|
8052
8078
|
const size_t __n = n.type == 1 && n.val > 0 ? (size_t)n.val : 0;
|
|
8053
8079
|
char __r[64]; __r[0] = 0;
|
|
8054
|
-
if (
|
|
8080
|
+
if (__op == 'u' || __op == 'W' || (__id & 0x80000000u)) {
|
|
8081
|
+
long long __w = -1;
|
|
8082
|
+
if (!sb_wsc_hook) snprintf(__r, sizeof(__r), "0");
|
|
8083
|
+
else if (__op == 'W') snprintf(__r, sizeof(__r), "%d", sb_wsc_wakeable_hook && sb_wsc_wakeable_hook() ? 1 : 0);
|
|
8084
|
+
else if (__op == 'p') {
|
|
8085
|
+
sb_wsc_hook('p', __id, 0, 0, 0, __r, sizeof(__r));
|
|
8086
|
+
} else if (__op == 'i') {
|
|
8087
|
+
char* __at = 0; size_t __cap = 0;
|
|
8088
|
+
if (data.type == 81) {
|
|
8089
|
+
const u32 __ta = (u32)data.val;
|
|
8090
|
+
const int32_t __len = *(int32_t*)(MEM + __ta);
|
|
8091
|
+
if (__len > 0) { __at = (char*)(MEM + *(u32*)(MEM + __ta + 4) + 4); __cap = (size_t)__len; }
|
|
8092
|
+
}
|
|
8093
|
+
__w = sb_wsc_hook('i', __id, 0, 0, 0, __at, __cap);
|
|
8094
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8095
|
+
} else if (__op == 'b') {
|
|
8096
|
+
const char* __d = 0; size_t __dl = 0;
|
|
8097
|
+
if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_wsc_hook('b', __id, __d, __dl, 0, 0, 0);
|
|
8098
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8099
|
+
} else if (__op == 't' || __op == 'c' || __op == 'u') {
|
|
8100
|
+
const char* __d; size_t __dl; char* __do = 0;
|
|
8101
|
+
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
8102
|
+
__w = sb_wsc_hook(__op, __id, __d, __dl, __n, 0, 0);
|
|
8103
|
+
if (__do) free(__do);
|
|
8104
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8105
|
+
} else {
|
|
8106
|
+
__w = sb_wsc_hook(__op, __id, 0, 0, __n, 0, 0);
|
|
8107
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8108
|
+
}
|
|
8109
|
+
} else if (sb_ws_hook) {
|
|
8055
8110
|
long long __w = -1;
|
|
8056
8111
|
if (__op == 'p') {
|
|
8057
8112
|
sb_ws_hook('p', __id, 0, 0, __r, sizeof(__r));
|
|
@@ -8068,7 +8123,7 @@ function __sbWsOp(op, id, data, n) {
|
|
|
8068
8123
|
const char* __d = 0; size_t __dl = 0;
|
|
8069
8124
|
if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_ws_hook('b', __id, __d, __dl, 0, 0);
|
|
8070
8125
|
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8071
|
-
} else if (__op == 't' || __op == 'c') {
|
|
8126
|
+
} else if (__op == 't' || __op == 'c' || __op == 'o' || __op == 'a') {
|
|
8072
8127
|
const char* __d; size_t __dl; char* __do = 0;
|
|
8073
8128
|
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
8074
8129
|
__w = sb_ws_hook(__op, __id, __d, __dl, 0, __n);
|
|
@@ -8094,9 +8149,12 @@ const __sbWsKey = {};
|
|
|
8094
8149
|
// addEventListener listeners, as workerd does. Binary messages arrive as a Blob unless
|
|
8095
8150
|
// binaryType is "arraybuffer" (workerd's standard binary type).
|
|
8096
8151
|
class __SbWebSocket {
|
|
8097
|
-
constructor(key) {
|
|
8098
|
-
//
|
|
8099
|
-
|
|
8152
|
+
constructor(key, protocols) {
|
|
8153
|
+
// new WebSocket(url) connects out from compatibility date 2026-11-01; before it only a
|
|
8154
|
+
// WebSocketPair makes a WebSocket.
|
|
8155
|
+
if (key !== __sbWsKey && !__sbWsOutboundOn()) throw new TypeError("WebSocket: only WebSocketPair can make a WebSocket here");
|
|
8156
|
+
this._outbound = false;
|
|
8157
|
+
this._pending = null;
|
|
8100
8158
|
this._peer = null;
|
|
8101
8159
|
this._accepted = false;
|
|
8102
8160
|
this._inbox = [];
|
|
@@ -8114,6 +8172,7 @@ class __SbWebSocket {
|
|
|
8114
8172
|
this.url = null;
|
|
8115
8173
|
this.protocol = "";
|
|
8116
8174
|
this.extensions = "";
|
|
8175
|
+
if (key !== __sbWsKey) __sbWsConnectOutbound(this, key, protocols);
|
|
8117
8176
|
}
|
|
8118
8177
|
get readyState() {
|
|
8119
8178
|
return this._state;
|
|
@@ -8122,13 +8181,14 @@ class __SbWebSocket {
|
|
|
8122
8181
|
return 0;
|
|
8123
8182
|
}
|
|
8124
8183
|
accept() {
|
|
8125
|
-
if (this._host !== 0) throw new TypeError("Can't accept() a WebSocket that was already used in a response.");
|
|
8184
|
+
if (this._host !== 0 && !this._outbound) throw new TypeError("Can't accept() a WebSocket that was already used in a response.");
|
|
8126
8185
|
if (this._hib !== null) throw new TypeError("Can't accept() a WebSocket that was accepted with acceptWebSocket().");
|
|
8127
8186
|
if (this._accepted) return;
|
|
8128
8187
|
this._accepted = true;
|
|
8129
8188
|
__sbWsSchedule(this);
|
|
8130
8189
|
}
|
|
8131
8190
|
send(message) {
|
|
8191
|
+
if (this._outbound) return __sbWsOutSend(this, message);
|
|
8132
8192
|
if (!this._accepted && this._hib === null)
|
|
8133
8193
|
throw new TypeError("You must call one of accept() or state.acceptWebSocket() on this WebSocket before sending messages.");
|
|
8134
8194
|
if (this._closeSent) throw new TypeError("Can't call WebSocket send() after close().");
|
|
@@ -8149,6 +8209,7 @@ class __SbWebSocket {
|
|
|
8149
8209
|
const text = reason === undefined ? "" : String(reason);
|
|
8150
8210
|
if (__sbToBytes(text).length > 123)
|
|
8151
8211
|
throw __sbCryptoError("SyntaxError", "WebSocket close reason must not be longer than 123 bytes when UTF-8 encoded.");
|
|
8212
|
+
if (this._outbound) return __sbWsOutClose(this, code, text);
|
|
8152
8213
|
if (this._closeSent) return;
|
|
8153
8214
|
this._closeSent = true;
|
|
8154
8215
|
this._state = this._closeSeen ? __SB_WS_CLOSED : __SB_WS_CLOSING;
|
|
@@ -8176,6 +8237,7 @@ class __SbWebSocket {
|
|
|
8176
8237
|
}
|
|
8177
8238
|
serializeAttachment(value) {
|
|
8178
8239
|
this._attachment = value === undefined ? undefined : structuredClone(value);
|
|
8240
|
+
if (this._hib !== null && this._hib.hostId > 0) __sbWsHibUpdate(this);
|
|
8179
8241
|
}
|
|
8180
8242
|
deserializeAttachment() {
|
|
8181
8243
|
return this._attachment === undefined ? null : structuredClone(this._attachment);
|
|
@@ -8276,6 +8338,7 @@ function __sbWsDispatch(ws, event) {
|
|
|
8276
8338
|
if (ws._peer !== null) __sbWsDeliver(ws._peer, { kind: 2, code: event.code, reason: event.reason, clean: event.clean });
|
|
8277
8339
|
}
|
|
8278
8340
|
ws._state = __SB_WS_CLOSED;
|
|
8341
|
+
if (ws._hib !== null && ws._hib.hostId > 0) __sbWsHibForget(ws);
|
|
8279
8342
|
if (ws._hib !== null) return __sbWsHibernated(ws, "webSocketClose", [ws, event.code, event.reason, event.clean]);
|
|
8280
8343
|
return __sbWsFire(ws, { type: "close", code: event.code, reason: event.reason, wasClean: event.clean, target: ws });
|
|
8281
8344
|
}
|
|
@@ -8317,13 +8380,14 @@ function __sbDOWebSocketState(state, holder) {
|
|
|
8317
8380
|
for (let i = sockets.length - 1; i >= 0; i--) if (sockets[i]._state === __SB_WS_CLOSED) sockets.splice(i, 1);
|
|
8318
8381
|
return sockets;
|
|
8319
8382
|
};
|
|
8383
|
+
__sbWsHibRestore(holder, sockets);
|
|
8320
8384
|
state.acceptWebSocket = function (ws, tags) {
|
|
8321
8385
|
if (!(ws instanceof __SbWebSocket)) throw new TypeError("acceptWebSocket needs a WebSocket");
|
|
8322
8386
|
if (ws._accepted || ws._hib !== null || ws._host !== 0) throw new TypeError("This WebSocket was already accepted.");
|
|
8323
8387
|
const list = [];
|
|
8324
8388
|
if (tags != null) for (const t of tags) list.push(String(t));
|
|
8325
8389
|
if (list.length > 10) throw new TypeError("A WebSocket can have at most 10 tags.");
|
|
8326
|
-
ws._hib = { holder, tags: list };
|
|
8390
|
+
ws._hib = { holder, tags: list, hostId: 0 };
|
|
8327
8391
|
live().push(ws);
|
|
8328
8392
|
__sbWsSchedule(ws);
|
|
8329
8393
|
};
|
|
@@ -8341,6 +8405,7 @@ function __sbDOWebSocketState(state, holder) {
|
|
|
8341
8405
|
if (pair != null && !(pair instanceof __SbWebSocketRequestResponsePair))
|
|
8342
8406
|
throw new TypeError("setWebSocketAutoResponse needs a WebSocketRequestResponsePair");
|
|
8343
8407
|
holder.auto = pair == null ? null : pair;
|
|
8408
|
+
if (__sbWsHibOn()) __sbWsHibAuto(holder, live());
|
|
8344
8409
|
};
|
|
8345
8410
|
state.getWebSocketAutoResponse = function () {
|
|
8346
8411
|
return holder.auto;
|
|
@@ -8364,13 +8429,17 @@ function __sbHasWebSocket(r) {
|
|
|
8364
8429
|
// The client end of a 101 response becomes a host socket: marked x-sb-websocket for the
|
|
8365
8430
|
// host (which upgrades the connection), with what the app already sent going out after it.
|
|
8366
8431
|
function __sbWsHostResponse(r) {
|
|
8432
|
+
if (r._webSocket instanceof __SbWebSocket && r._webSocket._outbound && !r._webSocket._accepted) __sbWsProxy(r, r._webSocket);
|
|
8367
8433
|
const ws = r._webSocket;
|
|
8368
8434
|
if (!(ws instanceof __SbWebSocket) || ws._host !== 0 || ws._accepted || ws._hib !== null) {
|
|
8369
8435
|
return __sbErrorResponse(new TypeError("A 101 response needs the unaccepted client end of a WebSocketPair."), false);
|
|
8370
8436
|
}
|
|
8371
|
-
|
|
8437
|
+
// The compatibility date goes along: a standalone or platform server carries client sockets
|
|
8438
|
+
// from 2026-11-01 only (sproutd, whose bridge ignores it, from 2026-10-30).
|
|
8439
|
+
const id = Number(__sbWsOp("o", 0, globalThis.__sbCompat, 0));
|
|
8372
8440
|
if (!(id > 0)) return new Response("WebSockets are not supported by this host yet", { status: 501 });
|
|
8373
8441
|
ws._host = id;
|
|
8442
|
+
if (ws._peer !== null && ws._peer._hib !== null && __sbWsHibOn()) __sbWsHibRegister(ws._peer, id);
|
|
8374
8443
|
r.headers._immutable = false;
|
|
8375
8444
|
r.headers.set("x-sb-websocket", String(id));
|
|
8376
8445
|
const held = ws._inbox;
|
|
@@ -8388,7 +8457,8 @@ function __sbWsPump(ws) {
|
|
|
8388
8457
|
const head = __sbWsOp("p", id, "", 0);
|
|
8389
8458
|
if (head === "") {
|
|
8390
8459
|
const timer = setTimeout(() => __sbWsPump(ws), 2147483647);
|
|
8391
|
-
|
|
8460
|
+
// From 2026-11-01 a parked socket is not background work (z): its app may be reaped.
|
|
8461
|
+
__sbWsOp(__sbWsHibOn() ? "z" : "w", id, "", timer);
|
|
8392
8462
|
return;
|
|
8393
8463
|
}
|
|
8394
8464
|
const parts = head.split(" ");
|
|
@@ -8418,6 +8488,432 @@ function __sbWsPump(ws) {
|
|
|
8418
8488
|
}
|
|
8419
8489
|
}
|
|
8420
8490
|
|
|
8491
|
+
// ---------------------------------------------------------------------------
|
|
8492
|
+
// Outbound WebSockets, from compatibility date 2026-11-01: new WebSocket(url, protocols) and
|
|
8493
|
+
// fetch(url, { headers: { Upgrade: "websocket" } }), whose Response carries the connected
|
|
8494
|
+
// socket as response.webSocket (call accept() on it). The transport's C code owns the
|
|
8495
|
+
// connection (sb_wsc_hook, the embedded transport); this end is a host socket with the ops
|
|
8496
|
+
// described at __sbWsOp, ids with the top bit set. Returning that Response from a handler
|
|
8497
|
+
// relays the socket to the client through a pair.
|
|
8498
|
+
function __sbWsOutboundOn() {
|
|
8499
|
+
return globalThis.__sbCompat >= "2026-11-01";
|
|
8500
|
+
}
|
|
8501
|
+
let __sbWsWakeable = -1;
|
|
8502
|
+
function __sbWsCanWake() {
|
|
8503
|
+
if (__sbWsWakeable < 0) __sbWsWakeable = __sbWsOp("W", 0, "", 0) === "1" ? 1 : 0;
|
|
8504
|
+
return __sbWsWakeable === 1;
|
|
8505
|
+
}
|
|
8506
|
+
|
|
8507
|
+
// ws:, wss:, http: and https: URLs, as workerd takes them.
|
|
8508
|
+
function __sbWsTarget(input, what) {
|
|
8509
|
+
let u;
|
|
8510
|
+
try {
|
|
8511
|
+
u = new URL(String(input));
|
|
8512
|
+
} catch {
|
|
8513
|
+
throw new SyntaxError("Failed to construct '" + what + "': The URL '" + String(input) + "' is invalid.");
|
|
8514
|
+
}
|
|
8515
|
+
const scheme = u.protocol;
|
|
8516
|
+
if (scheme !== "ws:" && scheme !== "wss:" && scheme !== "http:" && scheme !== "https:")
|
|
8517
|
+
throw new SyntaxError("Failed to construct '" + what + "': The URL's scheme must be either 'http', 'https', 'ws', or 'wss'. '" + scheme.slice(0, scheme.length - 1) + "' is not allowed.");
|
|
8518
|
+
if (u.hash !== "" && u.hash !== "#") throw new SyntaxError("Failed to construct '" + what + "': The URL contains a fragment identifier ('" + u.hash.slice(1) + "'). Fragment identifiers are not allowed in WebSocket URLs.");
|
|
8519
|
+
const tls = scheme === "wss:" || scheme === "https:";
|
|
8520
|
+
const port = u.port === "" ? (tls ? 443 : 80) : Number(u.port);
|
|
8521
|
+
const path = (u.pathname === "" ? "/" : u.pathname) + u.search;
|
|
8522
|
+
const hostPart = u.hostname + (u.port === "" ? "" : ":" + u.port);
|
|
8523
|
+
return { tls, host: u.hostname, port, path, url: (tls ? "wss://" : "ws://") + hostPart + path };
|
|
8524
|
+
}
|
|
8525
|
+
|
|
8526
|
+
// Opens the connection: the host socket's id, or 0 (outbound sockets not available here, or
|
|
8527
|
+
// too many open).
|
|
8528
|
+
function __sbWsOpen(target, protocols, headers) {
|
|
8529
|
+
let lines = "";
|
|
8530
|
+
if (headers != null) {
|
|
8531
|
+
for (let i = 0; i < headers.length; i++) {
|
|
8532
|
+
const name = String(headers[i][0]).toLowerCase();
|
|
8533
|
+
const value = String(headers[i][1]);
|
|
8534
|
+
if (name === "host" || name === "upgrade" || name === "connection" || name === "content-length" || name === "transfer-encoding") continue;
|
|
8535
|
+
if (name.indexOf("sec-websocket-") === 0) continue;
|
|
8536
|
+
if (name.indexOf("\r") >= 0 || name.indexOf("\n") >= 0 || value.indexOf("\r") >= 0 || value.indexOf("\n") >= 0) continue;
|
|
8537
|
+
lines += name + ": " + value + "\r\n";
|
|
8538
|
+
}
|
|
8539
|
+
}
|
|
8540
|
+
const spec = target.host + "\n" + target.port + "\n" + (target.tls ? "1" : "0") + "\n" + target.path + "\n" + protocols.join(",") + "\n" + lines;
|
|
8541
|
+
return Number(__sbWsOp("u", 0, spec, 0));
|
|
8542
|
+
}
|
|
8543
|
+
|
|
8544
|
+
function __sbWsProtocols(protocols) {
|
|
8545
|
+
if (protocols === undefined) return [];
|
|
8546
|
+
const list = __sbIsStr(protocols) ? [protocols] : Array.from(protocols).map(String);
|
|
8547
|
+
for (let i = 0; i < list.length; i++) {
|
|
8548
|
+
if (list[i] === "" || list[i].indexOf(" ") >= 0 || list[i].indexOf(",") >= 0) throw new SyntaxError("Failed to construct 'WebSocket': The subprotocol '" + list[i] + "' is invalid.");
|
|
8549
|
+
if (list.indexOf(list[i]) !== i) throw new SyntaxError("Failed to construct 'WebSocket': The subprotocol '" + list[i] + "' is duplicated.");
|
|
8550
|
+
}
|
|
8551
|
+
return list;
|
|
8552
|
+
}
|
|
8553
|
+
|
|
8554
|
+
// new WebSocket(url, protocols): CONNECTING until the handshake, then open, as in a browser.
|
|
8555
|
+
function __sbWsConnectOutbound(ws, input, protocols) {
|
|
8556
|
+
const target = __sbWsTarget(input, "WebSocket");
|
|
8557
|
+
const list = __sbWsProtocols(protocols);
|
|
8558
|
+
ws._outbound = true;
|
|
8559
|
+
ws._accepted = true;
|
|
8560
|
+
ws._state = 0;
|
|
8561
|
+
ws.url = target.url;
|
|
8562
|
+
const id = __sbWsOpen(target, list, null);
|
|
8563
|
+
if (!(id > 0)) {
|
|
8564
|
+
ws._state = __SB_WS_CLOSED;
|
|
8565
|
+
setTimeout(() => {
|
|
8566
|
+
__sbWsFire(ws, { type: "error", target: ws });
|
|
8567
|
+
__sbWsFire(ws, { type: "close", code: 1006, reason: "", wasClean: false, target: ws });
|
|
8568
|
+
}, 0);
|
|
8569
|
+
return;
|
|
8570
|
+
}
|
|
8571
|
+
ws._host = id;
|
|
8572
|
+
__sbWsPumpOut(ws);
|
|
8573
|
+
}
|
|
8574
|
+
|
|
8575
|
+
// Takes what the connection received and applies it to ws itself, then waits (a parked timer
|
|
8576
|
+
// the transport fires, or a short poll where nothing can wake the loop) for more.
|
|
8577
|
+
function __sbWsPumpOut(ws) {
|
|
8578
|
+
const id = ws._host;
|
|
8579
|
+
if (id === 0) return;
|
|
8580
|
+
for (;;) {
|
|
8581
|
+
const head = __sbWsOp("p", id, "", 0);
|
|
8582
|
+
if (head === "") {
|
|
8583
|
+
if (__sbWsCanWake()) {
|
|
8584
|
+
const timer = setTimeout(() => __sbWsPumpOut(ws), 2147483647);
|
|
8585
|
+
__sbWsOp("w", id, "", timer);
|
|
8586
|
+
} else setTimeout(() => __sbWsPumpOut(ws), 15);
|
|
8587
|
+
return;
|
|
8588
|
+
}
|
|
8589
|
+
const parts = head.split(" ");
|
|
8590
|
+
const kind = Number(parts[0]);
|
|
8591
|
+
const length = Number(parts[1]);
|
|
8592
|
+
const bytes = new Uint8Array(length);
|
|
8593
|
+
__sbWsOp("i", id, bytes, 0);
|
|
8594
|
+
__sbWsOutEvent(ws, kind, Number(parts[2]), bytes, length);
|
|
8595
|
+
if (ws._host === 0) return;
|
|
8596
|
+
}
|
|
8597
|
+
}
|
|
8598
|
+
|
|
8599
|
+
function __sbWsText(bytes, length) {
|
|
8600
|
+
if (length === 0) return "";
|
|
8601
|
+
let text = __sbFromUtf8Native(bytes);
|
|
8602
|
+
if (text === "") text = new TextDecoder().decode(bytes);
|
|
8603
|
+
return text;
|
|
8604
|
+
}
|
|
8605
|
+
|
|
8606
|
+
// One event from the connection: 0 text, 1 binary, 2 close, 3 lost or never connected,
|
|
8607
|
+
// 4 opened, 5 the server answered the upgrade with something else (its raw head and body).
|
|
8608
|
+
function __sbWsOutEvent(ws, kind, code, bytes, length) {
|
|
8609
|
+
const id = ws._host;
|
|
8610
|
+
const pending = ws._pending;
|
|
8611
|
+
if (kind === 4) {
|
|
8612
|
+
ws._state = __SB_WS_OPEN;
|
|
8613
|
+
ws.protocol = __sbWsText(bytes, length);
|
|
8614
|
+
if (pending) {
|
|
8615
|
+
ws._pending = null;
|
|
8616
|
+
const headers = new Headers();
|
|
8617
|
+
headers.set("upgrade", "websocket");
|
|
8618
|
+
headers.set("connection", "Upgrade");
|
|
8619
|
+
if (ws.protocol !== "") headers.set("sec-websocket-protocol", ws.protocol);
|
|
8620
|
+
pending.resolve(new Response(null, { status: 101, webSocket: ws, headers }));
|
|
8621
|
+
} else __sbWsFire(ws, { type: "open", target: ws });
|
|
8622
|
+
return;
|
|
8623
|
+
}
|
|
8624
|
+
if (kind === 0 || kind === 1) {
|
|
8625
|
+
ws._inbox.push(kind === 1 ? { kind: 1, data: bytes.buffer } : { kind: 0, data: __sbWsText(bytes, length) });
|
|
8626
|
+
if (!ws._flushing) __sbWsFlush(ws);
|
|
8627
|
+
__sbWsOp("k", id, "", length);
|
|
8628
|
+
return;
|
|
8629
|
+
}
|
|
8630
|
+
ws._host = 0;
|
|
8631
|
+
__sbWsOp("r", id, "", 0);
|
|
8632
|
+
if (pending && (kind === 3 || kind === 5)) {
|
|
8633
|
+
ws._pending = null;
|
|
8634
|
+
ws._state = __SB_WS_CLOSED;
|
|
8635
|
+
if (kind === 5) pending.resolve(__sbWsRefusal(code, bytes, length));
|
|
8636
|
+
else pending.reject(new TypeError(__sbWsText(bytes, length) || "WebSocket connection failed"));
|
|
8637
|
+
return;
|
|
8638
|
+
}
|
|
8639
|
+
if (kind === 3) {
|
|
8640
|
+
if (ws._state === 0) __sbWsFire(ws, { type: "error", target: ws });
|
|
8641
|
+
ws._inbox.push({ kind: 2, code: 1006, reason: "", clean: false });
|
|
8642
|
+
} else {
|
|
8643
|
+
ws._inbox.push({ kind: 2, code, reason: __sbWsText(bytes, length), clean: true });
|
|
8644
|
+
}
|
|
8645
|
+
ws._closeSent = true;
|
|
8646
|
+
if (!ws._flushing) __sbWsFlush(ws);
|
|
8647
|
+
if (ws._state !== __SB_WS_CLOSED) ws._state = __SB_WS_CLOSED;
|
|
8648
|
+
}
|
|
8649
|
+
|
|
8650
|
+
// An upgrade the server refused: its answer is the Response.
|
|
8651
|
+
function __sbWsRefusal(status, bytes, length) {
|
|
8652
|
+
let at = 0;
|
|
8653
|
+
while (at + 3 < length && !(bytes[at] === 13 && bytes[at + 1] === 10 && bytes[at + 2] === 13 && bytes[at + 3] === 10)) at++;
|
|
8654
|
+
const head = __sbWsText(bytes.subarray(0, at), at);
|
|
8655
|
+
const headers = new Headers();
|
|
8656
|
+
const lines = head.split("\r\n");
|
|
8657
|
+
for (let i = 1; i < lines.length; i++) {
|
|
8658
|
+
const colon = lines[i].indexOf(":");
|
|
8659
|
+
if (colon > 0) headers.append(lines[i].slice(0, colon).trim(), lines[i].slice(colon + 1).trim());
|
|
8660
|
+
}
|
|
8661
|
+
headers.delete("content-length");
|
|
8662
|
+
headers.delete("transfer-encoding");
|
|
8663
|
+
headers.delete("connection");
|
|
8664
|
+
const body = at + 4 < length ? bytes.slice(at + 4, length) : null;
|
|
8665
|
+
return new Response(status === 204 || status === 304 ? null : body, { status, headers });
|
|
8666
|
+
}
|
|
8667
|
+
|
|
8668
|
+
function __sbWsOutSend(ws, message) {
|
|
8669
|
+
if (ws._state === 0) throw __sbCryptoError("InvalidStateError", "WebSocket is still in CONNECTING state.");
|
|
8670
|
+
if (!ws._accepted) throw new TypeError("You must call accept() on this WebSocket before sending messages.");
|
|
8671
|
+
if (ws._closeSent) throw new TypeError("Can't call WebSocket send() after close().");
|
|
8672
|
+
if (ws._state === __SB_WS_CLOSED || ws._host === 0) return;
|
|
8673
|
+
if (__sbIsStr(message)) __sbWsOp("t", ws._host, message, 0);
|
|
8674
|
+
else if (message instanceof ArrayBuffer) __sbWsOp("b", ws._host, __sbToBytes(new Uint8Array(message)), 0);
|
|
8675
|
+
else if (ArrayBuffer.isView(message)) __sbWsOp("b", ws._host, __sbToBytes(new Uint8Array(message.buffer, message.byteOffset, message.byteLength)), 0);
|
|
8676
|
+
else __sbWsOp("t", ws._host, String(message), 0);
|
|
8677
|
+
}
|
|
8678
|
+
|
|
8679
|
+
function __sbWsOutClose(ws, code, text) {
|
|
8680
|
+
if (ws._closeSent) return;
|
|
8681
|
+
ws._closeSent = true;
|
|
8682
|
+
const id = ws._host;
|
|
8683
|
+
if (id === 0) return;
|
|
8684
|
+
if (ws._state === 0) {
|
|
8685
|
+
// Still connecting: the attempt fails.
|
|
8686
|
+
ws._host = 0;
|
|
8687
|
+
ws._state = __SB_WS_CLOSED;
|
|
8688
|
+
__sbWsOp("r", id, "", 0);
|
|
8689
|
+
const pending = ws._pending;
|
|
8690
|
+
ws._pending = null;
|
|
8691
|
+
if (pending) pending.reject(new TypeError("WebSocket connection was closed before it opened"));
|
|
8692
|
+
else setTimeout(() => {
|
|
8693
|
+
__sbWsFire(ws, { type: "error", target: ws });
|
|
8694
|
+
__sbWsFire(ws, { type: "close", code: 1006, reason: "", wasClean: false, target: ws });
|
|
8695
|
+
}, 0);
|
|
8696
|
+
return;
|
|
8697
|
+
}
|
|
8698
|
+
ws._state = __SB_WS_CLOSING;
|
|
8699
|
+
__sbWsOp("c", id, text, code === undefined ? 1000 : code);
|
|
8700
|
+
// A peer that never answers the close is dropped after a while.
|
|
8701
|
+
setTimeout(() => {
|
|
8702
|
+
if (ws._host !== id) return;
|
|
8703
|
+
ws._host = 0;
|
|
8704
|
+
__sbWsOp("r", id, "", 0);
|
|
8705
|
+
ws._inbox.push({ kind: 2, code: 1006, reason: "", clean: false });
|
|
8706
|
+
if (!ws._flushing) __sbWsFlush(ws);
|
|
8707
|
+
ws._state = __SB_WS_CLOSED;
|
|
8708
|
+
}, 10000);
|
|
8709
|
+
}
|
|
8710
|
+
|
|
8711
|
+
// fetch(url, { headers: { Upgrade: "websocket" } }).
|
|
8712
|
+
function __sbWsIsUpgrade(parts) {
|
|
8713
|
+
if (parts.method !== "GET") return false;
|
|
8714
|
+
for (let i = 0; i < parts.headers.length; i++)
|
|
8715
|
+
if (String(parts.headers[i][0]).toLowerCase() === "upgrade" && String(parts.headers[i][1]).toLowerCase() === "websocket") return true;
|
|
8716
|
+
return false;
|
|
8717
|
+
}
|
|
8718
|
+
function __sbWsFetchUpgrade(parts) {
|
|
8719
|
+
return new Promise((resolve, reject) => {
|
|
8720
|
+
let target;
|
|
8721
|
+
try {
|
|
8722
|
+
target = __sbWsTarget(parts.url, "fetch");
|
|
8723
|
+
} catch (error) {
|
|
8724
|
+
reject(new TypeError(error.message));
|
|
8725
|
+
return;
|
|
8726
|
+
}
|
|
8727
|
+
const protocols = [];
|
|
8728
|
+
for (let i = 0; i < parts.headers.length; i++) {
|
|
8729
|
+
if (String(parts.headers[i][0]).toLowerCase() !== "sec-websocket-protocol") continue;
|
|
8730
|
+
const items = String(parts.headers[i][1]).split(",");
|
|
8731
|
+
for (let j = 0; j < items.length; j++) if (items[j].trim() !== "") protocols.push(items[j].trim());
|
|
8732
|
+
}
|
|
8733
|
+
const ws = new __SbWebSocket(__sbWsKey);
|
|
8734
|
+
ws._outbound = true;
|
|
8735
|
+
ws._state = 0;
|
|
8736
|
+
ws.url = target.url;
|
|
8737
|
+
ws._pending = { resolve, reject };
|
|
8738
|
+
const id = __sbWsOpen(target, protocols, parts.headers);
|
|
8739
|
+
if (!(id > 0)) {
|
|
8740
|
+
reject(new TypeError("Outbound WebSockets are not available here (too many are open, or this transport does not carry them)."));
|
|
8741
|
+
return;
|
|
8742
|
+
}
|
|
8743
|
+
ws._host = id;
|
|
8744
|
+
__sbWsPumpOut(ws);
|
|
8745
|
+
});
|
|
8746
|
+
}
|
|
8747
|
+
|
|
8748
|
+
// A handler returned a fetch() upgrade's Response: the client's socket and the upstream's are
|
|
8749
|
+
// joined through a pair, message for message and close for close.
|
|
8750
|
+
function __sbWsProxy(r, upstream) {
|
|
8751
|
+
const pair = new __SbWebSocketPair();
|
|
8752
|
+
const client = pair[0];
|
|
8753
|
+
const server = pair[1];
|
|
8754
|
+
server.accept();
|
|
8755
|
+
upstream.accept();
|
|
8756
|
+
server.binaryType = "arraybuffer";
|
|
8757
|
+
upstream.binaryType = "arraybuffer";
|
|
8758
|
+
server.addEventListener("message", (e) => {
|
|
8759
|
+
if (upstream._state === __SB_WS_OPEN) upstream.send(e.data);
|
|
8760
|
+
});
|
|
8761
|
+
upstream.addEventListener("message", (e) => {
|
|
8762
|
+
if (server._state === __SB_WS_OPEN) server.send(e.data);
|
|
8763
|
+
});
|
|
8764
|
+
server.addEventListener("close", (e) => {
|
|
8765
|
+
if (upstream._state === __SB_WS_OPEN) upstream.close(e.code === 1005 || e.code === 1006 ? 1000 : e.code, e.reason);
|
|
8766
|
+
});
|
|
8767
|
+
upstream.addEventListener("close", (e) => {
|
|
8768
|
+
if (server._state === __SB_WS_OPEN) server.close(e.code === 1005 || e.code === 1006 ? 1000 : e.code, e.reason);
|
|
8769
|
+
});
|
|
8770
|
+
r._webSocket = client;
|
|
8771
|
+
return r;
|
|
8772
|
+
}
|
|
8773
|
+
|
|
8774
|
+
// ---------------------------------------------------------------------------
|
|
8775
|
+
// Hibernation, from compatibility date 2026-11-01 on a host that keeps client sockets open while
|
|
8776
|
+
// the worker is gone (sproutd; ops "H" "h" "a" "l" "R" "N" of sb_ws_hook). A Durable Object's
|
|
8777
|
+
// hibernatable socket (ctx.acceptWebSocket) whose other end is a host socket is recorded in
|
|
8778
|
+
// storage scoped to the object: its host id, tags, attachment, and the object's auto-response.
|
|
8779
|
+
// The host answers the auto-response itself and may stop the worker while the socket is idle.
|
|
8780
|
+
// When a message or close arrives for a socket the worker no longer knows, the host starts a
|
|
8781
|
+
// worker, tells it which sockets are still open ("l"), and wakes the restore loop ("R", "N"):
|
|
8782
|
+
// the object is constructed again (ctx.getWebSockets() lists its sockets from the start, with
|
|
8783
|
+
// their tags and attachments) and the event is dispatched as webSocketMessage/webSocketClose.
|
|
8784
|
+
let __sbWsHibHost = -1;
|
|
8785
|
+
function __sbWsHibOn() {
|
|
8786
|
+
if (globalThis.__sbCompat < "2026-11-01" || !__sbDOGated()) return false;
|
|
8787
|
+
if (__sbWsHibHost < 0 && __sbWsOp("H", 0, "", 0) === "1") __sbWsHibHost = 1;
|
|
8788
|
+
return __sbWsHibHost === 1;
|
|
8789
|
+
}
|
|
8790
|
+
function __sbWsHibScope(holder) {
|
|
8791
|
+
return "do:" + holder.cls + ":" + holder.id;
|
|
8792
|
+
}
|
|
8793
|
+
function __sbWsHibLoad(holder) {
|
|
8794
|
+
const r = __sbRpc("do.storage.get", { cls: "__sbws", id: __sbWsHibScope(holder), key: "rec" });
|
|
8795
|
+
if (r.found) {
|
|
8796
|
+
try {
|
|
8797
|
+
const rec = JSON.parse(r.value);
|
|
8798
|
+
if (rec && rec.s) return rec;
|
|
8799
|
+
} catch {
|
|
8800
|
+
// A record this runtime cannot read is dropped.
|
|
8801
|
+
}
|
|
8802
|
+
}
|
|
8803
|
+
return { s: {}, auto: null };
|
|
8804
|
+
}
|
|
8805
|
+
function __sbWsHibSave(holder, rec) {
|
|
8806
|
+
__sbRpc("do.storage.put", { cls: "__sbws", id: __sbWsHibScope(holder), key: "rec", value: JSON.stringify(rec) });
|
|
8807
|
+
}
|
|
8808
|
+
function __sbWsHibIndex(id, holder) {
|
|
8809
|
+
if (holder === null) __sbRpc("do.storage.delete", { cls: "__sbws", id: "index", key: "s" + id });
|
|
8810
|
+
else __sbRpc("do.storage.put", { cls: "__sbws", id: "index", key: "s" + id, value: JSON.stringify([holder.cls, holder.id]) });
|
|
8811
|
+
}
|
|
8812
|
+
function __sbWsHibEntry(server) {
|
|
8813
|
+
return { tags: server._hib.tags.slice(), att: server._attachment === undefined ? null : __sbDOEncode(server._attachment) };
|
|
8814
|
+
}
|
|
8815
|
+
|
|
8816
|
+
// The 101 response that carries a hibernatable socket to the host went out: record it.
|
|
8817
|
+
function __sbWsHibRegister(server, id) {
|
|
8818
|
+
const holder = server._hib.holder;
|
|
8819
|
+
server._hib.hostId = id;
|
|
8820
|
+
__sbWsOp("h", id, "", 0);
|
|
8821
|
+
const rec = __sbWsHibLoad(holder);
|
|
8822
|
+
rec.s[String(id)] = __sbWsHibEntry(server);
|
|
8823
|
+
if (holder.auto !== null) {
|
|
8824
|
+
rec.auto = [holder.auto.request, holder.auto.response];
|
|
8825
|
+
__sbWsOp("a", id, holder.auto.request + String.fromCharCode(0) + holder.auto.response, 0);
|
|
8826
|
+
}
|
|
8827
|
+
__sbWsHibSave(holder, rec);
|
|
8828
|
+
__sbWsHibIndex(id, holder);
|
|
8829
|
+
}
|
|
8830
|
+
function __sbWsHibUpdate(server) {
|
|
8831
|
+
const holder = server._hib.holder;
|
|
8832
|
+
const rec = __sbWsHibLoad(holder);
|
|
8833
|
+
rec.s[String(server._hib.hostId)] = __sbWsHibEntry(server);
|
|
8834
|
+
__sbWsHibSave(holder, rec);
|
|
8835
|
+
}
|
|
8836
|
+
function __sbWsHibForget(server) {
|
|
8837
|
+
const id = server._hib.hostId;
|
|
8838
|
+
server._hib.hostId = 0;
|
|
8839
|
+
const holder = server._hib.holder;
|
|
8840
|
+
const rec = __sbWsHibLoad(holder);
|
|
8841
|
+
delete rec.s[String(id)];
|
|
8842
|
+
__sbWsHibSave(holder, rec);
|
|
8843
|
+
__sbWsHibIndex(id, null);
|
|
8844
|
+
}
|
|
8845
|
+
// setWebSocketAutoResponse: the host answers it for every socket of the object.
|
|
8846
|
+
function __sbWsHibAuto(holder, sockets) {
|
|
8847
|
+
const rec = __sbWsHibLoad(holder);
|
|
8848
|
+
rec.auto = holder.auto === null ? null : [holder.auto.request, holder.auto.response];
|
|
8849
|
+
__sbWsHibSave(holder, rec);
|
|
8850
|
+
const text = holder.auto === null ? "" : holder.auto.request + String.fromCharCode(0) + holder.auto.response;
|
|
8851
|
+
for (let i = 0; i < sockets.length; i++) if (sockets[i]._hib.hostId > 0) __sbWsOp("a", sockets[i]._hib.hostId, text, 0);
|
|
8852
|
+
}
|
|
8853
|
+
|
|
8854
|
+
// A new instance of an object: its recorded sockets that the host still holds come back as
|
|
8855
|
+
// hibernatable sockets, before the constructor runs. Their pumps start once the object exists
|
|
8856
|
+
// (holder.restored), so the first events reach webSocketMessage.
|
|
8857
|
+
function __sbWsHibRestore(holder, sockets) {
|
|
8858
|
+
holder.restored = [];
|
|
8859
|
+
if (!__sbWsHibOn()) return;
|
|
8860
|
+
const rec = __sbWsHibLoad(holder);
|
|
8861
|
+
let dirty = false;
|
|
8862
|
+
for (const key in rec.s) {
|
|
8863
|
+
const id = Number(key);
|
|
8864
|
+
if (__sbWsOp("l", id, "", 0) !== "1") {
|
|
8865
|
+
delete rec.s[key];
|
|
8866
|
+
__sbWsHibIndex(id, null);
|
|
8867
|
+
dirty = true;
|
|
8868
|
+
continue;
|
|
8869
|
+
}
|
|
8870
|
+
const pair = new __SbWebSocketPair();
|
|
8871
|
+
const client = pair[0];
|
|
8872
|
+
const server = pair[1];
|
|
8873
|
+
client._host = id;
|
|
8874
|
+
server._hib = { holder, tags: rec.s[key].tags.slice(), hostId: id };
|
|
8875
|
+
server._attachment = rec.s[key].att == null ? undefined : __sbDODecode(rec.s[key].att);
|
|
8876
|
+
sockets.push(server);
|
|
8877
|
+
holder.restored.push(client);
|
|
8878
|
+
}
|
|
8879
|
+
if (rec.auto) holder.auto = new __SbWebSocketRequestResponsePair(rec.auto[0], rec.auto[1]);
|
|
8880
|
+
if (dirty) __sbWsHibSave(holder, rec);
|
|
8881
|
+
}
|
|
8882
|
+
|
|
8883
|
+
// Woken by the host when an event arrives for a socket this worker has not restored.
|
|
8884
|
+
function __sbWsRestoreLoop() {
|
|
8885
|
+
for (;;) {
|
|
8886
|
+
const id = Number(__sbWsOp("N", 0, "", 0));
|
|
8887
|
+
if (!(id > 0)) break;
|
|
8888
|
+
try {
|
|
8889
|
+
__sbWsRestoreSocket(id);
|
|
8890
|
+
} catch (error) {
|
|
8891
|
+
__sbLogUncaught("websocket", error);
|
|
8892
|
+
}
|
|
8893
|
+
}
|
|
8894
|
+
const timer = setTimeout(__sbWsRestoreLoop, 2147483647);
|
|
8895
|
+
__sbWsOp("R", 0, "", timer);
|
|
8896
|
+
}
|
|
8897
|
+
function __sbWsRestoreSocket(id) {
|
|
8898
|
+
const r = __sbRpc("do.storage.get", { cls: "__sbws", id: "index", key: "s" + id });
|
|
8899
|
+
let target = null;
|
|
8900
|
+
if (r.found) {
|
|
8901
|
+
try {
|
|
8902
|
+
target = JSON.parse(r.value);
|
|
8903
|
+
} catch {
|
|
8904
|
+
target = null;
|
|
8905
|
+
}
|
|
8906
|
+
}
|
|
8907
|
+
if (target === null) {
|
|
8908
|
+
// Nothing remembers this socket (another deployment's): close it.
|
|
8909
|
+
__sbWsOp("c", id, "", 1011);
|
|
8910
|
+
__sbWsOp("r", id, "", 0);
|
|
8911
|
+
return;
|
|
8912
|
+
}
|
|
8913
|
+
// Constructing the object restores its sockets (__sbWsHibRestore) and starts their pumps.
|
|
8914
|
+
__sbGetDOInstance(target[0], target[1]);
|
|
8915
|
+
}
|
|
8916
|
+
|
|
8421
8917
|
function __sbInstallWebSockets() {
|
|
8422
8918
|
if (globalThis.__sbCompat < "2026-10-30") return;
|
|
8423
8919
|
globalThis.WebSocketPair = __SbWebSocketPair;
|
|
@@ -8503,6 +8999,21 @@ function __sbLogUncaught(where, error) {
|
|
|
8503
8999
|
}
|
|
8504
9000
|
}
|
|
8505
9001
|
|
|
9002
|
+
// A promise rejected and nothing was waiting on it (an un-awaited `db.prepare(...).run()`
|
|
9003
|
+
// that failed, say). As on Workers the request, the process and every other request go on:
|
|
9004
|
+
// the rejection is logged, and an "unhandledrejection" event fires on the global scope when
|
|
9005
|
+
// something listens. Porffor's promise job loop calls this hook (patchUnhandledRejections);
|
|
9006
|
+
// without it, it throws the reason out of the event loop and the process exits.
|
|
9007
|
+
globalThis.__sbOnUnhandledRejection = function (reason, promise) {
|
|
9008
|
+
__sbLogUncaught("promise", reason);
|
|
9009
|
+
try {
|
|
9010
|
+
if (__sbIsFn(globalThis.PromiseRejectionEvent) && __sbIsFn(globalThis.dispatchEvent))
|
|
9011
|
+
globalThis.dispatchEvent(new globalThis.PromiseRejectionEvent("unhandledrejection", { promise, reason, cancelable: true }));
|
|
9012
|
+
} catch (error) {
|
|
9013
|
+
__sbLogUncaught("unhandledrejection listener", error);
|
|
9014
|
+
}
|
|
9015
|
+
};
|
|
9016
|
+
|
|
8506
9017
|
// Shared by alarm and scheduled: both reply with an empty 204 once their
|
|
8507
9018
|
// handler (and anything it queued via `waitUntil`) has settled.
|
|
8508
9019
|
async function __sb204WithDrain(res, tasks) {
|
|
@@ -8800,6 +9311,12 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
8800
9311
|
|
|
8801
9312
|
// B22: a host that delivers this worker's triggers (sproutd) asks it to deliver the queue
|
|
8802
9313
|
// messages and alarms that are due; the embedded transport answers.
|
|
9314
|
+
// Hibernated WebSockets (2026-11-01): the host woke this worker for one; park the restore timer.
|
|
9315
|
+
if (trigger === "websocket") {
|
|
9316
|
+
if (__sbWsHibOn()) __sbWsRestoreLoop();
|
|
9317
|
+
return new Response(null, { status: 204 });
|
|
9318
|
+
}
|
|
9319
|
+
|
|
8803
9320
|
if (trigger === "due") return __sbRunLocalDue();
|
|
8804
9321
|
|
|
8805
9322
|
return new Response("unknown trigger", { status: 400 });
|