@sproutboat/runtime 0.32.0 → 0.34.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.
- package/CHANGELOG.md +24 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +902 -2
- package/src/source.ts +12 -2
- package/src/transport-embedded.js +639 -0
- package/test/native-websockets-outbound.ts +244 -0
- package/test/native-websockets.ts +261 -0
|
@@ -4295,6 +4295,13 @@ 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;
|
|
4301
|
+
// Outbound WebSockets (from 2026-11-01): set by the embedded transport (a constructor there);
|
|
4302
|
+
// null with the broker transport. Ids with the top bit set belong to it. See __sbWsOp.
|
|
4303
|
+
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;
|
|
4304
|
+
int (*sb_wsc_wakeable_hook)(void) = 0;
|
|
4298
4305
|
|
|
4299
4306
|
// --- #133: SHA-2 + HMAC ---------------------------------------------------
|
|
4300
4307
|
// Standalone links BearSSL, but a deployed sprout does not, and this inline C
|
|
@@ -6671,6 +6678,7 @@ function __sbUpstream(op, msg, label, signal, always) {
|
|
|
6671
6678
|
// prelude must not replace fetch just by being evaluated.
|
|
6672
6679
|
globalThis.__sbInstallFetch = function () {
|
|
6673
6680
|
__sbIngressStreamingSetup();
|
|
6681
|
+
__sbInstallWebSockets();
|
|
6674
6682
|
globalThis.fetch = function (input, init) {
|
|
6675
6683
|
let parts;
|
|
6676
6684
|
try {
|
|
@@ -6680,6 +6688,7 @@ globalThis.__sbInstallFetch = function () {
|
|
|
6680
6688
|
if (globalThis.__sbCompat >= "2026-10-23") return Promise.reject(error);
|
|
6681
6689
|
throw error;
|
|
6682
6690
|
}
|
|
6691
|
+
if (__sbWsOutboundOn() && __sbWsIsUpgrade(parts)) return __sbWsFetchUpgrade(parts);
|
|
6683
6692
|
const send = (body) => {
|
|
6684
6693
|
const msg = { url: parts.url, method: parts.method, headers: parts.headers, ...body };
|
|
6685
6694
|
return __sbFetchFollowing("fetch", msg, "sproutboat fetch", parts.redirect, parts.signal);
|
|
@@ -7032,10 +7041,18 @@ function __sbGetDOInstance(cls, idText, idObj) {
|
|
|
7032
7041
|
if (p && __sbIsFn(p.then)) __sbTasks.push(p);
|
|
7033
7042
|
},
|
|
7034
7043
|
};
|
|
7044
|
+
// The hibernation API, from 2026-10-30 (where WebSocketPair exists).
|
|
7045
|
+
const holder = { inst: null, auto: null, cls, id, restored: [] };
|
|
7046
|
+
if (__sbWsEnabled()) __sbDOWebSocketState(state, holder);
|
|
7035
7047
|
inst = new Ctor(state, globalThis.env);
|
|
7048
|
+
holder.inst = inst;
|
|
7036
7049
|
inst.__sbTasks = __sbTasks;
|
|
7037
7050
|
inst.__sbGate = gate;
|
|
7038
7051
|
__sbDOInstances[cacheKey] = inst;
|
|
7052
|
+
// Sockets restored from hibernation start delivering now that the object exists.
|
|
7053
|
+
const restored = holder.restored;
|
|
7054
|
+
holder.restored = [];
|
|
7055
|
+
for (let i = 0; i < restored.length; i++) __sbWsPump(restored[i]);
|
|
7039
7056
|
}
|
|
7040
7057
|
return inst;
|
|
7041
7058
|
}
|
|
@@ -8014,10 +8031,885 @@ function __sbStreamResponse(response) {
|
|
|
8014
8031
|
return response;
|
|
8015
8032
|
}
|
|
8016
8033
|
|
|
8017
|
-
|
|
8034
|
+
// ---------------------------------------------------------------------------
|
|
8035
|
+
// WebSockets, from compatibility date 2026-10-30, as in workerd: WebSocketPair, accept(),
|
|
8036
|
+
// send(), close(), message and close events, new Response(null, { status: 101, webSocket })
|
|
8037
|
+
// and, for Durable Objects, the hibernation API (__sbDOWebSocketState).
|
|
8038
|
+
//
|
|
8039
|
+
// A pair whose ends both stay in this worker (a Worker and its Durable Object) is plain
|
|
8040
|
+
// JavaScript, so it works on every host. The client end of a 101 response becomes a real
|
|
8041
|
+
// socket only on a host that carries it (sproutd: its bridge sets sb_ws_hook); the host
|
|
8042
|
+
// then stands in for that end: what the app sends to it goes out, and what the remote
|
|
8043
|
+
// client sends arrives as if that end had sent it.
|
|
8044
|
+
//
|
|
8045
|
+
// __sbWsOp(op, id, data, n), on the host socket `id`:
|
|
8046
|
+
// "o" open one for the app at compatibility date data (returns its id, "0" without a host or
|
|
8047
|
+
// when the host does not carry sockets at that date), "t" send text data, "b" send the
|
|
8048
|
+
// bytes in data, "c" close with code n and reason data, "p" poll the next event ("" for
|
|
8049
|
+
// none, else "KIND LENGTH CODE": 0 text, 1 binary, 2 close, 3 connection lost), "i" take
|
|
8050
|
+
// that event's payload into the Uint8Array data, "k" acknowledge n bytes dispatched,
|
|
8051
|
+
// "w" fire timer n when an event arrives, "r" release. Returns a short string.
|
|
8052
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
8053
|
+
function __sbWsOp(op, id, data, n) {
|
|
8054
|
+
let out = "";
|
|
8055
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
8056
|
+
Porffor.c`
|
|
8057
|
+
const char* __o; size_t __ol; char* __oo = 0;
|
|
8058
|
+
porf_native_fetch_read_value(op, &__o, &__ol, &__oo);
|
|
8059
|
+
char __op = __ol ? __o[0] : 0;
|
|
8060
|
+
if (__oo) free(__oo);
|
|
8061
|
+
const unsigned int __id = id.type == 1 && id.val > 0 ? (unsigned int)id.val : 0;
|
|
8062
|
+
const size_t __n = n.type == 1 && n.val > 0 ? (size_t)n.val : 0;
|
|
8063
|
+
char __r[64]; __r[0] = 0;
|
|
8064
|
+
if (__op == 'u' || __op == 'W' || (__id & 0x80000000u)) {
|
|
8065
|
+
long long __w = -1;
|
|
8066
|
+
if (!sb_wsc_hook) snprintf(__r, sizeof(__r), "0");
|
|
8067
|
+
else if (__op == 'W') snprintf(__r, sizeof(__r), "%d", sb_wsc_wakeable_hook && sb_wsc_wakeable_hook() ? 1 : 0);
|
|
8068
|
+
else if (__op == 'p') {
|
|
8069
|
+
sb_wsc_hook('p', __id, 0, 0, 0, __r, sizeof(__r));
|
|
8070
|
+
} else if (__op == 'i') {
|
|
8071
|
+
char* __at = 0; size_t __cap = 0;
|
|
8072
|
+
if (data.type == 81) {
|
|
8073
|
+
const u32 __ta = (u32)data.val;
|
|
8074
|
+
const int32_t __len = *(int32_t*)(MEM + __ta);
|
|
8075
|
+
if (__len > 0) { __at = (char*)(MEM + *(u32*)(MEM + __ta + 4) + 4); __cap = (size_t)__len; }
|
|
8076
|
+
}
|
|
8077
|
+
__w = sb_wsc_hook('i', __id, 0, 0, 0, __at, __cap);
|
|
8078
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8079
|
+
} else if (__op == 'b') {
|
|
8080
|
+
const char* __d = 0; size_t __dl = 0;
|
|
8081
|
+
if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_wsc_hook('b', __id, __d, __dl, 0, 0, 0);
|
|
8082
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8083
|
+
} else if (__op == 't' || __op == 'c' || __op == 'u') {
|
|
8084
|
+
const char* __d; size_t __dl; char* __do = 0;
|
|
8085
|
+
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
8086
|
+
__w = sb_wsc_hook(__op, __id, __d, __dl, __n, 0, 0);
|
|
8087
|
+
if (__do) free(__do);
|
|
8088
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8089
|
+
} else {
|
|
8090
|
+
__w = sb_wsc_hook(__op, __id, 0, 0, __n, 0, 0);
|
|
8091
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8092
|
+
}
|
|
8093
|
+
} else if (sb_ws_hook) {
|
|
8094
|
+
long long __w = -1;
|
|
8095
|
+
if (__op == 'p') {
|
|
8096
|
+
sb_ws_hook('p', __id, 0, 0, __r, sizeof(__r));
|
|
8097
|
+
} else if (__op == 'i') {
|
|
8098
|
+
char* __at = 0; size_t __cap = 0;
|
|
8099
|
+
if (data.type == 81) {
|
|
8100
|
+
const u32 __ta = (u32)data.val;
|
|
8101
|
+
const int32_t __len = *(int32_t*)(MEM + __ta);
|
|
8102
|
+
if (__len > 0) { __at = (char*)(MEM + *(u32*)(MEM + __ta + 4) + 4); __cap = (size_t)__len; }
|
|
8103
|
+
}
|
|
8104
|
+
__w = sb_ws_hook('i', __id, 0, 0, __at, __cap);
|
|
8105
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8106
|
+
} else if (__op == 'b') {
|
|
8107
|
+
const char* __d = 0; size_t __dl = 0;
|
|
8108
|
+
if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_ws_hook('b', __id, __d, __dl, 0, 0);
|
|
8109
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8110
|
+
} else if (__op == 't' || __op == 'c' || __op == 'o' || __op == 'a') {
|
|
8111
|
+
const char* __d; size_t __dl; char* __do = 0;
|
|
8112
|
+
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
8113
|
+
__w = sb_ws_hook(__op, __id, __d, __dl, 0, __n);
|
|
8114
|
+
if (__do) free(__do);
|
|
8115
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8116
|
+
} else {
|
|
8117
|
+
__w = sb_ws_hook(__op, __id, 0, 0, 0, __n);
|
|
8118
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8119
|
+
}
|
|
8120
|
+
} else if (__op == 'o') snprintf(__r, sizeof(__r), "0");
|
|
8121
|
+
out = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
|
|
8122
|
+
`;
|
|
8123
|
+
return out;
|
|
8124
|
+
}
|
|
8125
|
+
|
|
8126
|
+
const __SB_WS_OPEN = 1;
|
|
8127
|
+
const __SB_WS_CLOSING = 2;
|
|
8128
|
+
const __SB_WS_CLOSED = 3;
|
|
8129
|
+
const __sbWsKey = {};
|
|
8130
|
+
|
|
8131
|
+
// One end of a WebSocketPair. Events are plain objects with the fields apps read (type,
|
|
8132
|
+
// data; code, reason, wasClean), dispatched to on<type> (when assigned) and then to the
|
|
8133
|
+
// addEventListener listeners, as workerd does. Binary messages arrive as a Blob unless
|
|
8134
|
+
// binaryType is "arraybuffer" (workerd's standard binary type).
|
|
8135
|
+
class __SbWebSocket {
|
|
8136
|
+
constructor(key, protocols) {
|
|
8137
|
+
// new WebSocket(url) connects out from compatibility date 2026-11-01; before it only a
|
|
8138
|
+
// WebSocketPair makes a WebSocket.
|
|
8139
|
+
if (key !== __sbWsKey && !__sbWsOutboundOn()) throw new TypeError("WebSocket: only WebSocketPair can make a WebSocket here");
|
|
8140
|
+
this._outbound = false;
|
|
8141
|
+
this._pending = null;
|
|
8142
|
+
this._peer = null;
|
|
8143
|
+
this._accepted = false;
|
|
8144
|
+
this._inbox = [];
|
|
8145
|
+
this._flushing = false;
|
|
8146
|
+
this._listeners = {};
|
|
8147
|
+
this._state = __SB_WS_OPEN;
|
|
8148
|
+
this._closeSent = false;
|
|
8149
|
+
this._closeSeen = false;
|
|
8150
|
+
// The host socket this end stands for (the client end of a 101 response), or 0.
|
|
8151
|
+
this._host = 0;
|
|
8152
|
+
// { holder, tags } once a Durable Object took it with ctx.acceptWebSocket().
|
|
8153
|
+
this._hib = null;
|
|
8154
|
+
this._attachment = undefined;
|
|
8155
|
+
this.binaryType = "blob";
|
|
8156
|
+
this.url = null;
|
|
8157
|
+
this.protocol = "";
|
|
8158
|
+
this.extensions = "";
|
|
8159
|
+
if (key !== __sbWsKey) __sbWsConnectOutbound(this, key, protocols);
|
|
8160
|
+
}
|
|
8161
|
+
get readyState() {
|
|
8162
|
+
return this._state;
|
|
8163
|
+
}
|
|
8164
|
+
get bufferedAmount() {
|
|
8165
|
+
return 0;
|
|
8166
|
+
}
|
|
8167
|
+
accept() {
|
|
8168
|
+
if (this._host !== 0 && !this._outbound) throw new TypeError("Can't accept() a WebSocket that was already used in a response.");
|
|
8169
|
+
if (this._hib !== null) throw new TypeError("Can't accept() a WebSocket that was accepted with acceptWebSocket().");
|
|
8170
|
+
if (this._accepted) return;
|
|
8171
|
+
this._accepted = true;
|
|
8172
|
+
__sbWsSchedule(this);
|
|
8173
|
+
}
|
|
8174
|
+
send(message) {
|
|
8175
|
+
if (this._outbound) return __sbWsOutSend(this, message);
|
|
8176
|
+
if (!this._accepted && this._hib === null)
|
|
8177
|
+
throw new TypeError("You must call one of accept() or state.acceptWebSocket() on this WebSocket before sending messages.");
|
|
8178
|
+
if (this._closeSent) throw new TypeError("Can't call WebSocket send() after close().");
|
|
8179
|
+
if (this._state === __SB_WS_CLOSED || this._peer === null) return;
|
|
8180
|
+
let event;
|
|
8181
|
+
if (__sbIsStr(message)) event = { kind: 0, data: message };
|
|
8182
|
+
else if (message instanceof ArrayBuffer) event = { kind: 1, data: message.slice(0) };
|
|
8183
|
+
else if (ArrayBuffer.isView(message))
|
|
8184
|
+
event = { kind: 1, data: new Uint8Array(message.buffer, message.byteOffset, message.byteLength).slice().buffer };
|
|
8185
|
+
else event = { kind: 0, data: String(message) };
|
|
8186
|
+
__sbWsDeliver(this._peer, event);
|
|
8187
|
+
}
|
|
8188
|
+
close(code, reason) {
|
|
8189
|
+
if (code !== undefined) {
|
|
8190
|
+
code = Number(code);
|
|
8191
|
+
if (!(code === 1000 || (code >= 3000 && code <= 4999))) throw __sbCryptoError("InvalidAccessError", "Invalid WebSocket close code: " + code + ".");
|
|
8192
|
+
}
|
|
8193
|
+
const text = reason === undefined ? "" : String(reason);
|
|
8194
|
+
if (__sbToBytes(text).length > 123)
|
|
8195
|
+
throw __sbCryptoError("SyntaxError", "WebSocket close reason must not be longer than 123 bytes when UTF-8 encoded.");
|
|
8196
|
+
if (this._outbound) return __sbWsOutClose(this, code, text);
|
|
8197
|
+
if (this._closeSent) return;
|
|
8198
|
+
this._closeSent = true;
|
|
8199
|
+
this._state = this._closeSeen ? __SB_WS_CLOSED : __SB_WS_CLOSING;
|
|
8200
|
+
if (this._peer !== null) __sbWsDeliver(this._peer, { kind: 2, code: code === undefined ? 1005 : code, reason: text, clean: true });
|
|
8201
|
+
}
|
|
8202
|
+
addEventListener(type, listener) {
|
|
8203
|
+
if (listener == null) return;
|
|
8204
|
+
const list = this._listeners[type] || (this._listeners[type] = []);
|
|
8205
|
+
for (let i = 0; i < list.length; i++) if (list[i] === listener) return;
|
|
8206
|
+
list.push(listener);
|
|
8207
|
+
}
|
|
8208
|
+
removeEventListener(type, listener) {
|
|
8209
|
+
const list = this._listeners[type];
|
|
8210
|
+
if (!list) return;
|
|
8211
|
+
for (let i = 0; i < list.length; i++) {
|
|
8212
|
+
if (list[i] === listener) {
|
|
8213
|
+
list.splice(i, 1);
|
|
8214
|
+
return;
|
|
8215
|
+
}
|
|
8216
|
+
}
|
|
8217
|
+
}
|
|
8218
|
+
dispatchEvent(event) {
|
|
8219
|
+
__sbWsFire(this, event);
|
|
8220
|
+
return true;
|
|
8221
|
+
}
|
|
8222
|
+
serializeAttachment(value) {
|
|
8223
|
+
this._attachment = value === undefined ? undefined : structuredClone(value);
|
|
8224
|
+
if (this._hib !== null && this._hib.hostId > 0) __sbWsHibUpdate(this);
|
|
8225
|
+
}
|
|
8226
|
+
deserializeAttachment() {
|
|
8227
|
+
return this._attachment === undefined ? null : structuredClone(this._attachment);
|
|
8228
|
+
}
|
|
8229
|
+
}
|
|
8230
|
+
__SbWebSocket.CONNECTING = 0;
|
|
8231
|
+
__SbWebSocket.OPEN = __SB_WS_OPEN;
|
|
8232
|
+
__SbWebSocket.CLOSING = __SB_WS_CLOSING;
|
|
8233
|
+
__SbWebSocket.CLOSED = __SB_WS_CLOSED;
|
|
8234
|
+
__SbWebSocket.READY_STATE_CONNECTING = 0;
|
|
8235
|
+
__SbWebSocket.READY_STATE_OPEN = __SB_WS_OPEN;
|
|
8236
|
+
__SbWebSocket.READY_STATE_CLOSING = __SB_WS_CLOSING;
|
|
8237
|
+
__SbWebSocket.READY_STATE_CLOSED = __SB_WS_CLOSED;
|
|
8238
|
+
|
|
8239
|
+
class __SbWebSocketPair {
|
|
8240
|
+
constructor() {
|
|
8241
|
+
const client = new __SbWebSocket(__sbWsKey);
|
|
8242
|
+
const server = new __SbWebSocket(__sbWsKey);
|
|
8243
|
+
client._peer = server;
|
|
8244
|
+
server._peer = client;
|
|
8245
|
+
this[0] = client;
|
|
8246
|
+
this[1] = server;
|
|
8247
|
+
}
|
|
8248
|
+
}
|
|
8249
|
+
|
|
8250
|
+
class __SbWebSocketRequestResponsePair {
|
|
8251
|
+
constructor(request, response) {
|
|
8252
|
+
if (!__sbIsStr(request) || !__sbIsStr(response)) throw new TypeError("WebSocketRequestResponsePair needs two strings");
|
|
8253
|
+
this.request = request;
|
|
8254
|
+
this.response = response;
|
|
8255
|
+
}
|
|
8256
|
+
}
|
|
8257
|
+
|
|
8258
|
+
function __sbWsFire(ws, event) {
|
|
8259
|
+
const handler = ws["on" + event.type];
|
|
8260
|
+
if (__sbIsFn(handler)) {
|
|
8261
|
+
try {
|
|
8262
|
+
handler.call(ws, event);
|
|
8263
|
+
} catch (error) {
|
|
8264
|
+
__sbLogUncaught("websocket", error);
|
|
8265
|
+
}
|
|
8266
|
+
}
|
|
8267
|
+
const list = ws._listeners[event.type];
|
|
8268
|
+
if (!list) return;
|
|
8269
|
+
const copy = list.slice();
|
|
8270
|
+
for (let i = 0; i < copy.length; i++) {
|
|
8271
|
+
const fn = copy[i];
|
|
8272
|
+
try {
|
|
8273
|
+
if (__sbIsFn(fn)) fn.call(ws, event);
|
|
8274
|
+
else if (fn && __sbIsFn(fn.handleEvent)) fn.handleEvent(event);
|
|
8275
|
+
} catch (error) {
|
|
8276
|
+
__sbLogUncaught("websocket", error);
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
}
|
|
8280
|
+
|
|
8281
|
+
// An event arrives at `ws`, sent by its peer: to the host when `ws` is a host socket,
|
|
8282
|
+
// else into ws's inbox, dispatched once it is accepted.
|
|
8283
|
+
function __sbWsDeliver(ws, event) {
|
|
8284
|
+
if (ws._host !== 0) {
|
|
8285
|
+
const id = ws._host;
|
|
8286
|
+
if (event.kind === 0) __sbWsOp("t", id, event.data, 0);
|
|
8287
|
+
else if (event.kind === 1) __sbWsOp("b", id, __sbToBytes(new Uint8Array(event.data)), 0);
|
|
8288
|
+
else if (!ws._closeSeen) {
|
|
8289
|
+
ws._closeSeen = true;
|
|
8290
|
+
__sbWsOp("c", id, event.reason, event.code);
|
|
8291
|
+
}
|
|
8292
|
+
return;
|
|
8293
|
+
}
|
|
8294
|
+
ws._inbox.push(event);
|
|
8295
|
+
__sbWsSchedule(ws);
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
// One event per turn, as workerd's receive loop does: a reply the handler sends reaches the
|
|
8299
|
+
// other end before this end's next message.
|
|
8300
|
+
function __sbWsSchedule(ws) {
|
|
8301
|
+
if (ws._flushing || !ws._inbox.length || (!ws._accepted && ws._hib === null)) return;
|
|
8302
|
+
ws._flushing = true;
|
|
8303
|
+
Promise.resolve().then(() => {
|
|
8304
|
+
ws._flushing = false;
|
|
8305
|
+
if (ws._inbox.length && (ws._accepted || ws._hib !== null)) __sbWsDispatch(ws, ws._inbox.shift());
|
|
8306
|
+
__sbWsSchedule(ws);
|
|
8307
|
+
});
|
|
8308
|
+
}
|
|
8309
|
+
|
|
8310
|
+
function __sbWsFlush(ws) {
|
|
8311
|
+
while (ws._inbox.length && (ws._accepted || ws._hib !== null)) __sbWsDispatch(ws, ws._inbox.shift());
|
|
8312
|
+
}
|
|
8313
|
+
|
|
8314
|
+
// Dispatches one event that arrived at ws: to its listeners, or to its Durable Object's
|
|
8315
|
+
// webSocketMessage / webSocketClose. A close is answered with a close (workerd replies to
|
|
8316
|
+
// a close automatically), and ends the socket.
|
|
8317
|
+
function __sbWsDispatch(ws, event) {
|
|
8318
|
+
if (event.kind === 2) {
|
|
8319
|
+
ws._closeSeen = true;
|
|
8320
|
+
if (!ws._closeSent) {
|
|
8321
|
+
ws._closeSent = true;
|
|
8322
|
+
if (ws._peer !== null) __sbWsDeliver(ws._peer, { kind: 2, code: event.code, reason: event.reason, clean: event.clean });
|
|
8323
|
+
}
|
|
8324
|
+
ws._state = __SB_WS_CLOSED;
|
|
8325
|
+
if (ws._hib !== null && ws._hib.hostId > 0) __sbWsHibForget(ws);
|
|
8326
|
+
if (ws._hib !== null) return __sbWsHibernated(ws, "webSocketClose", [ws, event.code, event.reason, event.clean]);
|
|
8327
|
+
return __sbWsFire(ws, { type: "close", code: event.code, reason: event.reason, wasClean: event.clean, target: ws });
|
|
8328
|
+
}
|
|
8329
|
+
if (ws._hib !== null) {
|
|
8330
|
+
const auto = ws._hib.holder.auto;
|
|
8331
|
+
if (auto !== null && event.kind === 0 && event.data === auto.request) {
|
|
8332
|
+
ws.send(auto.response);
|
|
8333
|
+
return;
|
|
8334
|
+
}
|
|
8335
|
+
return __sbWsHibernated(ws, "webSocketMessage", [ws, event.data]);
|
|
8336
|
+
}
|
|
8337
|
+
const data = event.kind === 1 && ws.binaryType !== "arraybuffer" ? new Blob([event.data]) : event.data;
|
|
8338
|
+
__sbWsFire(ws, { type: "message", data, target: ws });
|
|
8339
|
+
}
|
|
8340
|
+
|
|
8341
|
+
// A hibernatable socket's event runs the object's handler through its input gate.
|
|
8342
|
+
function __sbWsHibernated(ws, name, args) {
|
|
8343
|
+
const inst = ws._hib.holder.inst;
|
|
8344
|
+
if (inst == null || !__sbIsFn(inst[name])) return;
|
|
8345
|
+
const run = () => {
|
|
8346
|
+
if (name === "webSocketClose") return inst.webSocketClose(args[0], args[1], args[2], args[3]);
|
|
8347
|
+
return inst.webSocketMessage(args[0], args[1]);
|
|
8348
|
+
};
|
|
8349
|
+
const done = (p) => {
|
|
8350
|
+
if (p && __sbIsFn(p.then)) p.then(undefined, (error) => __sbLogUncaught("websocket", error));
|
|
8351
|
+
};
|
|
8352
|
+
try {
|
|
8353
|
+
done(__sbDOGated() ? __sbDOEnter(inst.__sbGate, run) : run());
|
|
8354
|
+
} catch (error) {
|
|
8355
|
+
__sbLogUncaught("websocket", error);
|
|
8356
|
+
}
|
|
8357
|
+
}
|
|
8358
|
+
|
|
8359
|
+
// ctx.acceptWebSocket() and the rest of the hibernation API, added to a Durable Object's
|
|
8360
|
+
// state from 2026-10-30. `holder.inst` is the object once it exists.
|
|
8361
|
+
function __sbDOWebSocketState(state, holder) {
|
|
8362
|
+
const sockets = [];
|
|
8363
|
+
const live = () => {
|
|
8364
|
+
for (let i = sockets.length - 1; i >= 0; i--) if (sockets[i]._state === __SB_WS_CLOSED) sockets.splice(i, 1);
|
|
8365
|
+
return sockets;
|
|
8366
|
+
};
|
|
8367
|
+
__sbWsHibRestore(holder, sockets);
|
|
8368
|
+
state.acceptWebSocket = function (ws, tags) {
|
|
8369
|
+
if (!(ws instanceof __SbWebSocket)) throw new TypeError("acceptWebSocket needs a WebSocket");
|
|
8370
|
+
if (ws._accepted || ws._hib !== null || ws._host !== 0) throw new TypeError("This WebSocket was already accepted.");
|
|
8371
|
+
const list = [];
|
|
8372
|
+
if (tags != null) for (const t of tags) list.push(String(t));
|
|
8373
|
+
if (list.length > 10) throw new TypeError("A WebSocket can have at most 10 tags.");
|
|
8374
|
+
ws._hib = { holder, tags: list, hostId: 0 };
|
|
8375
|
+
live().push(ws);
|
|
8376
|
+
__sbWsSchedule(ws);
|
|
8377
|
+
};
|
|
8378
|
+
state.getWebSockets = function (tag) {
|
|
8379
|
+
const out = [];
|
|
8380
|
+
const all = live();
|
|
8381
|
+
for (let i = 0; i < all.length; i++) if (tag === undefined || all[i]._hib.tags.indexOf(String(tag)) >= 0) out.push(all[i]);
|
|
8382
|
+
return out;
|
|
8383
|
+
};
|
|
8384
|
+
state.getTags = function (ws) {
|
|
8385
|
+
if (!(ws instanceof __SbWebSocket) || ws._hib === null) throw new TypeError("getTags: not a WebSocket accepted by this object");
|
|
8386
|
+
return ws._hib.tags.slice();
|
|
8387
|
+
};
|
|
8388
|
+
state.setWebSocketAutoResponse = function (pair) {
|
|
8389
|
+
if (pair != null && !(pair instanceof __SbWebSocketRequestResponsePair))
|
|
8390
|
+
throw new TypeError("setWebSocketAutoResponse needs a WebSocketRequestResponsePair");
|
|
8391
|
+
holder.auto = pair == null ? null : pair;
|
|
8392
|
+
if (__sbWsHibOn()) __sbWsHibAuto(holder, live());
|
|
8393
|
+
};
|
|
8394
|
+
state.getWebSocketAutoResponse = function () {
|
|
8395
|
+
return holder.auto;
|
|
8396
|
+
};
|
|
8397
|
+
state.getWebSocketAutoResponseTimestamp = function () {
|
|
8398
|
+
return null;
|
|
8399
|
+
};
|
|
8400
|
+
state.setHibernatableWebSocketEventTimeout = function () {};
|
|
8401
|
+
state.getHibernatableWebSocketEventTimeout = function () {
|
|
8402
|
+
return null;
|
|
8403
|
+
};
|
|
8404
|
+
}
|
|
8405
|
+
|
|
8406
|
+
function __sbWsEnabled() {
|
|
8407
|
+
return globalThis.WebSocketPair === __SbWebSocketPair;
|
|
8408
|
+
}
|
|
8409
|
+
function __sbHasWebSocket(r) {
|
|
8410
|
+
return r != null && r._webSocket != null && __sbWsEnabled();
|
|
8411
|
+
}
|
|
8412
|
+
|
|
8413
|
+
// The client end of a 101 response becomes a host socket: marked x-sb-websocket for the
|
|
8414
|
+
// host (which upgrades the connection), with what the app already sent going out after it.
|
|
8415
|
+
function __sbWsHostResponse(r) {
|
|
8416
|
+
if (r._webSocket instanceof __SbWebSocket && r._webSocket._outbound && !r._webSocket._accepted) __sbWsProxy(r, r._webSocket);
|
|
8417
|
+
const ws = r._webSocket;
|
|
8418
|
+
if (!(ws instanceof __SbWebSocket) || ws._host !== 0 || ws._accepted || ws._hib !== null) {
|
|
8419
|
+
return __sbErrorResponse(new TypeError("A 101 response needs the unaccepted client end of a WebSocketPair."), false);
|
|
8420
|
+
}
|
|
8421
|
+
// The compatibility date goes along: a standalone or platform server carries client sockets
|
|
8422
|
+
// from 2026-11-01 only (sproutd, whose bridge ignores it, from 2026-10-30).
|
|
8423
|
+
const id = Number(__sbWsOp("o", 0, globalThis.__sbCompat, 0));
|
|
8424
|
+
if (!(id > 0)) return new Response("WebSockets are not supported by this host yet", { status: 501 });
|
|
8425
|
+
ws._host = id;
|
|
8426
|
+
if (ws._peer !== null && ws._peer._hib !== null && __sbWsHibOn()) __sbWsHibRegister(ws._peer, id);
|
|
8427
|
+
r.headers._immutable = false;
|
|
8428
|
+
r.headers.set("x-sb-websocket", String(id));
|
|
8429
|
+
const held = ws._inbox;
|
|
8430
|
+
ws._inbox = [];
|
|
8431
|
+
for (let i = 0; i < held.length; i++) __sbWsDeliver(ws, held[i]);
|
|
8432
|
+
__sbWsPump(ws);
|
|
8433
|
+
return r;
|
|
8434
|
+
}
|
|
8435
|
+
|
|
8436
|
+
// Takes what the host received on ws's socket and dispatches it to the app's end, then
|
|
8437
|
+
// waits (a parked timer the host fires) for more.
|
|
8438
|
+
function __sbWsPump(ws) {
|
|
8439
|
+
const id = ws._host;
|
|
8440
|
+
for (;;) {
|
|
8441
|
+
const head = __sbWsOp("p", id, "", 0);
|
|
8442
|
+
if (head === "") {
|
|
8443
|
+
const timer = setTimeout(() => __sbWsPump(ws), 2147483647);
|
|
8444
|
+
// From 2026-11-01 a parked socket is not background work (z): its app may be reaped.
|
|
8445
|
+
__sbWsOp(__sbWsHibOn() ? "z" : "w", id, "", timer);
|
|
8446
|
+
return;
|
|
8447
|
+
}
|
|
8448
|
+
const parts = head.split(" ");
|
|
8449
|
+
const kind = Number(parts[0]);
|
|
8450
|
+
const length = Number(parts[1]);
|
|
8451
|
+
const bytes = new Uint8Array(length);
|
|
8452
|
+
__sbWsOp("i", id, bytes, 0);
|
|
8453
|
+
let event;
|
|
8454
|
+
if (kind === 1) event = { kind: 1, data: bytes.buffer };
|
|
8455
|
+
else if (kind === 0 || kind === 2) {
|
|
8456
|
+
let text = length > 0 ? __sbFromUtf8Native(bytes) : "";
|
|
8457
|
+
if (text === "" && length > 0) text = new TextDecoder().decode(bytes);
|
|
8458
|
+
event = kind === 0 ? { kind: 0, data: text } : { kind: 2, code: Number(parts[2]), reason: text, clean: true };
|
|
8459
|
+
} else event = { kind: 2, code: 1006, reason: "WebSocket disconnected without sending Close frame.", clean: false };
|
|
8460
|
+
if (event.kind === 2) ws._closeSeen = true;
|
|
8461
|
+
const peer = ws._peer;
|
|
8462
|
+
if (peer !== null) {
|
|
8463
|
+
peer._inbox.push(event);
|
|
8464
|
+
// Dispatched now, so the acknowledgement below means the handler has run.
|
|
8465
|
+
if (!peer._flushing) __sbWsFlush(peer);
|
|
8466
|
+
}
|
|
8467
|
+
__sbWsOp("k", id, "", length);
|
|
8468
|
+
if (event.kind === 2) {
|
|
8469
|
+
__sbWsOp("r", id, "", 0);
|
|
8470
|
+
return;
|
|
8471
|
+
}
|
|
8472
|
+
}
|
|
8473
|
+
}
|
|
8474
|
+
|
|
8475
|
+
// ---------------------------------------------------------------------------
|
|
8476
|
+
// Outbound WebSockets, from compatibility date 2026-11-01: new WebSocket(url, protocols) and
|
|
8477
|
+
// fetch(url, { headers: { Upgrade: "websocket" } }), whose Response carries the connected
|
|
8478
|
+
// socket as response.webSocket (call accept() on it). The transport's C code owns the
|
|
8479
|
+
// connection (sb_wsc_hook, the embedded transport); this end is a host socket with the ops
|
|
8480
|
+
// described at __sbWsOp, ids with the top bit set. Returning that Response from a handler
|
|
8481
|
+
// relays the socket to the client through a pair.
|
|
8482
|
+
function __sbWsOutboundOn() {
|
|
8483
|
+
return globalThis.__sbCompat >= "2026-11-01";
|
|
8484
|
+
}
|
|
8485
|
+
let __sbWsWakeable = -1;
|
|
8486
|
+
function __sbWsCanWake() {
|
|
8487
|
+
if (__sbWsWakeable < 0) __sbWsWakeable = __sbWsOp("W", 0, "", 0) === "1" ? 1 : 0;
|
|
8488
|
+
return __sbWsWakeable === 1;
|
|
8489
|
+
}
|
|
8490
|
+
|
|
8491
|
+
// ws:, wss:, http: and https: URLs, as workerd takes them.
|
|
8492
|
+
function __sbWsTarget(input, what) {
|
|
8493
|
+
let u;
|
|
8494
|
+
try {
|
|
8495
|
+
u = new URL(String(input));
|
|
8496
|
+
} catch {
|
|
8497
|
+
throw new SyntaxError("Failed to construct '" + what + "': The URL '" + String(input) + "' is invalid.");
|
|
8498
|
+
}
|
|
8499
|
+
const scheme = u.protocol;
|
|
8500
|
+
if (scheme !== "ws:" && scheme !== "wss:" && scheme !== "http:" && scheme !== "https:")
|
|
8501
|
+
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.");
|
|
8502
|
+
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.");
|
|
8503
|
+
const tls = scheme === "wss:" || scheme === "https:";
|
|
8504
|
+
const port = u.port === "" ? (tls ? 443 : 80) : Number(u.port);
|
|
8505
|
+
const path = (u.pathname === "" ? "/" : u.pathname) + u.search;
|
|
8506
|
+
const hostPart = u.hostname + (u.port === "" ? "" : ":" + u.port);
|
|
8507
|
+
return { tls, host: u.hostname, port, path, url: (tls ? "wss://" : "ws://") + hostPart + path };
|
|
8508
|
+
}
|
|
8509
|
+
|
|
8510
|
+
// Opens the connection: the host socket's id, or 0 (outbound sockets not available here, or
|
|
8511
|
+
// too many open).
|
|
8512
|
+
function __sbWsOpen(target, protocols, headers) {
|
|
8513
|
+
let lines = "";
|
|
8514
|
+
if (headers != null) {
|
|
8515
|
+
for (let i = 0; i < headers.length; i++) {
|
|
8516
|
+
const name = String(headers[i][0]).toLowerCase();
|
|
8517
|
+
const value = String(headers[i][1]);
|
|
8518
|
+
if (name === "host" || name === "upgrade" || name === "connection" || name === "content-length" || name === "transfer-encoding") continue;
|
|
8519
|
+
if (name.indexOf("sec-websocket-") === 0) continue;
|
|
8520
|
+
if (name.indexOf("\r") >= 0 || name.indexOf("\n") >= 0 || value.indexOf("\r") >= 0 || value.indexOf("\n") >= 0) continue;
|
|
8521
|
+
lines += name + ": " + value + "\r\n";
|
|
8522
|
+
}
|
|
8523
|
+
}
|
|
8524
|
+
const spec = target.host + "\n" + target.port + "\n" + (target.tls ? "1" : "0") + "\n" + target.path + "\n" + protocols.join(",") + "\n" + lines;
|
|
8525
|
+
return Number(__sbWsOp("u", 0, spec, 0));
|
|
8526
|
+
}
|
|
8527
|
+
|
|
8528
|
+
function __sbWsProtocols(protocols) {
|
|
8529
|
+
if (protocols === undefined) return [];
|
|
8530
|
+
const list = __sbIsStr(protocols) ? [protocols] : Array.from(protocols).map(String);
|
|
8531
|
+
for (let i = 0; i < list.length; i++) {
|
|
8532
|
+
if (list[i] === "" || list[i].indexOf(" ") >= 0 || list[i].indexOf(",") >= 0) throw new SyntaxError("Failed to construct 'WebSocket': The subprotocol '" + list[i] + "' is invalid.");
|
|
8533
|
+
if (list.indexOf(list[i]) !== i) throw new SyntaxError("Failed to construct 'WebSocket': The subprotocol '" + list[i] + "' is duplicated.");
|
|
8534
|
+
}
|
|
8535
|
+
return list;
|
|
8536
|
+
}
|
|
8537
|
+
|
|
8538
|
+
// new WebSocket(url, protocols): CONNECTING until the handshake, then open, as in a browser.
|
|
8539
|
+
function __sbWsConnectOutbound(ws, input, protocols) {
|
|
8540
|
+
const target = __sbWsTarget(input, "WebSocket");
|
|
8541
|
+
const list = __sbWsProtocols(protocols);
|
|
8542
|
+
ws._outbound = true;
|
|
8543
|
+
ws._accepted = true;
|
|
8544
|
+
ws._state = 0;
|
|
8545
|
+
ws.url = target.url;
|
|
8546
|
+
const id = __sbWsOpen(target, list, null);
|
|
8547
|
+
if (!(id > 0)) {
|
|
8548
|
+
ws._state = __SB_WS_CLOSED;
|
|
8549
|
+
setTimeout(() => {
|
|
8550
|
+
__sbWsFire(ws, { type: "error", target: ws });
|
|
8551
|
+
__sbWsFire(ws, { type: "close", code: 1006, reason: "", wasClean: false, target: ws });
|
|
8552
|
+
}, 0);
|
|
8553
|
+
return;
|
|
8554
|
+
}
|
|
8555
|
+
ws._host = id;
|
|
8556
|
+
__sbWsPumpOut(ws);
|
|
8557
|
+
}
|
|
8558
|
+
|
|
8559
|
+
// Takes what the connection received and applies it to ws itself, then waits (a parked timer
|
|
8560
|
+
// the transport fires, or a short poll where nothing can wake the loop) for more.
|
|
8561
|
+
function __sbWsPumpOut(ws) {
|
|
8562
|
+
const id = ws._host;
|
|
8563
|
+
if (id === 0) return;
|
|
8564
|
+
for (;;) {
|
|
8565
|
+
const head = __sbWsOp("p", id, "", 0);
|
|
8566
|
+
if (head === "") {
|
|
8567
|
+
if (__sbWsCanWake()) {
|
|
8568
|
+
const timer = setTimeout(() => __sbWsPumpOut(ws), 2147483647);
|
|
8569
|
+
__sbWsOp("w", id, "", timer);
|
|
8570
|
+
} else setTimeout(() => __sbWsPumpOut(ws), 15);
|
|
8571
|
+
return;
|
|
8572
|
+
}
|
|
8573
|
+
const parts = head.split(" ");
|
|
8574
|
+
const kind = Number(parts[0]);
|
|
8575
|
+
const length = Number(parts[1]);
|
|
8576
|
+
const bytes = new Uint8Array(length);
|
|
8577
|
+
__sbWsOp("i", id, bytes, 0);
|
|
8578
|
+
__sbWsOutEvent(ws, kind, Number(parts[2]), bytes, length);
|
|
8579
|
+
if (ws._host === 0) return;
|
|
8580
|
+
}
|
|
8581
|
+
}
|
|
8582
|
+
|
|
8583
|
+
function __sbWsText(bytes, length) {
|
|
8584
|
+
if (length === 0) return "";
|
|
8585
|
+
let text = __sbFromUtf8Native(bytes);
|
|
8586
|
+
if (text === "") text = new TextDecoder().decode(bytes);
|
|
8587
|
+
return text;
|
|
8588
|
+
}
|
|
8589
|
+
|
|
8590
|
+
// One event from the connection: 0 text, 1 binary, 2 close, 3 lost or never connected,
|
|
8591
|
+
// 4 opened, 5 the server answered the upgrade with something else (its raw head and body).
|
|
8592
|
+
function __sbWsOutEvent(ws, kind, code, bytes, length) {
|
|
8593
|
+
const id = ws._host;
|
|
8594
|
+
const pending = ws._pending;
|
|
8595
|
+
if (kind === 4) {
|
|
8596
|
+
ws._state = __SB_WS_OPEN;
|
|
8597
|
+
ws.protocol = __sbWsText(bytes, length);
|
|
8598
|
+
if (pending) {
|
|
8599
|
+
ws._pending = null;
|
|
8600
|
+
const headers = new Headers();
|
|
8601
|
+
headers.set("upgrade", "websocket");
|
|
8602
|
+
headers.set("connection", "Upgrade");
|
|
8603
|
+
if (ws.protocol !== "") headers.set("sec-websocket-protocol", ws.protocol);
|
|
8604
|
+
pending.resolve(new Response(null, { status: 101, webSocket: ws, headers }));
|
|
8605
|
+
} else __sbWsFire(ws, { type: "open", target: ws });
|
|
8606
|
+
return;
|
|
8607
|
+
}
|
|
8608
|
+
if (kind === 0 || kind === 1) {
|
|
8609
|
+
ws._inbox.push(kind === 1 ? { kind: 1, data: bytes.buffer } : { kind: 0, data: __sbWsText(bytes, length) });
|
|
8610
|
+
if (!ws._flushing) __sbWsFlush(ws);
|
|
8611
|
+
__sbWsOp("k", id, "", length);
|
|
8612
|
+
return;
|
|
8613
|
+
}
|
|
8614
|
+
ws._host = 0;
|
|
8615
|
+
__sbWsOp("r", id, "", 0);
|
|
8616
|
+
if (pending && (kind === 3 || kind === 5)) {
|
|
8617
|
+
ws._pending = null;
|
|
8618
|
+
ws._state = __SB_WS_CLOSED;
|
|
8619
|
+
if (kind === 5) pending.resolve(__sbWsRefusal(code, bytes, length));
|
|
8620
|
+
else pending.reject(new TypeError(__sbWsText(bytes, length) || "WebSocket connection failed"));
|
|
8621
|
+
return;
|
|
8622
|
+
}
|
|
8623
|
+
if (kind === 3) {
|
|
8624
|
+
if (ws._state === 0) __sbWsFire(ws, { type: "error", target: ws });
|
|
8625
|
+
ws._inbox.push({ kind: 2, code: 1006, reason: "", clean: false });
|
|
8626
|
+
} else {
|
|
8627
|
+
ws._inbox.push({ kind: 2, code, reason: __sbWsText(bytes, length), clean: true });
|
|
8628
|
+
}
|
|
8629
|
+
ws._closeSent = true;
|
|
8630
|
+
if (!ws._flushing) __sbWsFlush(ws);
|
|
8631
|
+
if (ws._state !== __SB_WS_CLOSED) ws._state = __SB_WS_CLOSED;
|
|
8632
|
+
}
|
|
8633
|
+
|
|
8634
|
+
// An upgrade the server refused: its answer is the Response.
|
|
8635
|
+
function __sbWsRefusal(status, bytes, length) {
|
|
8636
|
+
let at = 0;
|
|
8637
|
+
while (at + 3 < length && !(bytes[at] === 13 && bytes[at + 1] === 10 && bytes[at + 2] === 13 && bytes[at + 3] === 10)) at++;
|
|
8638
|
+
const head = __sbWsText(bytes.subarray(0, at), at);
|
|
8639
|
+
const headers = new Headers();
|
|
8640
|
+
const lines = head.split("\r\n");
|
|
8641
|
+
for (let i = 1; i < lines.length; i++) {
|
|
8642
|
+
const colon = lines[i].indexOf(":");
|
|
8643
|
+
if (colon > 0) headers.append(lines[i].slice(0, colon).trim(), lines[i].slice(colon + 1).trim());
|
|
8644
|
+
}
|
|
8645
|
+
headers.delete("content-length");
|
|
8646
|
+
headers.delete("transfer-encoding");
|
|
8647
|
+
headers.delete("connection");
|
|
8648
|
+
const body = at + 4 < length ? bytes.slice(at + 4, length) : null;
|
|
8649
|
+
return new Response(status === 204 || status === 304 ? null : body, { status, headers });
|
|
8650
|
+
}
|
|
8651
|
+
|
|
8652
|
+
function __sbWsOutSend(ws, message) {
|
|
8653
|
+
if (ws._state === 0) throw __sbCryptoError("InvalidStateError", "WebSocket is still in CONNECTING state.");
|
|
8654
|
+
if (!ws._accepted) throw new TypeError("You must call accept() on this WebSocket before sending messages.");
|
|
8655
|
+
if (ws._closeSent) throw new TypeError("Can't call WebSocket send() after close().");
|
|
8656
|
+
if (ws._state === __SB_WS_CLOSED || ws._host === 0) return;
|
|
8657
|
+
if (__sbIsStr(message)) __sbWsOp("t", ws._host, message, 0);
|
|
8658
|
+
else if (message instanceof ArrayBuffer) __sbWsOp("b", ws._host, __sbToBytes(new Uint8Array(message)), 0);
|
|
8659
|
+
else if (ArrayBuffer.isView(message)) __sbWsOp("b", ws._host, __sbToBytes(new Uint8Array(message.buffer, message.byteOffset, message.byteLength)), 0);
|
|
8660
|
+
else __sbWsOp("t", ws._host, String(message), 0);
|
|
8661
|
+
}
|
|
8662
|
+
|
|
8663
|
+
function __sbWsOutClose(ws, code, text) {
|
|
8664
|
+
if (ws._closeSent) return;
|
|
8665
|
+
ws._closeSent = true;
|
|
8666
|
+
const id = ws._host;
|
|
8667
|
+
if (id === 0) return;
|
|
8668
|
+
if (ws._state === 0) {
|
|
8669
|
+
// Still connecting: the attempt fails.
|
|
8670
|
+
ws._host = 0;
|
|
8671
|
+
ws._state = __SB_WS_CLOSED;
|
|
8672
|
+
__sbWsOp("r", id, "", 0);
|
|
8673
|
+
const pending = ws._pending;
|
|
8674
|
+
ws._pending = null;
|
|
8675
|
+
if (pending) pending.reject(new TypeError("WebSocket connection was closed before it opened"));
|
|
8676
|
+
else setTimeout(() => {
|
|
8677
|
+
__sbWsFire(ws, { type: "error", target: ws });
|
|
8678
|
+
__sbWsFire(ws, { type: "close", code: 1006, reason: "", wasClean: false, target: ws });
|
|
8679
|
+
}, 0);
|
|
8680
|
+
return;
|
|
8681
|
+
}
|
|
8682
|
+
ws._state = __SB_WS_CLOSING;
|
|
8683
|
+
__sbWsOp("c", id, text, code === undefined ? 1000 : code);
|
|
8684
|
+
// A peer that never answers the close is dropped after a while.
|
|
8685
|
+
setTimeout(() => {
|
|
8686
|
+
if (ws._host !== id) return;
|
|
8687
|
+
ws._host = 0;
|
|
8688
|
+
__sbWsOp("r", id, "", 0);
|
|
8689
|
+
ws._inbox.push({ kind: 2, code: 1006, reason: "", clean: false });
|
|
8690
|
+
if (!ws._flushing) __sbWsFlush(ws);
|
|
8691
|
+
ws._state = __SB_WS_CLOSED;
|
|
8692
|
+
}, 10000);
|
|
8693
|
+
}
|
|
8694
|
+
|
|
8695
|
+
// fetch(url, { headers: { Upgrade: "websocket" } }).
|
|
8696
|
+
function __sbWsIsUpgrade(parts) {
|
|
8697
|
+
if (parts.method !== "GET") return false;
|
|
8698
|
+
for (let i = 0; i < parts.headers.length; i++)
|
|
8699
|
+
if (String(parts.headers[i][0]).toLowerCase() === "upgrade" && String(parts.headers[i][1]).toLowerCase() === "websocket") return true;
|
|
8700
|
+
return false;
|
|
8701
|
+
}
|
|
8702
|
+
function __sbWsFetchUpgrade(parts) {
|
|
8703
|
+
return new Promise((resolve, reject) => {
|
|
8704
|
+
let target;
|
|
8705
|
+
try {
|
|
8706
|
+
target = __sbWsTarget(parts.url, "fetch");
|
|
8707
|
+
} catch (error) {
|
|
8708
|
+
reject(new TypeError(error.message));
|
|
8709
|
+
return;
|
|
8710
|
+
}
|
|
8711
|
+
const protocols = [];
|
|
8712
|
+
for (let i = 0; i < parts.headers.length; i++) {
|
|
8713
|
+
if (String(parts.headers[i][0]).toLowerCase() !== "sec-websocket-protocol") continue;
|
|
8714
|
+
const items = String(parts.headers[i][1]).split(",");
|
|
8715
|
+
for (let j = 0; j < items.length; j++) if (items[j].trim() !== "") protocols.push(items[j].trim());
|
|
8716
|
+
}
|
|
8717
|
+
const ws = new __SbWebSocket(__sbWsKey);
|
|
8718
|
+
ws._outbound = true;
|
|
8719
|
+
ws._state = 0;
|
|
8720
|
+
ws.url = target.url;
|
|
8721
|
+
ws._pending = { resolve, reject };
|
|
8722
|
+
const id = __sbWsOpen(target, protocols, parts.headers);
|
|
8723
|
+
if (!(id > 0)) {
|
|
8724
|
+
reject(new TypeError("Outbound WebSockets are not available here (too many are open, or this transport does not carry them)."));
|
|
8725
|
+
return;
|
|
8726
|
+
}
|
|
8727
|
+
ws._host = id;
|
|
8728
|
+
__sbWsPumpOut(ws);
|
|
8729
|
+
});
|
|
8730
|
+
}
|
|
8731
|
+
|
|
8732
|
+
// A handler returned a fetch() upgrade's Response: the client's socket and the upstream's are
|
|
8733
|
+
// joined through a pair, message for message and close for close.
|
|
8734
|
+
function __sbWsProxy(r, upstream) {
|
|
8735
|
+
const pair = new __SbWebSocketPair();
|
|
8736
|
+
const client = pair[0];
|
|
8737
|
+
const server = pair[1];
|
|
8738
|
+
server.accept();
|
|
8739
|
+
upstream.accept();
|
|
8740
|
+
server.binaryType = "arraybuffer";
|
|
8741
|
+
upstream.binaryType = "arraybuffer";
|
|
8742
|
+
server.addEventListener("message", (e) => {
|
|
8743
|
+
if (upstream._state === __SB_WS_OPEN) upstream.send(e.data);
|
|
8744
|
+
});
|
|
8745
|
+
upstream.addEventListener("message", (e) => {
|
|
8746
|
+
if (server._state === __SB_WS_OPEN) server.send(e.data);
|
|
8747
|
+
});
|
|
8748
|
+
server.addEventListener("close", (e) => {
|
|
8749
|
+
if (upstream._state === __SB_WS_OPEN) upstream.close(e.code === 1005 || e.code === 1006 ? 1000 : e.code, e.reason);
|
|
8750
|
+
});
|
|
8751
|
+
upstream.addEventListener("close", (e) => {
|
|
8752
|
+
if (server._state === __SB_WS_OPEN) server.close(e.code === 1005 || e.code === 1006 ? 1000 : e.code, e.reason);
|
|
8753
|
+
});
|
|
8754
|
+
r._webSocket = client;
|
|
8755
|
+
return r;
|
|
8756
|
+
}
|
|
8757
|
+
|
|
8758
|
+
// ---------------------------------------------------------------------------
|
|
8759
|
+
// Hibernation, from compatibility date 2026-11-01 on a host that keeps client sockets open while
|
|
8760
|
+
// the worker is gone (sproutd; ops "H" "h" "a" "l" "R" "N" of sb_ws_hook). A Durable Object's
|
|
8761
|
+
// hibernatable socket (ctx.acceptWebSocket) whose other end is a host socket is recorded in
|
|
8762
|
+
// storage scoped to the object: its host id, tags, attachment, and the object's auto-response.
|
|
8763
|
+
// The host answers the auto-response itself and may stop the worker while the socket is idle.
|
|
8764
|
+
// When a message or close arrives for a socket the worker no longer knows, the host starts a
|
|
8765
|
+
// worker, tells it which sockets are still open ("l"), and wakes the restore loop ("R", "N"):
|
|
8766
|
+
// the object is constructed again (ctx.getWebSockets() lists its sockets from the start, with
|
|
8767
|
+
// their tags and attachments) and the event is dispatched as webSocketMessage/webSocketClose.
|
|
8768
|
+
let __sbWsHibHost = -1;
|
|
8769
|
+
function __sbWsHibOn() {
|
|
8770
|
+
if (globalThis.__sbCompat < "2026-11-01" || !__sbDOGated()) return false;
|
|
8771
|
+
if (__sbWsHibHost < 0 && __sbWsOp("H", 0, "", 0) === "1") __sbWsHibHost = 1;
|
|
8772
|
+
return __sbWsHibHost === 1;
|
|
8773
|
+
}
|
|
8774
|
+
function __sbWsHibScope(holder) {
|
|
8775
|
+
return "do:" + holder.cls + ":" + holder.id;
|
|
8776
|
+
}
|
|
8777
|
+
function __sbWsHibLoad(holder) {
|
|
8778
|
+
const r = __sbRpc("do.storage.get", { cls: "__sbws", id: __sbWsHibScope(holder), key: "rec" });
|
|
8779
|
+
if (r.found) {
|
|
8780
|
+
try {
|
|
8781
|
+
const rec = JSON.parse(r.value);
|
|
8782
|
+
if (rec && rec.s) return rec;
|
|
8783
|
+
} catch {
|
|
8784
|
+
// A record this runtime cannot read is dropped.
|
|
8785
|
+
}
|
|
8786
|
+
}
|
|
8787
|
+
return { s: {}, auto: null };
|
|
8788
|
+
}
|
|
8789
|
+
function __sbWsHibSave(holder, rec) {
|
|
8790
|
+
__sbRpc("do.storage.put", { cls: "__sbws", id: __sbWsHibScope(holder), key: "rec", value: JSON.stringify(rec) });
|
|
8791
|
+
}
|
|
8792
|
+
function __sbWsHibIndex(id, holder) {
|
|
8793
|
+
if (holder === null) __sbRpc("do.storage.delete", { cls: "__sbws", id: "index", key: "s" + id });
|
|
8794
|
+
else __sbRpc("do.storage.put", { cls: "__sbws", id: "index", key: "s" + id, value: JSON.stringify([holder.cls, holder.id]) });
|
|
8795
|
+
}
|
|
8796
|
+
function __sbWsHibEntry(server) {
|
|
8797
|
+
return { tags: server._hib.tags.slice(), att: server._attachment === undefined ? null : __sbDOEncode(server._attachment) };
|
|
8798
|
+
}
|
|
8799
|
+
|
|
8800
|
+
// The 101 response that carries a hibernatable socket to the host went out: record it.
|
|
8801
|
+
function __sbWsHibRegister(server, id) {
|
|
8802
|
+
const holder = server._hib.holder;
|
|
8803
|
+
server._hib.hostId = id;
|
|
8804
|
+
__sbWsOp("h", id, "", 0);
|
|
8805
|
+
const rec = __sbWsHibLoad(holder);
|
|
8806
|
+
rec.s[String(id)] = __sbWsHibEntry(server);
|
|
8807
|
+
if (holder.auto !== null) {
|
|
8808
|
+
rec.auto = [holder.auto.request, holder.auto.response];
|
|
8809
|
+
__sbWsOp("a", id, holder.auto.request + String.fromCharCode(0) + holder.auto.response, 0);
|
|
8810
|
+
}
|
|
8811
|
+
__sbWsHibSave(holder, rec);
|
|
8812
|
+
__sbWsHibIndex(id, holder);
|
|
8813
|
+
}
|
|
8814
|
+
function __sbWsHibUpdate(server) {
|
|
8815
|
+
const holder = server._hib.holder;
|
|
8816
|
+
const rec = __sbWsHibLoad(holder);
|
|
8817
|
+
rec.s[String(server._hib.hostId)] = __sbWsHibEntry(server);
|
|
8818
|
+
__sbWsHibSave(holder, rec);
|
|
8819
|
+
}
|
|
8820
|
+
function __sbWsHibForget(server) {
|
|
8821
|
+
const id = server._hib.hostId;
|
|
8822
|
+
server._hib.hostId = 0;
|
|
8823
|
+
const holder = server._hib.holder;
|
|
8824
|
+
const rec = __sbWsHibLoad(holder);
|
|
8825
|
+
delete rec.s[String(id)];
|
|
8826
|
+
__sbWsHibSave(holder, rec);
|
|
8827
|
+
__sbWsHibIndex(id, null);
|
|
8828
|
+
}
|
|
8829
|
+
// setWebSocketAutoResponse: the host answers it for every socket of the object.
|
|
8830
|
+
function __sbWsHibAuto(holder, sockets) {
|
|
8831
|
+
const rec = __sbWsHibLoad(holder);
|
|
8832
|
+
rec.auto = holder.auto === null ? null : [holder.auto.request, holder.auto.response];
|
|
8833
|
+
__sbWsHibSave(holder, rec);
|
|
8834
|
+
const text = holder.auto === null ? "" : holder.auto.request + String.fromCharCode(0) + holder.auto.response;
|
|
8835
|
+
for (let i = 0; i < sockets.length; i++) if (sockets[i]._hib.hostId > 0) __sbWsOp("a", sockets[i]._hib.hostId, text, 0);
|
|
8836
|
+
}
|
|
8837
|
+
|
|
8838
|
+
// A new instance of an object: its recorded sockets that the host still holds come back as
|
|
8839
|
+
// hibernatable sockets, before the constructor runs. Their pumps start once the object exists
|
|
8840
|
+
// (holder.restored), so the first events reach webSocketMessage.
|
|
8841
|
+
function __sbWsHibRestore(holder, sockets) {
|
|
8842
|
+
holder.restored = [];
|
|
8843
|
+
if (!__sbWsHibOn()) return;
|
|
8844
|
+
const rec = __sbWsHibLoad(holder);
|
|
8845
|
+
let dirty = false;
|
|
8846
|
+
for (const key in rec.s) {
|
|
8847
|
+
const id = Number(key);
|
|
8848
|
+
if (__sbWsOp("l", id, "", 0) !== "1") {
|
|
8849
|
+
delete rec.s[key];
|
|
8850
|
+
__sbWsHibIndex(id, null);
|
|
8851
|
+
dirty = true;
|
|
8852
|
+
continue;
|
|
8853
|
+
}
|
|
8854
|
+
const pair = new __SbWebSocketPair();
|
|
8855
|
+
const client = pair[0];
|
|
8856
|
+
const server = pair[1];
|
|
8857
|
+
client._host = id;
|
|
8858
|
+
server._hib = { holder, tags: rec.s[key].tags.slice(), hostId: id };
|
|
8859
|
+
server._attachment = rec.s[key].att == null ? undefined : __sbDODecode(rec.s[key].att);
|
|
8860
|
+
sockets.push(server);
|
|
8861
|
+
holder.restored.push(client);
|
|
8862
|
+
}
|
|
8863
|
+
if (rec.auto) holder.auto = new __SbWebSocketRequestResponsePair(rec.auto[0], rec.auto[1]);
|
|
8864
|
+
if (dirty) __sbWsHibSave(holder, rec);
|
|
8865
|
+
}
|
|
8866
|
+
|
|
8867
|
+
// Woken by the host when an event arrives for a socket this worker has not restored.
|
|
8868
|
+
function __sbWsRestoreLoop() {
|
|
8869
|
+
for (;;) {
|
|
8870
|
+
const id = Number(__sbWsOp("N", 0, "", 0));
|
|
8871
|
+
if (!(id > 0)) break;
|
|
8872
|
+
try {
|
|
8873
|
+
__sbWsRestoreSocket(id);
|
|
8874
|
+
} catch (error) {
|
|
8875
|
+
__sbLogUncaught("websocket", error);
|
|
8876
|
+
}
|
|
8877
|
+
}
|
|
8878
|
+
const timer = setTimeout(__sbWsRestoreLoop, 2147483647);
|
|
8879
|
+
__sbWsOp("R", 0, "", timer);
|
|
8880
|
+
}
|
|
8881
|
+
function __sbWsRestoreSocket(id) {
|
|
8882
|
+
const r = __sbRpc("do.storage.get", { cls: "__sbws", id: "index", key: "s" + id });
|
|
8883
|
+
let target = null;
|
|
8884
|
+
if (r.found) {
|
|
8885
|
+
try {
|
|
8886
|
+
target = JSON.parse(r.value);
|
|
8887
|
+
} catch {
|
|
8888
|
+
target = null;
|
|
8889
|
+
}
|
|
8890
|
+
}
|
|
8891
|
+
if (target === null) {
|
|
8892
|
+
// Nothing remembers this socket (another deployment's): close it.
|
|
8893
|
+
__sbWsOp("c", id, "", 1011);
|
|
8894
|
+
__sbWsOp("r", id, "", 0);
|
|
8895
|
+
return;
|
|
8896
|
+
}
|
|
8897
|
+
// Constructing the object restores its sockets (__sbWsHibRestore) and starts their pumps.
|
|
8898
|
+
__sbGetDOInstance(target[0], target[1]);
|
|
8899
|
+
}
|
|
8900
|
+
|
|
8901
|
+
function __sbInstallWebSockets() {
|
|
8902
|
+
if (globalThis.__sbCompat < "2026-10-30") return;
|
|
8903
|
+
globalThis.WebSocketPair = __SbWebSocketPair;
|
|
8904
|
+
globalThis.WebSocket = __SbWebSocket;
|
|
8905
|
+
globalThis.WebSocketRequestResponsePair = __SbWebSocketRequestResponsePair;
|
|
8906
|
+
}
|
|
8907
|
+
|
|
8908
|
+
async function __sbFetchWithDrain(res, tasks, bindingCall, rethrow, top) {
|
|
8018
8909
|
let r;
|
|
8019
8910
|
try {
|
|
8020
8911
|
r = res && __sbIsFn(res.then) ? await res : res;
|
|
8912
|
+
if (top && __sbHasWebSocket(r)) r = __sbWsHostResponse(r);
|
|
8021
8913
|
if (__sbHasStreamBody(r)) {
|
|
8022
8914
|
if (__sbStreamingSupported()) __sbStreamResponse(r);
|
|
8023
8915
|
else r.body = await __sbCollectBody(__sbRawBody(r));
|
|
@@ -8314,7 +9206,9 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
8314
9206
|
}
|
|
8315
9207
|
// Same promise-identity rule as above applies to `__sbFetchWithDrain`'s
|
|
8316
9208
|
// own return, which is why it's tail-called rather than chained on here.
|
|
8317
|
-
if (__sbTasks.length || (__res && __sbIsFn(__res.then)) || __sbHasStreamBody(__res)) return __sbFetchWithDrain(__res, __sbTasks, bindingCall);
|
|
9209
|
+
if (__sbTasks.length || (__res && __sbIsFn(__res.then)) || __sbHasStreamBody(__res)) return __sbFetchWithDrain(__res, __sbTasks, bindingCall, false, true);
|
|
9210
|
+
// A WebSocket handshake answer (from 2026-10-30): its client end becomes the host socket.
|
|
9211
|
+
if (__sbHasWebSocket(__res)) return __sbWsHostResponse(__res);
|
|
8318
9212
|
return __sbTagCpu(__res, __t0);
|
|
8319
9213
|
}
|
|
8320
9214
|
if (!__sbTriggerAuthed(request)) return new Response("forbidden", { status: 403 });
|
|
@@ -8386,6 +9280,12 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
8386
9280
|
|
|
8387
9281
|
// B22: a host that delivers this worker's triggers (sproutd) asks it to deliver the queue
|
|
8388
9282
|
// messages and alarms that are due; the embedded transport answers.
|
|
9283
|
+
// Hibernated WebSockets (2026-11-01): the host woke this worker for one; park the restore timer.
|
|
9284
|
+
if (trigger === "websocket") {
|
|
9285
|
+
if (__sbWsHibOn()) __sbWsRestoreLoop();
|
|
9286
|
+
return new Response(null, { status: 204 });
|
|
9287
|
+
}
|
|
9288
|
+
|
|
8389
9289
|
if (trigger === "due") return __sbRunLocalDue();
|
|
8390
9290
|
|
|
8391
9291
|
return new Response("unknown trigger", { status: 400 });
|