@statewalker/cellb 0.2.0 → 0.4.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 +7 -5
- package/dist/bucket/celld-cells.js +25 -3
- package/dist/bucket/celld-cells.js.map +1 -1
- package/dist/isolate/host.d.ts +1 -1
- package/dist/isolate/host.js +17 -6
- package/dist/isolate/host.js.map +1 -1
- package/dist/isolate/isolate.js +8 -0
- package/dist/isolate/isolate.js.map +1 -1
- package/dist/isolate/protocol.d.ts +3 -2
- package/dist/isolate/protocol.js +47 -51
- package/dist/isolate/protocol.js.map +1 -1
- package/dist/peer/celld-peer.d.ts +17 -0
- package/dist/peer/celld-peer.js +49 -7
- package/dist/peer/celld-peer.js.map +1 -1
- package/dist/project/browser-shards.d.ts +8 -9
- package/dist/project/browser-shards.js +88 -52
- package/dist/project/browser-shards.js.map +1 -1
- package/dist/project/browser-tabs.d.ts +14 -4
- package/dist/project/browser-tabs.js +193 -208
- package/dist/project/browser-tabs.js.map +1 -1
- package/dist/project/wrangler.js +1 -1
- package/dist/project/wrangler.js.map +1 -1
- package/dist/transport/cells-http.d.ts +6 -0
- package/dist/transport/cells-http.js +22 -1
- package/dist/transport/cells-http.js.map +1 -1
- package/dist/transport/cells-link.d.ts +2 -0
- package/dist/transport/cells-link.js +5 -0
- package/dist/transport/cells-link.js.map +1 -1
- package/dist/transport/port.d.ts +3 -4
- package/dist/transport/port.js +25 -79
- package/dist/transport/port.js.map +1 -1
- package/dist/ws/client.d.ts +23 -8
- package/dist/ws/client.js +50 -189
- package/dist/ws/client.js.map +1 -1
- package/dist/ws/frames.d.ts +17 -8
- package/dist/ws/frames.js +3 -7
- package/dist/ws/frames.js.map +1 -1
- package/dist/ws/server.d.ts +17 -19
- package/dist/ws/server.js +53 -121
- package/dist/ws/server.js.map +1 -1
- package/package.json +5 -6
package/dist/transport/port.js
CHANGED
|
@@ -1,100 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { connect, overPipe, serve, structuredCodec } from "@statewalker/webrun-rpc";
|
|
1
|
+
import { connectHttpLink, serveHttpLink } from "@statewalker/webrun-http-browser/sw";
|
|
3
2
|
import { cellsHttpHandler, remoteRuntime, } from "./cells-http.js";
|
|
4
3
|
import { withScheme } from "./cells-link.js";
|
|
5
4
|
/**
|
|
6
|
-
* Multiplexed ports per link: one per exchange in flight
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Multiplexed ports per link: one per exchange in flight, given back when the exchange ends
|
|
6
|
+
* (p31-port-leak.test.ts). 4096 per link, cellb's ceiling before the links were the package's (its
|
|
7
|
+
* default is 1024): a streaming response holds its port for its whole life.
|
|
8
|
+
* `globalThis.__cellbMaxPorts` overrides it: tests that check nothing leaks set it low.
|
|
9
|
+
*
|
|
10
|
+
* Since webrun-http-browser 0.12 a body's chunks are pipelined under the link's flow-control
|
|
11
|
+
* window (256 KiB by default) instead of going one confirmed chunk at a time: a reader that stops
|
|
12
|
+
* holds about one window of its body, so the bound is about `maxPorts` windows per link.
|
|
9
13
|
*/
|
|
10
14
|
const maxPorts = () => globalThis.__cellbMaxPorts ?? 4096;
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
13
|
-
* webrun-rpc
|
|
14
|
-
*
|
|
16
|
+
* The links are webrun-http-browser's (`connectHttpLink` / `serveHttpLink`): HTTP/1.1 with
|
|
17
|
+
* streaming bodies over webrun-rpc calls, one multiplexed port per exchange. They take a
|
|
18
|
+
* MessagePort; a worker_threads port has the same `postMessage` / `addEventListener` surface.
|
|
19
|
+
*/
|
|
20
|
+
const asPort = (port) => port;
|
|
21
|
+
/**
|
|
22
|
+
* Cell-host side: serves the runtime's HTTP surface over a port. Resolves to an async
|
|
23
|
+
* disposer.
|
|
15
24
|
*/
|
|
16
25
|
export function serveRuntime(port, runtime, opts) {
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
async function fetchClosing(call, req) {
|
|
20
|
-
let output;
|
|
21
|
-
const res = await fetchOverDuplex(((input) => {
|
|
22
|
-
output = call(input);
|
|
23
|
-
return output;
|
|
24
|
-
}), req);
|
|
25
|
-
const drain = () => void (async () => {
|
|
26
|
-
try {
|
|
27
|
-
for await (const _ of output)
|
|
28
|
-
;
|
|
29
|
-
}
|
|
30
|
-
catch { }
|
|
31
|
-
})();
|
|
32
|
-
if (!output)
|
|
33
|
-
return res;
|
|
34
|
-
if (res.body === null) {
|
|
35
|
-
drain();
|
|
36
|
-
return res;
|
|
37
|
-
}
|
|
38
|
-
// The same once the body is read to its end, or cancelled by its reader (then the exchange
|
|
39
|
-
// is abandoned: the call's iterator is returned, which closes its port).
|
|
40
|
-
const reader = res.body.getReader();
|
|
41
|
-
const body = new ReadableStream({
|
|
42
|
-
async pull(ctl) {
|
|
43
|
-
const r = await reader.read();
|
|
44
|
-
if (r.done) {
|
|
45
|
-
ctl.close();
|
|
46
|
-
drain();
|
|
47
|
-
}
|
|
48
|
-
else
|
|
49
|
-
ctl.enqueue(r.value);
|
|
50
|
-
},
|
|
51
|
-
async cancel(reason) {
|
|
52
|
-
await reader.cancel(reason).catch(() => { });
|
|
53
|
-
await output.return?.(undefined).catch?.(() => { });
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
return Object.assign(new Response(body, res), {
|
|
57
|
-
webSocket: res.webSocket ?? null,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
function servingClosing(duplex) {
|
|
61
|
-
return async function* (input) {
|
|
62
|
-
const it = input[Symbol.asyncIterator]();
|
|
63
|
-
const rest = { [Symbol.asyncIterator]: () => it };
|
|
64
|
-
try {
|
|
65
|
-
yield* duplex(rest);
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
void (async () => {
|
|
69
|
-
try {
|
|
70
|
-
for (let r = await it.next(); !r.done; r = await it.next())
|
|
71
|
-
;
|
|
72
|
-
}
|
|
73
|
-
catch { }
|
|
74
|
-
})();
|
|
75
|
-
}
|
|
76
|
-
};
|
|
26
|
+
return serveHttpLink(asPort(port), cellsHttpHandler(runtime, opts), { maxPorts: maxPorts() });
|
|
77
27
|
}
|
|
78
28
|
/** Front side (page, SW bridge, another tab): a RemoteRuntime over a port. */
|
|
79
29
|
export async function connectRuntime(port, opts) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
const remote = await remoteRuntime((req) => fetchClosing(call, req), opts);
|
|
84
|
-
return Object.assign(remote, { close: async () => void (await close()) });
|
|
30
|
+
const link = await connectHttpLink(asPort(port), { maxPorts: maxPorts() });
|
|
31
|
+
const remote = await remoteRuntime((req) => link.send(req), opts);
|
|
32
|
+
return Object.assign(remote, { close: () => link.close() });
|
|
85
33
|
}
|
|
86
34
|
/** Serves any HTTP handler over a port (responder side of a link). */
|
|
87
35
|
export function serveHandlerOverPort(port, handler) {
|
|
88
|
-
return
|
|
36
|
+
return serveHttpLink(asPort(port), handler, { maxPorts: maxPorts() });
|
|
89
37
|
}
|
|
90
38
|
/** A `send` over a port (initiator side of a link); the scheme rides in a header. */
|
|
91
39
|
export async function linkOverPort(port) {
|
|
92
|
-
const
|
|
93
|
-
mux: overPipe(port, { codec: structuredCodec, side: "initiator", maxPorts: maxPorts() }),
|
|
94
|
-
});
|
|
40
|
+
const link = await connectHttpLink(asPort(port), { maxPorts: maxPorts() });
|
|
95
41
|
return {
|
|
96
|
-
send: (req) =>
|
|
97
|
-
close:
|
|
42
|
+
send: (req) => link.send(withScheme(req)),
|
|
43
|
+
close: () => link.close(),
|
|
98
44
|
};
|
|
99
45
|
}
|
|
100
46
|
//# sourceMappingURL=port.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/transport/port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/transport/port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAGrF,OAAO,EAEL,gBAAgB,EAEhB,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAa,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAExD;;;;;;;;;GASG;AACH,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAE,UAA2C,CAAC,eAAe,IAAI,IAAI,CAAC;AAQ5F;;;;GAIG;AACH,MAAM,MAAM,GAAG,CAAC,IAAc,EAAE,EAAE,CAAC,IAA8B,CAAC;AAElE;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAc,EACd,OAAoB,EACpB,IAAuB;IAEvB,OAAO,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;AAChG,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAc,EACd,IAAuB;IAEvB,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;IAClE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,oBAAoB,CAAC,IAAc,EAAE,OAAa;IAChE,OAAO,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;AACxE,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAc;IAEd,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC3E,OAAO;QACL,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACzC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE;KAC1B,CAAC;AACJ,CAAC","sourcesContent":["import { connectHttpLink, serveHttpLink } from \"@statewalker/webrun-http-browser/sw\";\nimport type { overPipe } from \"@statewalker/webrun-rpc\";\nimport type { CellRuntime } from \"../core/runtime.js\";\nimport {\n type CellsHttpOptions,\n cellsHttpHandler,\n type RemoteRuntime,\n remoteRuntime,\n} from \"./cells-http.js\";\nimport { type Send, withScheme } from \"./cells-link.js\";\n\n/**\n * Multiplexed ports per link: one per exchange in flight, given back when the exchange ends\n * (p31-port-leak.test.ts). 4096 per link, cellb's ceiling before the links were the package's (its\n * default is 1024): a streaming response holds its port for its whole life.\n * `globalThis.__cellbMaxPorts` overrides it: tests that check nothing leaks set it low.\n *\n * Since webrun-http-browser 0.12 a body's chunks are pipelined under the link's flow-control\n * window (256 KiB by default) instead of going one confirmed chunk at a time: a reader that stops\n * holds about one window of its body, so the bound is about `maxPorts` windows per link.\n */\nconst maxPorts = () => (globalThis as { __cellbMaxPorts?: number }).__cellbMaxPorts ?? 4096;\n\n/**\n * A DEDICATED message pipe (MessagePort, or a worker_threads port). Don't pass a\n * Worker object that also carries other messages — transfer a MessageChannel port.\n */\nexport type CellPort = Parameters<typeof overPipe>[0];\n\n/**\n * The links are webrun-http-browser's (`connectHttpLink` / `serveHttpLink`): HTTP/1.1 with\n * streaming bodies over webrun-rpc calls, one multiplexed port per exchange. They take a\n * MessagePort; a worker_threads port has the same `postMessage` / `addEventListener` surface.\n */\nconst asPort = (port: CellPort) => port as unknown as MessagePort;\n\n/**\n * Cell-host side: serves the runtime's HTTP surface over a port. Resolves to an async\n * disposer.\n */\nexport function serveRuntime(\n port: CellPort,\n runtime: CellRuntime,\n opts?: CellsHttpOptions,\n): Promise<() => Promise<void>> {\n return serveHttpLink(asPort(port), cellsHttpHandler(runtime, opts), { maxPorts: maxPorts() });\n}\n\n/** Front side (page, SW bridge, another tab): a RemoteRuntime over a port. */\nexport async function connectRuntime(\n port: CellPort,\n opts?: CellsHttpOptions,\n): Promise<RemoteRuntime & { close(): Promise<void> }> {\n const link = await connectHttpLink(asPort(port), { maxPorts: maxPorts() });\n const remote = await remoteRuntime((req) => link.send(req), opts);\n return Object.assign(remote, { close: () => link.close() });\n}\n\n/** Serves any HTTP handler over a port (responder side of a link). */\nexport function serveHandlerOverPort(port: CellPort, handler: Send): Promise<() => Promise<void>> {\n return serveHttpLink(asPort(port), handler, { maxPorts: maxPorts() });\n}\n\n/** A `send` over a port (initiator side of a link); the scheme rides in a header. */\nexport async function linkOverPort(\n port: CellPort,\n): Promise<{ send: Send; close(): Promise<void> }> {\n const link = await connectHttpLink(asPort(port), { maxPorts: maxPorts() });\n return {\n send: (req) => link.send(withScheme(req)),\n close: () => link.close(),\n };\n}\n"]}
|
package/dist/ws/client.d.ts
CHANGED
|
@@ -3,23 +3,38 @@ export type WebSocketConnector = (url: string, protocols: string[]) => Promise<{
|
|
|
3
3
|
port: MessagePort;
|
|
4
4
|
protocol?: string;
|
|
5
5
|
}>;
|
|
6
|
+
type IntervalTimers = {
|
|
7
|
+
setInterval(fn: () => void, ms: number): unknown;
|
|
8
|
+
clearInterval(id: unknown): void;
|
|
9
|
+
};
|
|
6
10
|
export interface CellsWebSocketOptions {
|
|
7
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Liveness ping period (ms, default 10 s); 0 disables the heartbeat. The peer is declared dead
|
|
13
|
+
* after 2 periods without a pong.
|
|
14
|
+
*/
|
|
8
15
|
heartbeatMs?: number;
|
|
9
16
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
17
|
+
* Timers for the heartbeat (and, when given, the close handshake's timeout). On a page's main
|
|
18
|
+
* thread pass worker-timers' functions: background-tab throttling of main-thread timers would
|
|
12
19
|
* otherwise delay pings and make the server drop a live socket. Workers and Node do not
|
|
13
20
|
* throttle, so the built-in timers are the default.
|
|
14
21
|
*/
|
|
15
|
-
timers?: {
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
timers?: IntervalTimers & {
|
|
23
|
+
setTimeout?(fn: () => void, ms: number): unknown;
|
|
24
|
+
clearTimeout?(id: unknown): void;
|
|
18
25
|
};
|
|
26
|
+
/** How long `close()` waits for the server's echo before closing with 1006 (default 5 s). */
|
|
27
|
+
closeTimeoutMs?: number;
|
|
19
28
|
}
|
|
20
29
|
/**
|
|
21
|
-
* A WHATWG `WebSocket` class whose sockets reach a Worker through `connect
|
|
22
|
-
*
|
|
30
|
+
* A WHATWG `WebSocket` class whose sockets reach a Worker through `connect` (page →
|
|
31
|
+
* ServiceWorker → the shard worker that owns the object, or in-process). It is
|
|
32
|
+
* `@statewalker/webrun-http-browser`'s client (`createWebSocketClass`): the socket's frames on its
|
|
33
|
+
* MessagePort are that package's (version `WS_PORT_PROTOCOL_VERSION`), which the server ends of
|
|
34
|
+
* cellb's WebSocketPairs speak. Installed for user code through webrun-modules `~deps` (provided
|
|
35
|
+
* `WebSocket`), so the real `globalThis.WebSocket` is left untouched. Typed as the platform's
|
|
36
|
+
* `WebSocket` (the class implements that interface).
|
|
23
37
|
*/
|
|
24
38
|
export declare function createWebSocketClass(connect: WebSocketConnector, options?: CellsWebSocketOptions): typeof WebSocket;
|
|
25
39
|
export type CellsWebSocketClass = ReturnType<typeof createWebSocketClass>;
|
|
40
|
+
export {};
|
package/dist/ws/client.js
CHANGED
|
@@ -1,203 +1,64 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createWebSocketClass as createPortWebSocketClass } from "@statewalker/webrun-http-browser/sw";
|
|
2
|
+
/** The longest period a timer takes (2³¹−1 ms, about 24.8 days): `heartbeatMs: 0` pings that rarely. */
|
|
3
|
+
const LONGEST_PERIOD_MS = 2 ** 31 - 1;
|
|
2
4
|
/**
|
|
3
|
-
* A WHATWG
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A WHATWG `WebSocket` class whose sockets reach a Worker through `connect`. Typed as the
|
|
12
|
-
* platform's `WebSocket` (the class implements that interface; its own state is private).
|
|
5
|
+
* A WHATWG `WebSocket` class whose sockets reach a Worker through `connect` (page →
|
|
6
|
+
* ServiceWorker → the shard worker that owns the object, or in-process). It is
|
|
7
|
+
* `@statewalker/webrun-http-browser`'s client (`createWebSocketClass`): the socket's frames on its
|
|
8
|
+
* MessagePort are that package's (version `WS_PORT_PROTOCOL_VERSION`), which the server ends of
|
|
9
|
+
* cellb's WebSocketPairs speak. Installed for user code through webrun-modules `~deps` (provided
|
|
10
|
+
* `WebSocket`), so the real `globalThis.WebSocket` is left untouched. Typed as the platform's
|
|
11
|
+
* `WebSocket` (the class implements that interface).
|
|
13
12
|
*/
|
|
14
13
|
export function createWebSocketClass(connect, options = {}) {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/** Sizes of the messages posted and not acknowledged yet, in order (the port keeps it). */
|
|
35
|
-
#unacked = [];
|
|
36
|
-
/** Bytes sent and not yet received by the server end (it acknowledges each message). */
|
|
37
|
-
get bufferedAmount() {
|
|
38
|
-
return this.#buffered;
|
|
39
|
-
}
|
|
40
|
-
#binaryType = "blob";
|
|
41
|
-
get binaryType() {
|
|
42
|
-
return this.#binaryType;
|
|
43
|
-
}
|
|
44
|
-
/** Only "blob" and "arraybuffer" are taken; any other value is ignored (WHATWG). */
|
|
45
|
-
set binaryType(v) {
|
|
46
|
-
if (v === "blob" || v === "arraybuffer")
|
|
47
|
-
this.#binaryType = v;
|
|
48
|
-
}
|
|
49
|
-
onopen = null;
|
|
50
|
-
onmessage = null;
|
|
51
|
-
onclose = null;
|
|
52
|
-
onerror = null;
|
|
53
|
-
#port;
|
|
54
|
-
#pendingClose;
|
|
55
|
-
#beat;
|
|
56
|
-
#lastPong = 0;
|
|
57
|
-
#startHeartbeat() {
|
|
58
|
-
if (!heartbeatMs)
|
|
59
|
-
return;
|
|
60
|
-
this.#lastPong = Date.now();
|
|
61
|
-
this.#beat = timers.setInterval(() => {
|
|
62
|
-
if (this.readyState >= 3)
|
|
63
|
-
return this.#stopHeartbeat();
|
|
64
|
-
if (Date.now() - this.#lastPong > 2 * heartbeatMs)
|
|
65
|
-
return this.#fail(); // peer gone
|
|
66
|
-
this.#port?.postMessage({ t: "p", hb: heartbeatMs });
|
|
67
|
-
}, heartbeatMs);
|
|
68
|
-
this.#port?.postMessage({ t: "p", hb: heartbeatMs });
|
|
69
|
-
}
|
|
70
|
-
#stopHeartbeat() {
|
|
71
|
-
if (this.#beat !== undefined)
|
|
72
|
-
timers.clearInterval(this.#beat);
|
|
73
|
-
this.#beat = undefined;
|
|
74
|
-
}
|
|
14
|
+
const { timers } = options;
|
|
15
|
+
const setTimer = timers?.setTimeout?.bind(timers) ?? ((fn, ms) => setTimeout(fn, ms));
|
|
16
|
+
const clearTimer = timers?.clearTimeout?.bind(timers) ??
|
|
17
|
+
((id) => clearTimeout(id));
|
|
18
|
+
const Base = createPortWebSocketClass(async (url, protocols) => {
|
|
19
|
+
const { port, protocol } = await connect(url, protocols);
|
|
20
|
+
return { port, protocol: protocol ?? "" };
|
|
21
|
+
}, {
|
|
22
|
+
heartbeatMs: options.heartbeatMs === 0 ? LONGEST_PERIOD_MS : options.heartbeatMs,
|
|
23
|
+
closeTimeoutMs: options.closeTimeoutMs,
|
|
24
|
+
timers: timers && {
|
|
25
|
+
setInterval: (fn, ms) => timers.setInterval(fn, ms),
|
|
26
|
+
clearInterval: (id) => timers.clearInterval(id),
|
|
27
|
+
setTimeout: setTimer,
|
|
28
|
+
clearTimeout: clearTimer,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
// Two of cellb's own client's rules the package's does not apply.
|
|
32
|
+
class CellsWebSocket extends Base {
|
|
75
33
|
constructor(url, protocols = []) {
|
|
76
|
-
super();
|
|
77
|
-
// WHATWG "new WebSocket(url, protocols)" validation, in spec order.
|
|
78
|
-
let u;
|
|
79
|
-
try {
|
|
80
|
-
u = new URL(String(url), globalThis.location?.href ?? "http://localhost/");
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
throw new DOMException(`The URL '${String(url)}' is invalid.`, "SyntaxError");
|
|
84
|
-
}
|
|
85
|
-
if (u.protocol === "http:")
|
|
86
|
-
u.protocol = "ws:";
|
|
87
|
-
if (u.protocol === "https:")
|
|
88
|
-
u.protocol = "wss:";
|
|
89
|
-
if (u.protocol !== "ws:" && u.protocol !== "wss:")
|
|
90
|
-
throw new DOMException(`The URL's scheme must be either 'ws' or 'wss'.`, "SyntaxError");
|
|
91
|
-
if (u.hash || String(url).includes("#"))
|
|
92
|
-
throw new DOMException("The URL contains a fragment identifier.", "SyntaxError");
|
|
93
34
|
const list = typeof protocols === "string" ? [protocols] : [...protocols];
|
|
35
|
+
// Subprotocols are duplicates case-insensitively (WHATWG; WPT
|
|
36
|
+
// Create-protocols-repeated-case-insensitive).
|
|
94
37
|
const seen = new Set();
|
|
95
38
|
for (const p of list) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (seen.has(p.toLowerCase()))
|
|
39
|
+
const key = p.toLowerCase();
|
|
40
|
+
if (seen.has(key))
|
|
99
41
|
throw new DOMException(`The subprotocol '${p}' is duplicated.`, "SyntaxError");
|
|
100
|
-
seen.add(
|
|
42
|
+
seen.add(key);
|
|
101
43
|
}
|
|
102
|
-
|
|
103
|
-
connect(this.url, list).then(({ port, protocol }) => this.#opened(port, protocol ?? ""), () => this.#fail());
|
|
104
|
-
}
|
|
105
|
-
#emit(ev, handler) {
|
|
106
|
-
handler?.call(this, ev);
|
|
107
|
-
this.dispatchEvent(ev);
|
|
108
|
-
}
|
|
109
|
-
#opened(port, protocol) {
|
|
110
|
-
if (this.readyState === 3) {
|
|
111
|
-
port.close();
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
this.#port = port;
|
|
115
|
-
this.protocol = protocol;
|
|
116
|
-
port.onmessage = (e) => this.#frame(e.data);
|
|
117
|
-
port.start?.();
|
|
118
|
-
if (this.#pendingClose) {
|
|
119
|
-
// close() was called while connecting
|
|
120
|
-
this.readyState = 2;
|
|
121
|
-
port.postMessage({
|
|
122
|
-
t: "c",
|
|
123
|
-
code: this.#pendingClose.code ?? 1000,
|
|
124
|
-
reason: this.#pendingClose.reason ?? "",
|
|
125
|
-
});
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
this.readyState = 1;
|
|
129
|
-
this.#startHeartbeat();
|
|
130
|
-
this.#emit(new Event("open"), this.onopen);
|
|
131
|
-
}
|
|
132
|
-
#fail() {
|
|
133
|
-
if (this.readyState === 3)
|
|
134
|
-
return;
|
|
135
|
-
this.#stopHeartbeat();
|
|
136
|
-
this.#port?.close();
|
|
137
|
-
this.readyState = 3;
|
|
138
|
-
this.#emit(new Event("error"), this.onerror);
|
|
139
|
-
this.#emit(new CellsCloseEvent("close", { code: 1006, reason: "", wasClean: false }), this.onclose);
|
|
140
|
-
}
|
|
141
|
-
#frame(f) {
|
|
142
|
-
if (f.t === "q") {
|
|
143
|
-
this.#lastPong = Date.now();
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (f.t === "p")
|
|
147
|
-
return;
|
|
148
|
-
if (f.t === "a") {
|
|
149
|
-
this.#buffered -= this.#unacked.shift() ?? 0;
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (f.t === "m") {
|
|
153
|
-
if (this.readyState !== 1)
|
|
154
|
-
return;
|
|
155
|
-
let data = f.d;
|
|
156
|
-
if (typeof data !== "string" && this.binaryType === "blob")
|
|
157
|
-
data = new Blob([data]);
|
|
158
|
-
this.#emit(new MessageEvent("message", { data, origin: new URL(this.url).origin }), this.onmessage);
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
const echo = this.readyState === 1;
|
|
162
|
-
if (echo)
|
|
163
|
-
this.#port.postMessage({ t: "c", code: f.code, reason: f.reason }); // close handshake
|
|
164
|
-
this.readyState = 3;
|
|
165
|
-
this.#stopHeartbeat();
|
|
166
|
-
this.#port.close();
|
|
167
|
-
this.#emit(new CellsCloseEvent("close", { code: f.code, reason: f.reason, wasClean: true }), this.onclose);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
send(data) {
|
|
171
|
-
if (this.readyState === 0)
|
|
172
|
-
throw new DOMException("Still in CONNECTING state.", "InvalidStateError");
|
|
173
|
-
if (this.readyState !== 1)
|
|
174
|
-
return; // spec: silently discarded after closing
|
|
175
|
-
if (data instanceof Blob) {
|
|
176
|
-
this.#buffered += data.size;
|
|
177
|
-
void data.arrayBuffer().then((b) => this.readyState === 1 && this.#post(b));
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const d = toFrameData(data);
|
|
181
|
-
this.#buffered += byteSize(d);
|
|
182
|
-
this.#post(d);
|
|
183
|
-
}
|
|
184
|
-
#post(d) {
|
|
185
|
-
this.#unacked.push(byteSize(d));
|
|
186
|
-
this.#port.postMessage({ t: "m", d }, typeof d === "string" ? [] : [d]);
|
|
187
|
-
}
|
|
188
|
-
close(code, reason) {
|
|
189
|
-
checkClose(code, reason);
|
|
190
|
-
if (this.readyState >= 2)
|
|
191
|
-
return;
|
|
192
|
-
if (this.readyState === 0) {
|
|
193
|
-
this.#pendingClose = { code, reason };
|
|
194
|
-
this.readyState = 2;
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
this.readyState = 2;
|
|
198
|
-
this.#port.postMessage({ t: "c", code: code ?? 1005, reason: reason ?? "" });
|
|
44
|
+
super(resolved(url), list);
|
|
199
45
|
}
|
|
200
46
|
}
|
|
201
47
|
return CellsWebSocket;
|
|
202
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* A relative url resolved against the page, or, where there is none (Node, a worker without
|
|
51
|
+
* `location`), against `http://localhost/`, as cellb's client always did. An url that does not
|
|
52
|
+
* parse is passed on, for the class to refuse with its SyntaxError.
|
|
53
|
+
*/
|
|
54
|
+
function resolved(url) {
|
|
55
|
+
if (globalThis.location)
|
|
56
|
+
return url;
|
|
57
|
+
try {
|
|
58
|
+
return new URL(String(url), "http://localhost/").href;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
203
64
|
//# sourceMappingURL=client.js.map
|
package/dist/ws/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/ws/client.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n byteSize,\n CellsCloseEvent,\n checkClose,\n DEFAULT_HEARTBEAT_MS,\n type Frame,\n toFrameData,\n} from \"./frames.js\";\n\n/** Opens the transport of a socket: resolves with the MessagePort once the server accepted (101). */\nexport type WebSocketConnector = (\n url: string,\n protocols: string[],\n) => Promise<{ port: MessagePort; protocol?: string }>;\n\ntype Handler = ((ev: any) => void) | null;\n\n/**\n * A WHATWG-WebSocket-compatible class whose transport is a MessagePort obtained\n * from `connect` (page → ServiceWorker → owning shard worker, or in-process).\n * Installed for user code through webrun-modules `~deps` (provided `WebSocket`),\n * so the real `globalThis.WebSocket` is left untouched.\n */\n/** RFC 7230 token (subprotocol names). */\nconst TOKEN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\nexport interface CellsWebSocketOptions {\n /** Liveness ping period (ms); 0 disables. The peer is declared dead after 2 missed periods. */\n heartbeatMs?: number;\n /**\n * Interval timers for the heartbeat. On a page's main thread pass worker-timers'\n * `setInterval`/`clearInterval`: background-tab throttling of main-thread timers would\n * otherwise delay pings and make the server drop a live socket. Workers and Node do not\n * throttle, so the built-in timers are the default.\n */\n timers?: { setInterval(fn: () => void, ms: number): unknown; clearInterval(id: unknown): void };\n}\n\n/**\n * A WHATWG `WebSocket` class whose sockets reach a Worker through `connect`. Typed as the\n * platform's `WebSocket` (the class implements that interface; its own state is private).\n */\nexport function createWebSocketClass(\n connect: WebSocketConnector,\n options: CellsWebSocketOptions = {},\n): typeof WebSocket {\n const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;\n const timers = options.timers ?? {\n setInterval: (fn: () => void, ms: number) => setInterval(fn, ms),\n clearInterval: (id: unknown) => clearInterval(id as ReturnType<typeof setInterval>),\n };\n class CellsWebSocket extends EventTarget {\n static readonly CONNECTING = 0;\n static readonly OPEN = 1;\n static readonly CLOSING = 2;\n static readonly CLOSED = 3;\n readonly CONNECTING = 0;\n readonly OPEN = 1;\n readonly CLOSING = 2;\n readonly CLOSED = 3;\n\n readonly url: string;\n readyState = 0;\n protocol = \"\";\n extensions = \"\";\n #buffered = 0;\n /** Sizes of the messages posted and not acknowledged yet, in order (the port keeps it). */\n #unacked: number[] = [];\n /** Bytes sent and not yet received by the server end (it acknowledges each message). */\n get bufferedAmount(): number {\n return this.#buffered;\n }\n #binaryType: \"blob\" | \"arraybuffer\" = \"blob\";\n get binaryType(): \"blob\" | \"arraybuffer\" {\n return this.#binaryType;\n }\n /** Only \"blob\" and \"arraybuffer\" are taken; any other value is ignored (WHATWG). */\n set binaryType(v: \"blob\" | \"arraybuffer\") {\n if (v === \"blob\" || v === \"arraybuffer\") this.#binaryType = v;\n }\n onopen: Handler = null;\n onmessage: Handler = null;\n onclose: Handler = null;\n onerror: Handler = null;\n #port: MessagePort | undefined;\n #pendingClose: { code?: number; reason?: string } | undefined;\n #beat: unknown;\n #lastPong = 0;\n\n #startHeartbeat(): void {\n if (!heartbeatMs) return;\n this.#lastPong = Date.now();\n this.#beat = timers.setInterval(() => {\n if (this.readyState >= 3) return this.#stopHeartbeat();\n if (Date.now() - this.#lastPong > 2 * heartbeatMs) return this.#fail(); // peer gone\n this.#port?.postMessage({ t: \"p\", hb: heartbeatMs } satisfies Frame);\n }, heartbeatMs);\n this.#port?.postMessage({ t: \"p\", hb: heartbeatMs } satisfies Frame);\n }\n #stopHeartbeat(): void {\n if (this.#beat !== undefined) timers.clearInterval(this.#beat);\n this.#beat = undefined;\n }\n\n constructor(url: string | URL, protocols: string | string[] = []) {\n super();\n // WHATWG \"new WebSocket(url, protocols)\" validation, in spec order.\n let u: URL;\n try {\n u = new URL(String(url), (globalThis as any).location?.href ?? \"http://localhost/\");\n } catch {\n throw new DOMException(`The URL '${String(url)}' is invalid.`, \"SyntaxError\");\n }\n if (u.protocol === \"http:\") u.protocol = \"ws:\";\n if (u.protocol === \"https:\") u.protocol = \"wss:\";\n if (u.protocol !== \"ws:\" && u.protocol !== \"wss:\")\n throw new DOMException(`The URL's scheme must be either 'ws' or 'wss'.`, \"SyntaxError\");\n if (u.hash || String(url).includes(\"#\"))\n throw new DOMException(\"The URL contains a fragment identifier.\", \"SyntaxError\");\n const list = typeof protocols === \"string\" ? [protocols] : [...protocols];\n const seen = new Set<string>();\n for (const p of list) {\n if (!TOKEN.test(p))\n throw new DOMException(`The subprotocol '${p}' is invalid.`, \"SyntaxError\");\n if (seen.has(p.toLowerCase()))\n throw new DOMException(`The subprotocol '${p}' is duplicated.`, \"SyntaxError\");\n seen.add(p.toLowerCase());\n }\n this.url = u.toString();\n connect(this.url, list).then(\n ({ port, protocol }) => this.#opened(port, protocol ?? \"\"),\n () => this.#fail(),\n );\n }\n\n #emit(ev: Event, handler: Handler): void {\n handler?.call(this, ev);\n this.dispatchEvent(ev);\n }\n\n #opened(port: MessagePort, protocol: string): void {\n if (this.readyState === 3) {\n port.close();\n return;\n }\n this.#port = port;\n this.protocol = protocol;\n port.onmessage = (e) => this.#frame(e.data as Frame);\n port.start?.();\n if (this.#pendingClose) {\n // close() was called while connecting\n this.readyState = 2;\n port.postMessage({\n t: \"c\",\n code: this.#pendingClose.code ?? 1000,\n reason: this.#pendingClose.reason ?? \"\",\n } satisfies Frame);\n return;\n }\n this.readyState = 1;\n this.#startHeartbeat();\n this.#emit(new Event(\"open\"), this.onopen);\n }\n\n #fail(): void {\n if (this.readyState === 3) return;\n this.#stopHeartbeat();\n this.#port?.close();\n this.readyState = 3;\n this.#emit(new Event(\"error\"), this.onerror);\n this.#emit(\n new CellsCloseEvent(\"close\", { code: 1006, reason: \"\", wasClean: false }),\n this.onclose,\n );\n }\n\n #frame(f: Frame): void {\n if (f.t === \"q\") {\n this.#lastPong = Date.now();\n return;\n }\n if (f.t === \"p\") return;\n if (f.t === \"a\") {\n this.#buffered -= this.#unacked.shift() ?? 0;\n return;\n }\n if (f.t === \"m\") {\n if (this.readyState !== 1) return;\n let data: unknown = f.d;\n if (typeof data !== \"string\" && this.binaryType === \"blob\")\n data = new Blob([data as ArrayBuffer]);\n this.#emit(\n new MessageEvent(\"message\", { data, origin: new URL(this.url).origin }),\n this.onmessage,\n );\n } else {\n const echo = this.readyState === 1;\n if (echo)\n this.#port!.postMessage({ t: \"c\", code: f.code, reason: f.reason } satisfies Frame); // close handshake\n this.readyState = 3;\n this.#stopHeartbeat();\n this.#port!.close();\n this.#emit(\n new CellsCloseEvent(\"close\", { code: f.code, reason: f.reason, wasClean: true }),\n this.onclose,\n );\n }\n }\n\n send(data: string | ArrayBuffer | ArrayBufferView | Blob): void {\n if (this.readyState === 0)\n throw new DOMException(\"Still in CONNECTING state.\", \"InvalidStateError\");\n if (this.readyState !== 1) return; // spec: silently discarded after closing\n if (data instanceof Blob) {\n this.#buffered += data.size;\n void data.arrayBuffer().then((b) => this.readyState === 1 && this.#post(b));\n return;\n }\n const d = toFrameData(data);\n this.#buffered += byteSize(d);\n this.#post(d);\n }\n\n #post(d: string | ArrayBuffer): void {\n this.#unacked.push(byteSize(d));\n this.#port!.postMessage({ t: \"m\", d } satisfies Frame, typeof d === \"string\" ? [] : [d]);\n }\n\n close(code?: number, reason?: string): void {\n checkClose(code, reason);\n if (this.readyState >= 2) return;\n if (this.readyState === 0) {\n this.#pendingClose = { code, reason };\n this.readyState = 2;\n return;\n }\n this.readyState = 2;\n this.#port!.postMessage({ t: \"c\", code: code ?? 1005, reason: reason ?? \"\" } satisfies Frame);\n }\n }\n return CellsWebSocket as unknown as typeof WebSocket;\n}\n\nexport type CellsWebSocketClass = ReturnType<typeof createWebSocketClass>;\n"]}
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/ws/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,IAAI,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAavG,wGAAwG;AACxG,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAsBtC;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAA2B,EAC3B,OAAO,GAA0B,EAAE;IAEnC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAC3B,MAAM,QAAQ,GAAG,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IACtF,MAAM,UAAU,GACd,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC;QAClC,CAAC,CAAC,EAAW,EAAE,EAAE,CAAC,YAAY,CAAC,EAAmC,CAAC,CAAC,CAAC;IACvE,MAAM,IAAI,GAAG,wBAAwB,CACnC,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE;QACvB,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QACzD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,IAAI,EAAE,EAAE,CAAC;IAC5C,CAAC,EACD;QACE,WAAW,EAAE,OAAO,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW;QAChF,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,MAAM,EAAE,MAAM,IAAI;YAChB,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC;YACnD,aAAa,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;YAC/C,UAAU,EAAE,QAAQ;YACpB,YAAY,EAAE,UAAU;SACzB;KACF,CACF,CAAC;IACF,kEAAkE;IAClE,MAAM,cAAe,SAAQ,IAAI;QAC/B,YAAY,GAAiB,EAAE,SAAS,GAAsB,EAAE;YAC9D,MAAM,IAAI,GAAG,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;YAC1E,8DAA8D;YAC9D,+CAA+C;YAC/C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;YAC/B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACrB,MAAM,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;gBAC5B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;oBACf,MAAM,IAAI,YAAY,CAAC,oBAAoB,CAAC,kBAAkB,EAAE,aAAa,CAAC,CAAC;gBACjF,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAChB,CAAC;YACD,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC;KACF;IACD,OAAO,cAA6C,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,SAAS,QAAQ,CAAC,GAAiB;IACjC,IAAK,UAAqC,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC;IAChE,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,mBAAmB,CAAC,CAAC,IAAI,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC;IACb,CAAC;AACH,CAAC","sourcesContent":["import { createWebSocketClass as createPortWebSocketClass } from \"@statewalker/webrun-http-browser/sw\";\n\n/** Opens the transport of a socket: resolves with the MessagePort once the server accepted (101). */\nexport type WebSocketConnector = (\n url: string,\n protocols: string[],\n) => Promise<{ port: MessagePort; protocol?: string }>;\n\ntype IntervalTimers = {\n setInterval(fn: () => void, ms: number): unknown;\n clearInterval(id: unknown): void;\n};\n\n/** The longest period a timer takes (2³¹−1 ms, about 24.8 days): `heartbeatMs: 0` pings that rarely. */\nconst LONGEST_PERIOD_MS = 2 ** 31 - 1;\n\nexport interface CellsWebSocketOptions {\n /**\n * Liveness ping period (ms, default 10 s); 0 disables the heartbeat. The peer is declared dead\n * after 2 periods without a pong.\n */\n heartbeatMs?: number;\n /**\n * Timers for the heartbeat (and, when given, the close handshake's timeout). On a page's main\n * thread pass worker-timers' functions: background-tab throttling of main-thread timers would\n * otherwise delay pings and make the server drop a live socket. Workers and Node do not\n * throttle, so the built-in timers are the default.\n */\n timers?: IntervalTimers & {\n setTimeout?(fn: () => void, ms: number): unknown;\n clearTimeout?(id: unknown): void;\n };\n /** How long `close()` waits for the server's echo before closing with 1006 (default 5 s). */\n closeTimeoutMs?: number;\n}\n\n/**\n * A WHATWG `WebSocket` class whose sockets reach a Worker through `connect` (page →\n * ServiceWorker → the shard worker that owns the object, or in-process). It is\n * `@statewalker/webrun-http-browser`'s client (`createWebSocketClass`): the socket's frames on its\n * MessagePort are that package's (version `WS_PORT_PROTOCOL_VERSION`), which the server ends of\n * cellb's WebSocketPairs speak. Installed for user code through webrun-modules `~deps` (provided\n * `WebSocket`), so the real `globalThis.WebSocket` is left untouched. Typed as the platform's\n * `WebSocket` (the class implements that interface).\n */\nexport function createWebSocketClass(\n connect: WebSocketConnector,\n options: CellsWebSocketOptions = {},\n): typeof WebSocket {\n const { timers } = options;\n const setTimer = timers?.setTimeout?.bind(timers) ?? ((fn, ms) => setTimeout(fn, ms));\n const clearTimer =\n timers?.clearTimeout?.bind(timers) ??\n ((id: unknown) => clearTimeout(id as ReturnType<typeof setTimeout>));\n const Base = createPortWebSocketClass(\n async (url, protocols) => {\n const { port, protocol } = await connect(url, protocols);\n return { port, protocol: protocol ?? \"\" };\n },\n {\n heartbeatMs: options.heartbeatMs === 0 ? LONGEST_PERIOD_MS : options.heartbeatMs,\n closeTimeoutMs: options.closeTimeoutMs,\n timers: timers && {\n setInterval: (fn, ms) => timers.setInterval(fn, ms),\n clearInterval: (id) => timers.clearInterval(id),\n setTimeout: setTimer,\n clearTimeout: clearTimer,\n },\n },\n );\n // Two of cellb's own client's rules the package's does not apply.\n class CellsWebSocket extends Base {\n constructor(url: string | URL, protocols: string | string[] = []) {\n const list = typeof protocols === \"string\" ? [protocols] : [...protocols];\n // Subprotocols are duplicates case-insensitively (WHATWG; WPT\n // Create-protocols-repeated-case-insensitive).\n const seen = new Set<string>();\n for (const p of list) {\n const key = p.toLowerCase();\n if (seen.has(key))\n throw new DOMException(`The subprotocol '${p}' is duplicated.`, \"SyntaxError\");\n seen.add(key);\n }\n super(resolved(url), list);\n }\n }\n return CellsWebSocket as unknown as typeof WebSocket;\n}\n\n/**\n * A relative url resolved against the page, or, where there is none (Node, a worker without\n * `location`), against `http://localhost/`, as cellb's client always did. An url that does not\n * parse is passed on, for the class to refuse with its SyntaxError.\n */\nfunction resolved(url: string | URL): string | URL {\n if ((globalThis as { location?: unknown }).location) return url;\n try {\n return new URL(String(url), \"http://localhost/\").href;\n } catch {\n return url;\n }\n}\n\nexport type CellsWebSocketClass = ReturnType<typeof createWebSocketClass>;\n"]}
|
package/dist/ws/frames.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol of a WebSocket carried by a MessagePort (both directions):
|
|
3
|
+
* `@statewalker/webrun-http-browser`'s frames (version `WS_PORT_PROTOCOL_VERSION`), spoken by its
|
|
4
|
+
* client (`createWebSocketClass`) and by the ends of cellb's WebSocketPairs.
|
|
5
|
+
*
|
|
6
|
+
* m a message (an ArrayBuffer is transferred)
|
|
7
|
+
* c close, initiating or echo
|
|
8
|
+
* p ping (client → server), carries the client's heartbeat period
|
|
9
|
+
* q pong (server → client)
|
|
10
|
+
* a ack: the receiver dispatched `n` more bytes of messages (the sender's `bufferedAmount`)
|
|
11
|
+
*
|
|
12
|
+
* MessagePorts never report that the other side died (no `close` event in browsers), so both
|
|
13
|
+
* ends run a heartbeat: the client pings every `hb` ms and closes with 1006 when pongs stop for
|
|
14
|
+
* 2·hb; the server closes with 1006 when pings stop for 3·hb. This is what makes a socket notice a
|
|
15
|
+
* closed tab. A hidden tab's client announces a longer period (its timers are throttled).
|
|
16
|
+
*/
|
|
2
17
|
export type Frame = {
|
|
3
18
|
t: "m";
|
|
4
19
|
d: string | ArrayBuffer;
|
|
@@ -13,14 +28,8 @@ export type Frame = {
|
|
|
13
28
|
t: "q";
|
|
14
29
|
} | {
|
|
15
30
|
t: "a";
|
|
31
|
+
n: number;
|
|
16
32
|
};
|
|
17
|
-
/**
|
|
18
|
-
* MessagePorts never report that the other side died (no `close` event in
|
|
19
|
-
* browsers), so both ends run a heartbeat: the client pings every `hb` ms and
|
|
20
|
-
* closes with 1006 when pongs stop for 2·hb; the server closes with 1006 when
|
|
21
|
-
* pings stop for 3·hb. This is what makes a socket notice a closed tab.
|
|
22
|
-
*/
|
|
23
|
-
export declare const DEFAULT_HEARTBEAT_MS = 10000;
|
|
24
33
|
/** What a message adds to the sender's `bufferedAmount` (WHATWG: UTF-8 bytes of a string). */
|
|
25
34
|
export declare function byteSize(d: string | ArrayBuffer): number;
|
|
26
35
|
export declare function toFrameData(data: unknown): string | ArrayBuffer;
|
package/dist/ws/frames.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* closes with 1006 when pongs stop for 2·hb; the server closes with 1006 when
|
|
5
|
-
* pings stop for 3·hb. This is what makes a socket notice a closed tab.
|
|
6
|
-
*/
|
|
7
|
-
export const DEFAULT_HEARTBEAT_MS = 10_000;
|
|
1
|
+
// Declared here, not imported from webrun-http-browser/sw: its declarations name ServiceWorker
|
|
2
|
+
// types, and cellb's must compile with the DOM lib alone (scripts/pack-check.mjs). The two are
|
|
3
|
+
// kept the same by test/p07w-websocket.test.ts.
|
|
8
4
|
/** What a message adds to the sender's `bufferedAmount` (WHATWG: UTF-8 bytes of a string). */
|
|
9
5
|
export function byteSize(d) {
|
|
10
6
|
return typeof d === "string" ? new TextEncoder().encode(d).byteLength : d.byteLength;
|
package/dist/ws/frames.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/ws/frames.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/ws/frames.ts"],"names":[],"mappings":"AAsBA,+FAA+F;AAC/F,+FAA+F;AAC/F,gDAAgD;AAEhD,8FAA8F;AAC9F,MAAM,UAAU,QAAQ,CAAC,CAAuB;IAC9C,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;AACvF,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAa;IACvC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,IAAI,YAAY,WAAW;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtD,IAAI,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC;QAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAgB,CAAC;IAC9F,MAAM,IAAI,SAAS,CACjB,gFAAgF,CACjF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAa,EAAE,MAAe;IACvD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,IAAI,CAAC;QACrE,MAAM,IAAI,YAAY,CACpB,iEAAiE,IAAI,cAAc,EACnF,oBAAoB,CACrB,CAAC;IACJ,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,UAAU,GAAG,GAAG;QAC3E,MAAM,IAAI,YAAY,CACpB,4DAA4D,EAC5D,aAAa,CACd,CAAC;AACN,CAAC;AAED,0FAA0F;AAC1F,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,IAAI,CAAS;IACb,MAAM,CAAS;IACf,QAAQ,CAAU;IAC3B,YAAY,IAAY,EAAE,IAAI,GAA2D,EAAE;QACzF,KAAK,CAAC,IAAI,CAAC,CAAC;QACZ,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC;QAC9B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;QAChC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC;IACzC,CAAC;CACF;AASD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAiB,EAAY,EAAE,CAAC,CAAC;IACvD,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAChC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE;QACb,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAa,CAAC,CAAC;IAC9C,CAAC;IACD,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE;CAC1B,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,CAAW,EACX,CAAW,EACX,MAAmB;IAEnB,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,IAAI,IAAI;YAAE,OAAO;QACjB,IAAI,GAAG,IAAI,CAAC;QACZ,MAAM,EAAE,EAAE,CAAC;QACX,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE;YACxB,CAAC,CAAC,KAAK,EAAE,CAAC;YACV,CAAC,CAAC,KAAK,EAAE,CAAC;QACZ,CAAC,EAAE,KAAK,CAAC,CAAC;QACT,CAAwB,CAAC,KAAK,EAAE,EAAE,CAAC;IACtC,CAAC,CAAC;IACF,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACb,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACV,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE,OAAO;QACxB,KAAK,GAAG,IAAI,CAAC;QACb,IAAI,KAAK;YAAE,MAAM,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;IACH,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACb,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACV,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE,OAAO;QACxB,KAAK,GAAG,IAAI,CAAC;QACb,IAAI,KAAK;YAAE,MAAM,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;QACtB,IAAI,IAAI;YAAE,OAAO;QACjB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACjC,MAAM,EAAE,CAAC;IACX,CAAC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Wire protocol of a WebSocket carried by a MessagePort (both directions):\n * `@statewalker/webrun-http-browser`'s frames (version `WS_PORT_PROTOCOL_VERSION`), spoken by its\n * client (`createWebSocketClass`) and by the ends of cellb's WebSocketPairs.\n *\n * m a message (an ArrayBuffer is transferred)\n * c close, initiating or echo\n * p ping (client → server), carries the client's heartbeat period\n * q pong (server → client)\n * a ack: the receiver dispatched `n` more bytes of messages (the sender's `bufferedAmount`)\n *\n * MessagePorts never report that the other side died (no `close` event in browsers), so both\n * ends run a heartbeat: the client pings every `hb` ms and closes with 1006 when pongs stop for\n * 2·hb; the server closes with 1006 when pings stop for 3·hb. This is what makes a socket notice a\n * closed tab. A hidden tab's client announces a longer period (its timers are throttled).\n */\nexport type Frame =\n | { t: \"m\"; d: string | ArrayBuffer }\n | { t: \"c\"; code: number; reason: string }\n | { t: \"p\"; hb: number }\n | { t: \"q\" }\n | { t: \"a\"; n: number };\n// Declared here, not imported from webrun-http-browser/sw: its declarations name ServiceWorker\n// types, and cellb's must compile with the DOM lib alone (scripts/pack-check.mjs). The two are\n// kept the same by test/p07w-websocket.test.ts.\n\n/** What a message adds to the sender's `bufferedAmount` (WHATWG: UTF-8 bytes of a string). */\nexport function byteSize(d: string | ArrayBuffer): number {\n return typeof d === \"string\" ? new TextEncoder().encode(d).byteLength : d.byteLength;\n}\n\nexport function toFrameData(data: unknown): string | ArrayBuffer {\n if (typeof data === \"string\") return data;\n if (data instanceof ArrayBuffer) return data.slice(0);\n if (ArrayBuffer.isView(data))\n return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\n throw new TypeError(\n \"WebSocket.send: unsupported data type (string, ArrayBuffer or ArrayBufferView)\",\n );\n}\n\nexport function checkClose(code?: number, reason?: string): void {\n if (code !== undefined && code !== 1000 && (code < 3000 || code > 4999))\n throw new DOMException(\n `The close code must be either 1000, or between 3000 and 4999. ${code} is neither.`,\n \"InvalidAccessError\",\n );\n if (reason !== undefined && new TextEncoder().encode(reason).byteLength > 123)\n throw new DOMException(\n \"The close reason must not be greater than 123 UTF-8 bytes.\",\n \"SyntaxError\",\n );\n}\n\n/** CloseEvent is not a global in every runtime (Node 22): a compatible Event subclass. */\nexport class CellsCloseEvent extends Event {\n readonly code: number;\n readonly reason: string;\n readonly wasClean: boolean;\n constructor(type: string, init: { code?: number; reason?: string; wasClean?: boolean } = {}) {\n super(type);\n this.code = init.code ?? 1005;\n this.reason = init.reason ?? \"\";\n this.wasClean = init.wasClean ?? false;\n }\n}\n\n/** One side of a frame relay: a MessagePort, or a port of a multiplexer. */\nexport interface FrameEnd {\n post(f: Frame): void;\n listen(fn: (f: Frame) => void): void;\n close(): void;\n}\n\nexport const portEnd = (port: MessagePort): FrameEnd => ({\n post: (f) => port.postMessage(f),\n listen: (fn) => {\n port.onmessage = (e) => fn(e.data as Frame);\n },\n close: () => port.close(),\n});\n\n/**\n * Relays a socket's frames between `a` and `b`. Once a close frame has passed each way (the\n * closing handshake) both ends are closed, after a moment for the last frames to arrive.\n * Returns a function that ends the relay at once, telling `a` the socket closed with `code`;\n * `onDone` runs when the relay ends either way.\n */\nexport function relayFrames(\n a: FrameEnd,\n b: FrameEnd,\n onDone?: () => void,\n): (code: number, reason: string) => void {\n let fromA = false;\n let fromB = false;\n let done = false;\n const finish = () => {\n if (done) return;\n done = true;\n onDone?.();\n const t = setTimeout(() => {\n a.close();\n b.close();\n }, 1_000);\n (t as { unref?(): void }).unref?.();\n };\n a.listen((f) => {\n b.post(f);\n if (f.t !== \"c\") return;\n fromA = true;\n if (fromB) finish();\n });\n b.listen((f) => {\n a.post(f);\n if (f.t !== \"c\") return;\n fromB = true;\n if (fromA) finish();\n });\n return (code, reason) => {\n if (done) return;\n a.post({ t: \"c\", code, reason });\n finish();\n };\n}\n"]}
|