@statewalker/cellb 0.2.0 → 0.3.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.
@@ -1,100 +1,42 @@
1
- import { fetchOverDuplex, serveFetchOverDuplex } from "@statewalker/webrun-http-streams";
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 (they are released when an exchange
7
- * ends, is cancelled or has no body: fetchClosing, servingClosing; p31-port-leak.test.ts).
8
- * `globalThis.__cellbMaxPorts` overrides it (tests that check nothing leaks set it low).
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
9
  */
10
10
  const maxPorts = () => globalThis.__cellbMaxPorts ?? 4096;
11
11
  /**
12
- * Cell-host side: serves the runtime's HTTP surface over a port with
13
- * webrun-rpc (mux + backpressure) and webrun-http-streams (HTTP/1.1 on the
14
- * Duplex, streaming bodies). Resolves to an async disposer.
12
+ * The links are webrun-http-browser's (`connectHttpLink` / `serveHttpLink`): HTTP/1.1 with
13
+ * streaming bodies over webrun-rpc calls, one multiplexed port per exchange. They take a
14
+ * MessagePort; a worker_threads port has the same `postMessage` / `addEventListener` surface.
15
+ */
16
+ const asPort = (port) => port;
17
+ /**
18
+ * Cell-host side: serves the runtime's HTTP surface over a port. Resolves to an async
19
+ * disposer.
15
20
  */
16
21
  export function serveRuntime(port, runtime, opts) {
17
- return serve({ mux: overPipe(port, { codec: structuredCodec, side: "responder", maxPorts: maxPorts() }) }, servingClosing(serveFetchOverDuplex(cellsHttpHandler(runtime, opts))));
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
- };
22
+ return serveHttpLink(asPort(port), cellsHttpHandler(runtime, opts), { maxPorts: maxPorts() });
77
23
  }
78
24
  /** Front side (page, SW bridge, another tab): a RemoteRuntime over a port. */
79
25
  export async function connectRuntime(port, opts) {
80
- const { call, close } = await connect({
81
- mux: overPipe(port, { codec: structuredCodec, side: "initiator", maxPorts: maxPorts() }),
82
- });
83
- const remote = await remoteRuntime((req) => fetchClosing(call, req), opts);
84
- return Object.assign(remote, { close: async () => void (await close()) });
26
+ const link = await connectHttpLink(asPort(port), { maxPorts: maxPorts() });
27
+ const remote = await remoteRuntime((req) => link.send(req), opts);
28
+ return Object.assign(remote, { close: () => link.close() });
85
29
  }
86
30
  /** Serves any HTTP handler over a port (responder side of a link). */
87
31
  export function serveHandlerOverPort(port, handler) {
88
- return serve({ mux: overPipe(port, { codec: structuredCodec, side: "responder", maxPorts: maxPorts() }) }, servingClosing(serveFetchOverDuplex(handler)));
32
+ return serveHttpLink(asPort(port), handler, { maxPorts: maxPorts() });
89
33
  }
90
34
  /** A `send` over a port (initiator side of a link); the scheme rides in a header. */
91
35
  export async function linkOverPort(port) {
92
- const { call, close } = await connect({
93
- mux: overPipe(port, { codec: structuredCodec, side: "initiator", maxPorts: maxPorts() }),
94
- });
36
+ const link = await connectHttpLink(asPort(port), { maxPorts: maxPorts() });
95
37
  return {
96
- send: (req) => fetchClosing(call, withScheme(req)),
97
- close: async () => void (await close()),
38
+ send: (req) => link.send(withScheme(req)),
39
+ close: () => link.close(),
98
40
  };
99
41
  }
