@statewalker/webrun-http-browser 0.6.4 → 0.7.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/README.md +319 -65
- package/dist/core/data-calls.d.ts +17 -0
- package/dist/core/data-calls.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +3 -1
- package/dist/core/service-worker-control.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3907 -266
- package/dist/link/errors.d.ts +2 -0
- package/dist/link/errors.d.ts.map +1 -0
- package/dist/link/http-link.d.ts +58 -0
- package/dist/link/http-link.d.ts.map +1 -0
- package/dist/link/index.d.ts +4 -0
- package/dist/link/index.d.ts.map +1 -0
- package/dist/link/locks.d.ts +39 -0
- package/dist/link/locks.d.ts.map +1 -0
- package/dist/link/rendezvous.d.ts +21 -0
- package/dist/link/rendezvous.d.ts.map +1 -0
- package/dist/relay/index-sw.d.ts +12 -2
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +66 -8
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay-sw.js +1160 -55
- package/dist/relay-worker.js +1161 -55
- package/dist/sw/handler-endpoint.d.ts +81 -0
- package/dist/sw/handler-endpoint.d.ts.map +1 -0
- package/dist/sw/handler-table.d.ts +53 -0
- package/dist/sw/handler-table.d.ts.map +1 -0
- package/dist/sw/http-sw-dispatcher.d.ts +162 -21
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/index.d.ts +5 -0
- package/dist/sw/index.d.ts.map +1 -1
- package/dist/sw/protocol.d.ts +115 -0
- package/dist/sw/protocol.d.ts.map +1 -0
- package/dist/sw/sw-dispatcher.d.ts +2 -1
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +199 -0
- package/dist/sw/sw-http-adapter.d.ts.map +1 -0
- package/dist/sw/sw-websocket.d.ts +16 -0
- package/dist/sw/sw-websocket.d.ts.map +1 -0
- package/dist/sw/worker-service.d.ts +45 -0
- package/dist/sw/worker-service.d.ts.map +1 -0
- package/dist/sw/worker-url.d.ts +9 -0
- package/dist/sw/worker-url.d.ts.map +1 -0
- package/dist/sw-worker.js +2075 -210
- package/dist/sw.d.ts +2 -0
- package/dist/sw.d.ts.map +1 -1
- package/dist/sw.js +3853 -480
- package/dist/worker.d.ts +10 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +3507 -0
- package/dist/ws/client.d.ts +73 -0
- package/dist/ws/client.d.ts.map +1 -0
- package/dist/ws/frames.d.ts +68 -0
- package/dist/ws/frames.d.ts.map +1 -0
- package/dist/ws/index.d.ts +6 -0
- package/dist/ws/index.d.ts.map +1 -0
- package/dist/ws/server.d.ts +53 -0
- package/dist/ws/server.d.ts.map +1 -0
- package/dist/ws/socket-core.d.ts +91 -0
- package/dist/ws/socket-core.d.ts.map +1 -0
- package/dist/ws/upgrade.d.ts +93 -0
- package/dist/ws/upgrade.d.ts.map +1 -0
- package/package.json +10 -5
- package/src/core/data-calls.ts +52 -2
- package/src/core/service-worker-control.ts +7 -1
- package/src/index.ts +3 -0
- package/src/link/errors.ts +3 -0
- package/src/link/http-link.ts +222 -0
- package/src/link/index.ts +3 -0
- package/src/link/locks.ts +97 -0
- package/src/link/rendezvous.ts +60 -0
- package/src/relay/index-sw.ts +84 -12
- package/src/relay/index.ts +205 -29
- package/src/sw/handler-endpoint.ts +278 -0
- package/src/sw/handler-table.ts +130 -0
- package/src/sw/http-sw-dispatcher.ts +649 -90
- package/src/sw/index.ts +5 -0
- package/src/sw/protocol.ts +103 -0
- package/src/sw/sw-dispatcher.ts +12 -30
- package/src/sw/sw-http-adapter.ts +613 -0
- package/src/sw/sw-websocket.ts +43 -0
- package/src/sw/worker-service.ts +133 -0
- package/src/sw/worker-url.ts +19 -0
- package/src/sw-worker.ts +8 -3
- package/src/sw.ts +2 -0
- package/src/worker.ts +9 -0
- package/src/ws/client.ts +346 -0
- package/src/ws/frames.ts +119 -0
- package/src/ws/index.ts +11 -0
- package/src/ws/server.ts +132 -0
- package/src/ws/socket-core.ts +251 -0
- package/src/ws/upgrade.ts +200 -0
package/src/ws/server.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { DEFAULT_HEARTBEAT_MS, utf8Length, type WsFrame, type WsTimers } from "./frames.js";
|
|
2
|
+
import {
|
|
3
|
+
CLOSED,
|
|
4
|
+
CLOSING,
|
|
5
|
+
CONNECTING,
|
|
6
|
+
isBinary,
|
|
7
|
+
OPEN,
|
|
8
|
+
PortSocketCore,
|
|
9
|
+
toArrayBuffer,
|
|
10
|
+
} from "./socket-core.js";
|
|
11
|
+
|
|
12
|
+
export interface ServerWebSocketOptions {
|
|
13
|
+
/** The url the client asked for. */
|
|
14
|
+
url?: string;
|
|
15
|
+
/** The subprotocol the server picked, `""` for none. */
|
|
16
|
+
protocol?: string;
|
|
17
|
+
timers?: WsTimers;
|
|
18
|
+
/** How long `close()` waits for the client's echo, in ms. Default 5 s. */
|
|
19
|
+
closeTimeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The server end of a socket over a `MessagePort`: what a handler gets from
|
|
24
|
+
* `upgradeWebSocket` and talks to.
|
|
25
|
+
*
|
|
26
|
+
* - Events are delivered only after `accept()`; frames that arrive earlier are
|
|
27
|
+
* queued. Pings are answered even before `accept()`, so a slow handler does
|
|
28
|
+
* not look like a dead one.
|
|
29
|
+
* - Messages arrive as strings or `ArrayBuffer`s.
|
|
30
|
+
* - `bufferedAmount` counts what the client has not consumed yet, and
|
|
31
|
+
* `whenDrained(limit)` waits for it to fall: the backpressure a producer
|
|
32
|
+
* applies.
|
|
33
|
+
* - When the client's pings stop for three heartbeat periods (a closed tab, a
|
|
34
|
+
* terminated worker), the socket closes with 1006.
|
|
35
|
+
*/
|
|
36
|
+
export class ServerWebSocket extends PortSocketCore {
|
|
37
|
+
static readonly CONNECTING = CONNECTING;
|
|
38
|
+
static readonly OPEN = OPEN;
|
|
39
|
+
static readonly CLOSING = CLOSING;
|
|
40
|
+
static readonly CLOSED = CLOSED;
|
|
41
|
+
|
|
42
|
+
readonly url: string;
|
|
43
|
+
readonly protocol: string;
|
|
44
|
+
private _accepted = false;
|
|
45
|
+
private _queue: Array<Extract<WsFrame, { t: "m" | "c" }>> = [];
|
|
46
|
+
private _heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
47
|
+
private _lastPing = Date.now();
|
|
48
|
+
private _watch: unknown;
|
|
49
|
+
|
|
50
|
+
constructor(port: MessagePort, options: ServerWebSocketOptions = {}) {
|
|
51
|
+
super({ timers: options.timers, closeTimeoutMs: options.closeTimeoutMs });
|
|
52
|
+
this.url = options.url ?? "";
|
|
53
|
+
this.protocol = options.protocol ?? "";
|
|
54
|
+
this._attach(port);
|
|
55
|
+
this._state = OPEN;
|
|
56
|
+
this._watchHeartbeat();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether `accept()` was called. */
|
|
60
|
+
get accepted(): boolean {
|
|
61
|
+
return this._accepted;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Starts delivering events, the queued ones first. */
|
|
65
|
+
accept(): void {
|
|
66
|
+
if (this._accepted) return;
|
|
67
|
+
this._accepted = true;
|
|
68
|
+
for (const frame of this._queue.splice(0)) super._deliver(frame);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
send(data: string | ArrayBuffer | ArrayBufferView): void {
|
|
72
|
+
if (!this._accepted) {
|
|
73
|
+
throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
|
|
74
|
+
}
|
|
75
|
+
this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
close(code = 1000, reason = ""): void {
|
|
79
|
+
if (!isSendableCloseCode(code)) {
|
|
80
|
+
throw new DOMException(
|
|
81
|
+
`${code} is not a close code an endpoint may send.`,
|
|
82
|
+
"InvalidAccessError",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (utf8Length(reason) > 123) {
|
|
86
|
+
throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
|
|
87
|
+
}
|
|
88
|
+
this._startClose(code, reason);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
protected override _deliver(frame: Extract<WsFrame, { t: "m" | "c" }>): void {
|
|
92
|
+
if (!this._accepted) {
|
|
93
|
+
this._queue.push(frame);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
super._deliver(frame);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected _dispatchMessage(data: string | ArrayBuffer): void {
|
|
100
|
+
this.dispatchEvent(new MessageEvent("message", { data }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected override _onPing(hb: number): void {
|
|
104
|
+
this._post({ t: "q" });
|
|
105
|
+
this._lastPing = Date.now();
|
|
106
|
+
if (hb !== this._heartbeatMs) {
|
|
107
|
+
this._heartbeatMs = hb;
|
|
108
|
+
this._watchHeartbeat();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
protected override _onClosed(): void {
|
|
113
|
+
if (this._watch !== undefined) this._timers.clearInterval(this._watch);
|
|
114
|
+
this._watch = undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _watchHeartbeat(): void {
|
|
118
|
+
if (this._watch !== undefined) this._timers.clearInterval(this._watch);
|
|
119
|
+
this._watch = this._timers.setInterval(() => {
|
|
120
|
+
if (Date.now() - this._lastPing > 3 * this._heartbeatMs) {
|
|
121
|
+
this._finish(1006, "", false, true);
|
|
122
|
+
}
|
|
123
|
+
}, this._heartbeatMs);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Codes an endpoint may put in a close frame (RFC 6455 §7.4). */
|
|
128
|
+
function isSendableCloseCode(code: number): boolean {
|
|
129
|
+
if (!Number.isInteger(code)) return false;
|
|
130
|
+
if (code >= 3000 && code <= 4999) return true;
|
|
131
|
+
return code >= 1000 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
|
|
132
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CLOSE_TIMEOUT_MS,
|
|
3
|
+
defaultTimers,
|
|
4
|
+
messageSize,
|
|
5
|
+
postFrame,
|
|
6
|
+
readFrame,
|
|
7
|
+
type WsFrame,
|
|
8
|
+
type WsTimers,
|
|
9
|
+
} from "./frames.js";
|
|
10
|
+
|
|
11
|
+
export const CONNECTING = 0;
|
|
12
|
+
export const OPEN = 1;
|
|
13
|
+
export const CLOSING = 2;
|
|
14
|
+
export const CLOSED = 3;
|
|
15
|
+
|
|
16
|
+
export interface WsCloseEventInit extends EventInit {
|
|
17
|
+
code?: number;
|
|
18
|
+
reason?: string;
|
|
19
|
+
wasClean?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A `CloseEvent` for runtimes that have none: Node 22 has `Event` and
|
|
24
|
+
* `MessageEvent` as globals, but not `CloseEvent`.
|
|
25
|
+
*/
|
|
26
|
+
export class WsCloseEvent extends Event {
|
|
27
|
+
readonly code: number;
|
|
28
|
+
readonly reason: string;
|
|
29
|
+
readonly wasClean: boolean;
|
|
30
|
+
constructor(type: string, init: WsCloseEventInit = {}) {
|
|
31
|
+
super(type, init);
|
|
32
|
+
this.code = init.code ?? 0;
|
|
33
|
+
this.reason = init.reason ?? "";
|
|
34
|
+
this.wasClean = init.wasClean ?? false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A `close` event: the platform's `CloseEvent` where there is one. */
|
|
39
|
+
export function newCloseEvent(init: WsCloseEventInit): Event {
|
|
40
|
+
const Native = (globalThis as unknown as { CloseEvent?: typeof WsCloseEvent }).CloseEvent;
|
|
41
|
+
return Native ? new Native("close", init) : new WsCloseEvent("close", init);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PortSocketOptions {
|
|
45
|
+
timers?: WsTimers;
|
|
46
|
+
/** How long to wait for the echo of a close frame before giving up with 1006. */
|
|
47
|
+
closeTimeoutMs?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* What both ends of a socket over a `MessagePort` share: the frames, the close
|
|
52
|
+
* handshake, and the byte accounting behind `bufferedAmount`.
|
|
53
|
+
*
|
|
54
|
+
* Not exported from the package: the two ends are `createWebSocketClass`'s
|
|
55
|
+
* class and `ServerWebSocket`.
|
|
56
|
+
*/
|
|
57
|
+
export abstract class PortSocketCore extends EventTarget {
|
|
58
|
+
protected _port?: MessagePort;
|
|
59
|
+
protected _state: number = CONNECTING;
|
|
60
|
+
protected readonly _timers: WsTimers;
|
|
61
|
+
protected readonly _closeTimeoutMs: number;
|
|
62
|
+
/** Bytes sent and not yet acknowledged by the peer, plus those still being prepared. */
|
|
63
|
+
private _inFlight = 0;
|
|
64
|
+
/** Bytes of messages "sent" after the socket started closing: counted, never sent. */
|
|
65
|
+
private _discarded = 0;
|
|
66
|
+
private _toAck = 0;
|
|
67
|
+
private _ackScheduled = false;
|
|
68
|
+
private _closeTimer: unknown;
|
|
69
|
+
private _drainWaiters: Array<{ limit: number; resolve: () => void }> = [];
|
|
70
|
+
|
|
71
|
+
constructor(options: PortSocketOptions = {}) {
|
|
72
|
+
super();
|
|
73
|
+
this._timers = options.timers ?? defaultTimers;
|
|
74
|
+
this._closeTimeoutMs = options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get readyState(): number {
|
|
78
|
+
return this._state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Bytes queued by `send()` that the peer has not consumed yet. A peer
|
|
83
|
+
* acknowledges what it has dispatched, so this falls as the other side reads.
|
|
84
|
+
*/
|
|
85
|
+
get bufferedAmount(): number {
|
|
86
|
+
return this._inFlight + this._discarded;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves once `bufferedAmount` is at most `limit` (default 0), or the
|
|
91
|
+
* socket is closed. The backpressure a producer applies:
|
|
92
|
+
* `if (socket.bufferedAmount > HIGH) await socket.whenDrained(LOW)`.
|
|
93
|
+
*/
|
|
94
|
+
whenDrained(limit = 0): Promise<void> {
|
|
95
|
+
if (this._state === CLOSED || this._inFlight <= limit) return Promise.resolve();
|
|
96
|
+
return new Promise((resolve) => this._drainWaiters.push({ limit, resolve }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected _attach(port: MessagePort): void {
|
|
100
|
+
this._port = port;
|
|
101
|
+
port.onmessage = (event) => {
|
|
102
|
+
const frame = readFrame(event.data);
|
|
103
|
+
if (frame) this._onFrame(frame);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected _onFrame(frame: WsFrame): void {
|
|
108
|
+
switch (frame.t) {
|
|
109
|
+
case "m":
|
|
110
|
+
case "c":
|
|
111
|
+
this._deliver(frame);
|
|
112
|
+
return;
|
|
113
|
+
case "a":
|
|
114
|
+
this._inFlight = Math.max(0, this._inFlight - frame.n);
|
|
115
|
+
this._wakeDrainWaiters();
|
|
116
|
+
return;
|
|
117
|
+
case "p":
|
|
118
|
+
this._onPing(frame.hb);
|
|
119
|
+
return;
|
|
120
|
+
case "q":
|
|
121
|
+
this._onPong();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A message or close frame the socket is ready to act on. */
|
|
127
|
+
protected _deliver(frame: Extract<WsFrame, { t: "m" | "c" }>): void {
|
|
128
|
+
if (frame.t === "c") {
|
|
129
|
+
this._onCloseFrame(frame.code, frame.reason);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (this._state !== OPEN) return;
|
|
133
|
+
this._dispatchMessage(frame.d);
|
|
134
|
+
this._consumed(messageSize(frame.d));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
protected abstract _dispatchMessage(data: string | ArrayBuffer): void;
|
|
138
|
+
protected _onPing(_hb: number): void {}
|
|
139
|
+
protected _onPong(): void {}
|
|
140
|
+
protected _onClosed(): void {}
|
|
141
|
+
|
|
142
|
+
/** Acknowledge consumed bytes, coalescing the acks of one turn into one frame. */
|
|
143
|
+
protected _consumed(bytes: number): void {
|
|
144
|
+
this._toAck += bytes;
|
|
145
|
+
if (this._ackScheduled) return;
|
|
146
|
+
this._ackScheduled = true;
|
|
147
|
+
queueMicrotask(() => {
|
|
148
|
+
this._ackScheduled = false;
|
|
149
|
+
const n = this._toAck;
|
|
150
|
+
this._toAck = 0;
|
|
151
|
+
if (n > 0 && this._state !== CLOSED) this._post({ t: "a", n });
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Reserves `bytes` in `bufferedAmount` for a message still being prepared (a Blob). */
|
|
156
|
+
protected _reserve(bytes: number): void {
|
|
157
|
+
this._inFlight += bytes;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Sends one message whose bytes were reserved with `_reserve` (or `reserved = false`). */
|
|
161
|
+
protected _sendData(data: string | ArrayBuffer, reserved = false): void {
|
|
162
|
+
const size = messageSize(data);
|
|
163
|
+
if (this._state !== OPEN) {
|
|
164
|
+
if (reserved) this._inFlight = Math.max(0, this._inFlight - size);
|
|
165
|
+
this._discarded += size;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!reserved) this._inFlight += size;
|
|
169
|
+
this._post({ t: "m", d: data });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
protected _post(frame: WsFrame): void {
|
|
173
|
+
try {
|
|
174
|
+
if (this._port) postFrame(this._port, frame);
|
|
175
|
+
} catch {
|
|
176
|
+
// The port is gone; the heartbeat notices.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Starts the close handshake. */
|
|
181
|
+
protected _startClose(code: number, reason: string): void {
|
|
182
|
+
if (this._state === CLOSING || this._state === CLOSED) return;
|
|
183
|
+
this._state = CLOSING;
|
|
184
|
+
this._post({ t: "c", code, reason });
|
|
185
|
+
this._closeTimer = this._timers.setTimeout(
|
|
186
|
+
() => this._finish(1006, "", false),
|
|
187
|
+
this._closeTimeoutMs,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
protected _onCloseFrame(code: number, reason: string): void {
|
|
192
|
+
if (this._state === CLOSED) return;
|
|
193
|
+
if (this._state === CLOSING) {
|
|
194
|
+
// The echo of our own close.
|
|
195
|
+
this._finish(code, reason, true);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this._state = CLOSING;
|
|
199
|
+
this._post({ t: "c", code, reason });
|
|
200
|
+
this._finish(code, reason, true);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The socket is closed: release the port and tell the listeners. */
|
|
204
|
+
protected _finish(code: number, reason: string, wasClean: boolean, error = false): void {
|
|
205
|
+
if (this._state === CLOSED) return;
|
|
206
|
+
this._state = CLOSED;
|
|
207
|
+
if (this._closeTimer !== undefined) this._timers.clearTimeout(this._closeTimer);
|
|
208
|
+
this._closeTimer = undefined;
|
|
209
|
+
this._onClosed();
|
|
210
|
+
if (this._port) {
|
|
211
|
+
this._port.onmessage = null;
|
|
212
|
+
try {
|
|
213
|
+
this._port.close();
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
this._wakeDrainWaiters();
|
|
217
|
+
if (error) this.dispatchEvent(new Event("error"));
|
|
218
|
+
this.dispatchEvent(newCloseEvent({ code, reason, wasClean }));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private _wakeDrainWaiters(): void {
|
|
222
|
+
if (this._drainWaiters.length === 0) return;
|
|
223
|
+
const closed = this._state === CLOSED;
|
|
224
|
+
this._drainWaiters = this._drainWaiters.filter(({ limit, resolve }) => {
|
|
225
|
+
if (!closed && this._inFlight > limit) return true;
|
|
226
|
+
resolve();
|
|
227
|
+
return false;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Copies `data` into a fresh `ArrayBuffer`: `send()` must never detach the caller's buffer. */
|
|
233
|
+
export function toArrayBuffer(data: ArrayBuffer | ArrayBufferView): ArrayBuffer {
|
|
234
|
+
if (data instanceof ArrayBuffer) return data.slice(0);
|
|
235
|
+
const copy = new Uint8Array(data.byteLength);
|
|
236
|
+
copy.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
237
|
+
return copy.buffer;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function isBinary(data: unknown): data is ArrayBuffer | ArrayBufferView {
|
|
241
|
+
return data instanceof ArrayBuffer || ArrayBuffer.isView(data);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function isBlob(data: unknown): data is Blob {
|
|
245
|
+
return typeof Blob !== "undefined" && data instanceof Blob;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Valid close codes for an application to send: 1000, or 3000–4999. */
|
|
249
|
+
export function isApplicationCloseCode(code: number): boolean {
|
|
250
|
+
return code === 1000 || (code >= 3000 && code <= 4999);
|
|
251
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { errorMessage } from "../link/errors.js";
|
|
2
|
+
import { WS_PORT_PROTOCOL_VERSION } from "./frames.js";
|
|
3
|
+
import { ServerWebSocket, type ServerWebSocketOptions } from "./server.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The upgrade, as an HTTP exchange: a `GET` with `Upgrade: websocket` goes to
|
|
7
|
+
* an ordinary `(Request) => Response` handler, and a `101` answer carrying a
|
|
8
|
+
* server socket means accepted. Anything else is a refusal, with its status.
|
|
9
|
+
*
|
|
10
|
+
* FORBIDDEN HEADERS. A browser drops `Upgrade`, `Connection` and every
|
|
11
|
+
* `Sec-*` header from a `new Request()`. The upgrade request therefore carries
|
|
12
|
+
* its headers as an own `headers` property, a standalone `Headers` that keeps
|
|
13
|
+
* them, so handler code reads `request.headers.get("upgrade")` as usual.
|
|
14
|
+
*
|
|
15
|
+
* NO 101 IN A `Response`. The constructor refuses statuses below 200, so the
|
|
16
|
+
* response `upgradeWebSocket` returns reports 101 through an own `status`
|
|
17
|
+
* property; the port that goes back to the caller is found with `takePort`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The request header carrying the client's version of the frame protocol. */
|
|
21
|
+
export const WS_VERSION_HEADER = "x-webrun-ws-version";
|
|
22
|
+
|
|
23
|
+
export interface WebSocketUpgradeInit {
|
|
24
|
+
/** Requested subprotocols, in order of preference. */
|
|
25
|
+
protocols?: string[];
|
|
26
|
+
/** More headers for the handler to see (cookies, origin, authorization...). */
|
|
27
|
+
headers?: HeadersInit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The `GET` a handler sees for `new WebSocket(url, protocols)`. A `ws:` or
|
|
32
|
+
* `wss:` url is given as `http:` or `https:`, as a server sees it.
|
|
33
|
+
*/
|
|
34
|
+
export function newWebSocketUpgradeRequest(
|
|
35
|
+
url: string | URL,
|
|
36
|
+
{ protocols = [], headers }: WebSocketUpgradeInit = {},
|
|
37
|
+
): Request {
|
|
38
|
+
const httpUrl = new URL(`${url}`);
|
|
39
|
+
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
40
|
+
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
41
|
+
const all = new Headers(headers);
|
|
42
|
+
all.set("upgrade", "websocket");
|
|
43
|
+
all.set("connection", "Upgrade");
|
|
44
|
+
all.set("sec-websocket-version", "13");
|
|
45
|
+
if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
|
|
46
|
+
if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
|
|
47
|
+
if (!all.has(WS_VERSION_HEADER)) all.set(WS_VERSION_HEADER, `${WS_PORT_PROTOCOL_VERSION}`);
|
|
48
|
+
return withHeaders(new Request(httpUrl, { method: "GET", headers: all }), all);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Re-attaches `headers` to `request` as its own `headers` property, keeping
|
|
53
|
+
* the forbidden ones a browser dropped. Returns `request`.
|
|
54
|
+
*/
|
|
55
|
+
export function withHeaders(request: Request, headers: Headers): Request {
|
|
56
|
+
Object.defineProperty(request, "headers", { value: headers, configurable: true });
|
|
57
|
+
return request;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether `request` asks for a WebSocket. */
|
|
61
|
+
export function isWebSocketUpgrade(request: Request): boolean {
|
|
62
|
+
return request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The subprotocols `request` asks for, in order of preference. */
|
|
66
|
+
export function requestedProtocols(request: Request): string[] {
|
|
67
|
+
const header = request.headers.get("sec-websocket-protocol");
|
|
68
|
+
if (!header) return [];
|
|
69
|
+
return header
|
|
70
|
+
.split(",")
|
|
71
|
+
.map((protocol) => protocol.trim())
|
|
72
|
+
.filter((protocol) => protocol !== "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface UpgradeWebSocketOptions extends Omit<ServerWebSocketOptions, "url"> {}
|
|
76
|
+
|
|
77
|
+
export interface WebSocketUpgrade {
|
|
78
|
+
/** The server end. Call `accept()`, then listen and send. */
|
|
79
|
+
socket: ServerWebSocket;
|
|
80
|
+
/** Return it from the handler: status 101. */
|
|
81
|
+
response: Response;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface PendingUpgrade {
|
|
85
|
+
port: MessagePort;
|
|
86
|
+
protocol: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Where a response from `upgradeWebSocket` keeps its port. A registered
|
|
91
|
+
* symbol, not a module-level `WeakMap`: the handler and the host that takes
|
|
92
|
+
* the port often run different bundles of this package (a page's adapter from
|
|
93
|
+
* `dist/sw.js`, its handler code importing `dist/worker.js`), and each bundle
|
|
94
|
+
* has its own copy of every module.
|
|
95
|
+
*/
|
|
96
|
+
const UPGRADE = Symbol.for("webrun-http-browser:websocket-upgrade");
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Accepts a WebSocket upgrade in a handler:
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* if (isWebSocketUpgrade(request)) {
|
|
103
|
+
* const { socket, response } = upgradeWebSocket(request, { protocol: "v1" });
|
|
104
|
+
* socket.accept();
|
|
105
|
+
* socket.addEventListener("message", (event) => socket.send(event.data));
|
|
106
|
+
* return response;
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* The host takes the client's port from the response with `takePort` and
|
|
111
|
+
* hands it to the caller.
|
|
112
|
+
*/
|
|
113
|
+
export function upgradeWebSocket(
|
|
114
|
+
request: Request,
|
|
115
|
+
options: UpgradeWebSocketOptions = {},
|
|
116
|
+
): WebSocketUpgrade {
|
|
117
|
+
const protocol = options.protocol ?? "";
|
|
118
|
+
const channel = new MessageChannel();
|
|
119
|
+
const socket = new ServerWebSocket(channel.port1, { ...options, url: request.url, protocol });
|
|
120
|
+
const response = new Response(null, {
|
|
121
|
+
status: 200,
|
|
122
|
+
headers: protocol ? { "sec-websocket-protocol": protocol } : {},
|
|
123
|
+
});
|
|
124
|
+
Object.defineProperties(response, {
|
|
125
|
+
status: { value: 101, configurable: true },
|
|
126
|
+
statusText: { value: "Switching Protocols", configurable: true },
|
|
127
|
+
webSocket: { value: socket, configurable: true },
|
|
128
|
+
[UPGRADE]: { value: { port: channel.port2, protocol }, configurable: true, writable: true },
|
|
129
|
+
});
|
|
130
|
+
return { socket, response };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The port to hand to the caller, for a response from `upgradeWebSocket`.
|
|
135
|
+
* Taken once: a second call returns `undefined`.
|
|
136
|
+
*/
|
|
137
|
+
export function takePort(response: Response): MessagePort | undefined {
|
|
138
|
+
return takeUpgrade(response)?.port;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** `takePort`, with the subprotocol the server picked. */
|
|
142
|
+
export function takeUpgrade(response: Response): PendingUpgrade | undefined {
|
|
143
|
+
const holder = response as unknown as Record<symbol, PendingUpgrade | undefined>;
|
|
144
|
+
const upgrade = holder[UPGRADE];
|
|
145
|
+
if (upgrade) holder[UPGRADE] = undefined;
|
|
146
|
+
return upgrade;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type WebSocketUpgradeResult =
|
|
150
|
+
| { ok: true; port: MessagePort; protocol: string }
|
|
151
|
+
| { ok: false; status: number; message: string };
|
|
152
|
+
|
|
153
|
+
export interface ServeWebSocketUpgradeInit extends WebSocketUpgradeInit {
|
|
154
|
+
url: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Runs an upgrade through `handler`, as a host does when a caller asks for a
|
|
159
|
+
* socket: builds the upgrade request, calls the handler, and either takes the
|
|
160
|
+
* port of a `101` or reports the refusal. A subprotocol the caller did not ask
|
|
161
|
+
* for is a refusal, as in the standard.
|
|
162
|
+
*/
|
|
163
|
+
export async function serveWebSocketUpgrade(
|
|
164
|
+
handler: (request: Request) => Response | Promise<Response>,
|
|
165
|
+
{ url, protocols = [], headers }: ServeWebSocketUpgradeInit,
|
|
166
|
+
): Promise<WebSocketUpgradeResult> {
|
|
167
|
+
let response: Response;
|
|
168
|
+
try {
|
|
169
|
+
response = await handler(newWebSocketUpgradeRequest(url, { protocols, headers }));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return { ok: false, status: 500, message: errorMessage(error) };
|
|
172
|
+
}
|
|
173
|
+
const upgrade = takeUpgrade(response);
|
|
174
|
+
if (!upgrade || response.status !== 101) {
|
|
175
|
+
upgrade?.port.close();
|
|
176
|
+
try {
|
|
177
|
+
await response.body?.cancel();
|
|
178
|
+
} catch {}
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
status: response.status,
|
|
182
|
+
message: `The server refused the upgrade: ${response.status} ${response.statusText}`.trim(),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (upgrade.protocol !== "" && !protocols.includes(upgrade.protocol)) {
|
|
186
|
+
upgrade.port.close();
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
status: 400,
|
|
190
|
+
message: `The server picked the subprotocol '${upgrade.protocol}', which was not requested.`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return { ok: true, port: upgrade.port, protocol: upgrade.protocol };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function randomKey(): string {
|
|
197
|
+
const bytes = new Uint8Array(16);
|
|
198
|
+
crypto.getRandomValues(bytes);
|
|
199
|
+
return btoa(String.fromCharCode(...bytes));
|
|
200
|
+
}
|