100
42
  //# 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,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACzF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAEpF,OAAO,EAEL,gBAAgB,EAEhB,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAa,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAExD;;;;GAIG;AACH,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAE,UAA2C,CAAC,eAAe,IAAI,IAAI,CAAC;AAQ5F;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAc,EACd,OAAoB,EACpB,IAAuB;IAEvB,OAAO,KAAK,CACV,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,EAC5F,cAAc,CAAC,oBAAoB,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CACtC,CAAC;AACpC,CAAC;AAQD,KAAK,UAAU,YAAY,CAAC,IAAU,EAAE,GAAY;IAClD,IAAI,MAA0C,CAAC;IAC/C,MAAM,GAAG,GAAG,MAAM,eAAe,CAC/B,CAAC,CAAC,KAAc,EAAE,EAAE;QAClB,MAAM,GAAI,IAA+C,CAAC,KAAK,CAAC,CAAC;QACjE,OAAO,MAAM,CAAC;IAChB,CAAC,CAAS,EACV,GAAG,CACJ,CAAC;IACF,MAAM,KAAK,GAAG,GAAG,EAAE,CACjB,KAAK,CAAC,KAAK,IAAI,EAAE;QACf,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,CAAC,IAAI,MAAgC;gBAAC,CAAC;QAC1D,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACZ,CAAC,CAAC,EAAE,CAAC;IACP,IAAI,CAAC,MAAM;QAAE,OAAO,GAAG,CAAC;IACxB,IAAI,GAAG,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QACtB,KAAK,EAAE,CAAC;QACR,OAAO,GAAG,CAAC;IACb,CAAC;IACD,2FAA2F;IAC3F,yEAAyE;IACzE,MAAM,MAAM,GAAI,GAAG,CAAC,IAAmC,CAAC,SAAS,EAAE,CAAC;IACpE,MAAM,IAAI,GAAG,IAAI,cAAc,CAAa;QAC1C,KAAK,CAAC,IAAI,CAAC,GAAG;YACZ,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC9B,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;gBACX,GAAG,CAAC,KAAK,EAAE,CAAC;gBACZ,KAAK,EAAE,CAAC;YACV,CAAC;;gBAAM,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,MAAM;YACjB,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC5C,MAAO,MAAkC,CAAC,MAAM,EAAE,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAClF,CAAC;KACF,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE;QAC5C,SAAS,EAAG,GAA+B,CAAC,SAAS,IAAI,IAAI;KAC9D,CAAC,CAAC;AACL,CAAC;AAQD,SAAS,cAAc,CAAC,MAAc;IACpC,OAAO,KAAK,SAAS,CAAC,EAAE,KAA6B;QACnD,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,GAA2B,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1E,IAAI,CAAC;YACH,KAAK,CAAC,CAAE,MAAgE,CAAC,IAAI,CAAC,CAAC;QACjF,CAAC;gBAAS,CAAC;YACT,KAAK,CAAC,KAAK,IAAI,EAAE;gBACf,IAAI,CAAC;oBACH,KAAK,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE;wBAAC,CAAC;gBAC9D,CAAC;gBAAC,MAAM,CAAC,CAAA,CAAC;YACZ,CAAC,CAAC,EAAE,CAAC;QACP,CAAC;IACH,CAAsB,CAAC;AACzB,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAc,EACd,IAAuB;IAEvB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,OAAO,CAAC;QACpC,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC;KACzF,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;IAC3E,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,oBAAoB,CAAC,IAAc,EAAE,OAAa;IAChE,OAAO,KAAK,CACV,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,EAC5F,cAAc,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CACd,CAAC;AACpC,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAc;IAEd,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,OAAO,CAAC;QACpC,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC;KACzF,CAAC,CAAC;IACH,OAAO;QACL,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;QAClD,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,EAAE,CAAC;KACxC,CAAC;AACJ,CAAC","sourcesContent":["import { fetchOverDuplex, serveFetchOverDuplex } from \"@statewalker/webrun-http-streams\";\nimport { connect, overPipe, serve, structuredCodec } 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 (they are released when an exchange\n * ends, is cancelled or has no body: fetchClosing, servingClosing; p31-port-leak.test.ts).\n * `globalThis.__cellbMaxPorts` overrides it (tests that check nothing leaks set it low).\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 * Cell-host side: serves the runtime's HTTP surface over a port with\n * webrun-rpc (mux + backpressure) and webrun-http-streams (HTTP/1.1 on the\n * Duplex, streaming bodies). Resolves to an async disposer.\n */\nexport function serveRuntime(\n port: CellPort,\n runtime: CellRuntime,\n opts?: CellsHttpOptions,\n): Promise<() => Promise<void>> {\n return serve(\n { mux: overPipe(port, { codec: structuredCodec, side: \"responder\", maxPorts: maxPorts() }) },\n servingClosing(serveFetchOverDuplex(cellsHttpHandler(runtime, opts))),\n ) as Promise<() => Promise<void>>;\n}\n\n/**\n * fetchOverDuplex, draining the call's output when the response has no body (HEAD, 204, 304…):\n * webrun-http-streams then returns without reading the exchange to its end, and the call's\n * multiplexed port stayed open, one per such request, until `maxPorts` (1024) refused calls.\n */\ntype Call = Parameters<typeof fetchOverDuplex>[0];\nasync function fetchClosing(call: Call, req: Request): Promise<Response> {\n let output: AsyncIterable<unknown> | undefined;\n const res = await fetchOverDuplex(\n ((input: unknown) => {\n output = (call as (i: unknown) => AsyncIterable<unknown>)(input);\n return output;\n }) as Call,\n req,\n );\n const drain = () =>\n void (async () => {\n try {\n for await (const _ of output as AsyncIterable<unknown>);\n } catch {}\n })();\n if (!output) return res;\n if (res.body === null) {\n drain();\n return res;\n }\n // The same once the body is read to its end, or cancelled by its reader (then the exchange\n // is abandoned: the call's iterator is returned, which closes its port).\n const reader = (res.body as ReadableStream<Uint8Array>).getReader();\n const body = new ReadableStream<Uint8Array>({\n async pull(ctl) {\n const r = await reader.read();\n if (r.done) {\n ctl.close();\n drain();\n } else ctl.enqueue(r.value);\n },\n async cancel(reason) {\n await reader.cancel(reason).catch(() => {});\n await (output as AsyncGenerator<unknown>).return?.(undefined).catch?.(() => {});\n },\n });\n return Object.assign(new Response(body, res), {\n webSocket: (res as { webSocket?: unknown }).webSocket ?? null,\n });\n}\n\n/**\n * serveFetchOverDuplex, draining what is left of the request side once the response is out:\n * a handler that does not read the request body (a GET) left the exchange half open, and its\n * multiplexed port open, one per request.\n */\ntype Duplex = ReturnType<typeof serveFetchOverDuplex>;\nfunction servingClosing(duplex: Duplex): Duplex {\n return async function* (input: AsyncIterable<unknown>) {\n const it = input[Symbol.asyncIterator]();\n const rest: AsyncIterable<unknown> = { [Symbol.asyncIterator]: () => it };\n try {\n yield* (duplex as (i: AsyncIterable<unknown>) => AsyncIterable<unknown>)(rest);\n } finally {\n void (async () => {\n try {\n for (let r = await it.next(); !r.done; r = await it.next());\n } catch {}\n })();\n }\n } as unknown as Duplex;\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 { call, close } = await connect({\n mux: overPipe(port, { codec: structuredCodec, side: \"initiator\", maxPorts: maxPorts() }),\n });\n const remote = await remoteRuntime((req) => fetchClosing(call, req), opts);\n return Object.assign(remote, { close: async () => void (await 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 serve(\n { mux: overPipe(port, { codec: structuredCodec, side: \"responder\", maxPorts: maxPorts() }) },\n servingClosing(serveFetchOverDuplex(handler)),\n ) as Promise<() => Promise<void>>;\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 { call, close } = await connect({\n mux: overPipe(port, { codec: structuredCodec, side: \"initiator\", maxPorts: maxPorts() }),\n });\n return {\n send: (req) => fetchClosing(call, withScheme(req)),\n close: async () => void (await close()),\n };\n}\n"]}
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;;;;;GAKG;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 */\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"]}
@@ -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
- /** Liveness ping period (ms); 0 disables. The peer is declared dead after 2 missed periods. */
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
- * Interval timers for the heartbeat. On a page's main thread pass worker-timers'
11
- * `setInterval`/`clearInterval`: background-tab throttling of main-thread timers would
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
- setInterval(fn: () => void, ms: number): unknown;
17
- clearInterval(id: unknown): void;
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`. Typed as the
22
- * platform's `WebSocket` (the class implements that interface; its own state is private).
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 { byteSize, CellsCloseEvent, checkClose, DEFAULT_HEARTBEAT_MS, toFrameData, } from "./frames.js";
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-WebSocket-compatible class whose transport is a MessagePort obtained
4
- * from `connect` (page → ServiceWorker → owning shard worker, or in-process).
5
- * Installed for user code through webrun-modules `~deps` (provided `WebSocket`),
6
- * so the real `globalThis.WebSocket` is left untouched.
7
- */
8
- /** RFC 7230 token (subprotocol names). */
9
- const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
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 heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
16
- const timers = options.timers ?? {
17
- setInterval: (fn, ms) => setInterval(fn, ms),
18
- clearInterval: (id) => clearInterval(id),
19
- };
20
- class CellsWebSocket extends EventTarget {
21
- static CONNECTING = 0;
22
- static OPEN = 1;
23
- static CLOSING = 2;
24
- static CLOSED = 3;
25
- CONNECTING = 0;
26
- OPEN = 1;
27
- CLOSING = 2;
28
- CLOSED = 3;
29
- url;
30
- readyState = 0;
31
- protocol = "";
32
- extensions = "";
33
- #buffered = 0;
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
- if (!TOKEN.test(p))
97
- throw new DOMException(`The subprotocol '${p}' is invalid.`, "SyntaxError");
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(p.toLowerCase());
42
+ seen.add(key);
101
43
  }
102
- this.url = u.toString();
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
@@ -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"]}
@@ -1,4 +1,19 @@
1
- /** Wire protocol of a WebSocket carried by a MessagePort (both directions). */
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
- * MessagePorts never report that the other side died (no `close` event in
3
- * browsers), so both ends run a heartbeat: the client pings every `hb` ms and
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;
@@ -1 +1 @@
1
- {"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/ws/frames.ts"],"names":[],"mappings":"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","sourcesContent":["/** Wire protocol of a WebSocket carried by a MessagePort (both directions). */\nexport type Frame =\n | { t: \"m\"; d: string | ArrayBuffer } // message\n | { t: \"c\"; code: number; reason: string } // close (initiating or echo)\n | { t: \"p\"; hb: number } // ping (client → server), carries the heartbeat period\n | { t: \"q\" } // pong (server → client)\n | { t: \"a\" }; // ack: the server end dispatched the oldest message not acknowledged yet\n\n/**\n * MessagePorts never report that the other side died (no `close` event in\n * browsers), so both ends run a heartbeat: the client pings every `hb` ms and\n * closes with 1006 when pongs stop for 2·hb; the server closes with 1006 when\n * pings stop for 3·hb. This is what makes a socket notice a closed tab.\n */\nexport const DEFAULT_HEARTBEAT_MS = 10_000;\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"]}
1
+ {"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/ws/frames.ts"],"names":[],"mappings":"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","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"]}